Skip to content

Commit

Permalink
watcher-nodejs: first draft of the watcher, for node js
Browse files Browse the repository at this point in the history
  • Loading branch information
Will committed Jul 19, 2024
1 parent 09bf85f commit d2299ac
Show file tree
Hide file tree
Showing 10 changed files with 5,459 additions and 1 deletion.
2 changes: 1 addition & 1 deletion libcwatcher/src/watcher-cabi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void* wtr_watcher_open(
{
auto wrapped_callback = [callback, context](wtr::watcher::event ev_owned)
{
wtr_watcher_event ev_view = {0};
wtr_watcher_event ev_view = {};
#ifdef _WIN32
char path_name[PATH_BUF_LEN] = {0};
char associated_path_name[PATH_BUF_LEN] = {0};
Expand Down
131 changes: 131 additions & 0 deletions watcher-nodejs/.clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
AccessModifierOffset: -2
AlignAfterOpenBracket: AlwaysBreak
AlignArrayOfStructures: Right
# AlignConsecutiveAssignments: None
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: true
AcrossComments: true
# AlignConsecutiveDeclarations: None
# AlignConsecutiveMacros
AlignEscapedNewlines: Left
AlignOperands: AlignAfterOperator
AlignTrailingComments: true
# [v16+]
# AlignTrailingComments:
# Kind: Always
# OverEmptyLines: 2
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always # Empty
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All # Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All # Empty
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BitFieldColonSpacing: Both
# [v16+]
# BreakAfterAttributes: Always
BreakBeforeBinaryOperators: NonAssignment # All
# break before function scope (incl. lambdas) and else statements
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
# [v16+]
AfterControlStatement: Never
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: true
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeConceptDeclarations: Always
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeComma
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
ColumnLimit: 120
# CommentPragmas regex
CompactNamespaces: false
ContinuationIndentWidth: 2
Cpp11BracedListStyle: true
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: Always
FixNamespaceComments: false
IncludeBlocks: Merge
# IncludeCategories regex [maybe useful for tool/hone]
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: true
IndentExternBlock: false
IndentGotoLabels: true
# IndentPPDirectives: BeforeHash [messes up includes, nice w/modules]
IndentRequiresClause: false
IndentWidth: 2
# [v16+]
# InsertNewlineAtEOF: true
# [v16+]
# IntegerLiteralSeparator:
# Binary: 4
# Decimal: 3
# Hex: 2
# [v16+]
# LineEnding: LF
NamespaceIndentation: None
PackConstructorInitializers: Never
PenaltyBreakOpenParenthesis: 0
PenaltyBreakBeforeFirstCallParameter: 0
PointerAlignment: Left
QualifierAlignment: Custom
# [bad docs? 'friend' does not work]
QualifierOrder: ['inline', 'static', 'constexpr', 'volatile', 'restrict', 'type', 'const']
ReferenceAlignment: Left
ReflowComments: true
RequiresClausePosition: OwnLine
# [v16+]
# RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Always
SortIncludes: CaseInsensitive
# SortIncludes: true
# [bad docs?]
# SortUsingDeclarations: LexicographicNumeric
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: true # false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: true
SpaceBeforeCpp11BracedList: false # true
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false # true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInAngles: Never
SpacesInCStyleCastParentheses: false
# SpacesInLineCommentPrefix
SpacesInSquareBrackets: false
Standard: Latest
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements # Never
TabWidth: 2
UseTab: Never
2 changes: 2 additions & 0 deletions watcher-nodejs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
node_modules
20 changes: 20 additions & 0 deletions watcher-nodejs/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"targets": [
{
"target_name": "watcher",
"sources": [
"../libcwatcher/src/watcher-cabi.cpp",
"cwatcher_wrapper.cpp",
],
"include_dirs": [
"../libcwatcher/include",
],
"link_settings": {
"libraries": [
"-Wl,-rpath,/usr/local/lib",
]
}
}
]
}

160 changes: 160 additions & 0 deletions watcher-nodejs/cwatcher_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#define NAPI_VERSION 8
#include "wtr/watcher-cabi.h"
#include <node_api.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define WITH_TSFN_NONBLOCKING 1

/* Owns:
- A "handle" to a watch object,
which we can give back to the watcher
when we want to close it.
- A (thread-safe) function, storing the
user's callback, which we can call when
an event is ready.
- A reference to the user's callback, which
we release when the watcher is closed. */
struct WatcherWrapper {
napi_ref js_callback_ref = NULL;
napi_threadsafe_function tsfn = NULL;
void* watcher = NULL;
};

/* Called by the callback bridge when an event is ready.
Forwards event to js-land for the user.
Expects an "owned" event, which will be freed in this scope. */
static void callback_js_receiver(napi_env env, napi_value js_callback, void* _unused, void* ctx)
{
wtr_watcher_event* event = (wtr_watcher_event*)ctx;
napi_value event_obj;
napi_create_object(env, &event_obj);
napi_value path_name, effect_type, path_type, effect_time, associated_path_name;
napi_create_string_utf8(env, event->path_name, NAPI_AUTO_LENGTH, &path_name);
napi_create_int32(env, event->effect_type, &effect_type);
napi_create_int32(env, event->path_type, &path_type);
napi_create_bigint_int64(env, event->effect_time, &effect_time);
napi_set_named_property(env, event_obj, "pathName", path_name);
napi_set_named_property(env, event_obj, "effectType", effect_type);
napi_set_named_property(env, event_obj, "pathType", path_type);
napi_set_named_property(env, event_obj, "effectTime", effect_time);
if (event->associated_path_name) {
napi_create_string_utf8(env, event->associated_path_name, NAPI_AUTO_LENGTH, &associated_path_name);
napi_set_named_property(env, event_obj, "associatedPathName", associated_path_name);
}
napi_value global;
napi_get_global(env, &global);
napi_value result;
napi_call_function(env, global, js_callback, 1, &event_obj, &result);
#if WITH_TSFN_NONBLOCKING
free((void*)event->path_name);
free((void*)event->associated_path_name);
free(event);
#endif
}

/* Called by the watcher when an event is ready.
Passes a newly allocated event to the wrapper's stored function.
Expects the event it to be freed in the wrapper's stored function. */
static void callback_bridge(struct wtr_watcher_event event_view, void* ctx)
{
WatcherWrapper* wrapper = (WatcherWrapper*)ctx;
#if WITH_TSFN_NONBLOCKING
wtr_watcher_event* event_owned = (wtr_watcher_event*)malloc(sizeof(wtr_watcher_event));
event_owned->path_name = event_view.path_name ? strdup(event_view.path_name) : NULL;
event_owned->effect_type = event_view.effect_type;
event_owned->path_type = event_view.path_type;
event_owned->effect_time = event_view.effect_time;
event_owned->associated_path_name = event_view.associated_path_name ? strdup(event_view.associated_path_name) : NULL;
if (wrapper->tsfn) {
napi_call_threadsafe_function(wrapper->tsfn, event_owned, napi_tsfn_nonblocking);
#else
napi_call_threadsafe_function(wrapper->tsfn, &event_view, napi_tsfn_blocking);
#endif
}
}

/* Should be called by the user when they are done with the watcher.
Ends event processing and releases any owned resources. */
static napi_value close(napi_env env, napi_callback_info func_arg_info)
{
void* ctx;
napi_get_cb_info(env, func_arg_info, NULL, NULL, NULL, &ctx);
WatcherWrapper* wrapper = (WatcherWrapper*)ctx;
if (wrapper->tsfn) {
napi_release_threadsafe_function(wrapper->tsfn, napi_tsfn_release);
wrapper->tsfn = NULL;
}
if (wrapper->js_callback_ref) {
napi_delete_reference(env, wrapper->js_callback_ref);
wrapper->js_callback_ref = NULL;
}
bool closed_ok = wtr_watcher_close(wrapper->watcher);
free(wrapper);
wrapper->watcher = NULL;
napi_value result;
napi_get_boolean(env, closed_ok, &result);
return result;
}

/* Opens a watcher on a path (and any children).
Calls the provided callback when events happen.
Accepts two arguments, a path and a callback.
Returns an object with a single method: close.
Call `.close()` when you don't want to watch
things anymore. */
static napi_value watch(napi_env env, napi_callback_info func_arg_info)
{
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, func_arg_info, &argc, args, NULL, NULL);
if (argc != 2) {
napi_throw_error(env, NULL, "Wrong number of arguments");
return NULL;
}
char path[4096];
size_t path_len;
napi_get_value_string_utf8(env, args[0], path, sizeof(path), &path_len);
WatcherWrapper* wrapper = (WatcherWrapper*)malloc(sizeof(WatcherWrapper));
napi_value js_callback = args[1];
napi_create_reference(env, js_callback, 1, &wrapper->js_callback_ref);
napi_value work_name;
napi_create_string_utf8(env, "Watcher", NAPI_AUTO_LENGTH, &work_name);
size_t work_queue_size = 1024;
napi_create_threadsafe_function(
env,
js_callback,
NULL,
work_name,
work_queue_size,
1,
wrapper,
NULL,
NULL,
callback_js_receiver,
&wrapper->tsfn);
wrapper->watcher = wtr_watcher_open(path, callback_bridge, wrapper);
if (wrapper->watcher == NULL) {
napi_throw_error(env, NULL, "Failed to open watcher");
return NULL;
}
napi_value watcher_obj;
napi_create_object(env, &watcher_obj);
napi_value close_func;
napi_create_function(env, "close", NAPI_AUTO_LENGTH, close, wrapper, &close_func);
napi_set_named_property(env, watcher_obj, "close", close_func);
napi_wrap(env, watcher_obj, wrapper, NULL, NULL, NULL);
return watcher_obj;
}

/* Module initialization. (Does this work as a non-CommonJS module?) */
static napi_value mod_init(napi_env env, napi_value exports)
{
napi_value watch_func;
napi_create_function(env, NULL, 0, watch, NULL, &watch_func);
napi_set_named_property(env, exports, "watch", watch_func);
return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, mod_init)
12 changes: 12 additions & 0 deletions watcher-nodejs/example-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const watcher = require('./build/Debug/watcher');

const path = process.argv[2] || '.';
var w = watcher.watch(path, (event) => {
console.log(event);
});

process.stdin.resume();
process.stdin.on('data', () => {
w.close();
process.exit();
});
Loading

0 comments on commit d2299ac

Please sign in to comment.