diff --git a/binding.gyp b/binding.gyp index 3e8f35b..7742acf 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,13 +1,32 @@ { "targets": [{ "target_name": "epoll", + "cflags!": [ "-fno-exceptions" ], + "cflags_cc!": [ "-fno-exceptions" ], + "xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES", + "CLANG_CXX_LIBRARY": "libc++", + "MACOSX_DEPLOYMENT_TARGET": "10.7", + }, + "msvs_settings": { + "VCCLCompilerTool": { "ExceptionHandling": 1 }, + }, + "cflags!": [ "-fno-exceptions" ], + "cflags_cc!": [ "-fno-exceptions" ], + "xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES", + "CLANG_CXX_LIBRARY": "libc++", + "MACOSX_DEPLOYMENT_TARGET": "10.7", + }, + "msvs_settings": { + "VCCLCompilerTool": { "ExceptionHandling": 1 }, + }, "conditions": [[ 'OS == "linux"', { - "include_dirs" : [ - " void + ); + + get closed(): boolean; + + add(fd: number, events: number): Epoll; + close(): void; + remove(fd: number): Epoll; + modify(fd: number, events: number): Epoll; + + static EPOLLIN: number; + static EPOLLOUT: number; + static EPOLLRDHUP: number; + static EPOLLPRI: number; + static EPOLLERR: number; + static EPOLLHUP: number; + static EPOLLET: number; + static EPOLLONESHOT: number; +} + +// TODO - should it export as possibly null? +// export const Epoll: EpollClass | null; diff --git a/package.json b/package.json index 8b7acc5..0944ae5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epoll", - "version": "4.0.2", + "version": "5.0.0-0", "description": "A low-level Node.js binding for the Linux epoll API", "main": "epoll.js", "directories": { @@ -17,11 +17,11 @@ "url": "https://github.com/fivdi/epoll.git" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0" }, "dependencies": { "bindings": "^1.5.0", - "nan": "^2.17.0" + "node-addon-api": "^7.1.0" }, "devDependencies": { "jshint": "^2.13.6" diff --git a/src/epoll.cc b/src/epoll.cc index 17ee317..e659156 100644 --- a/src/epoll.cc +++ b/src/epoll.cc @@ -1,377 +1,290 @@ #ifdef __linux__ #include -#include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include #include "epoll.h" -// TODO - strerror isn't threadsafe, use strerror_r instead -// TODO - use uv_strerror rather than strerror_r for libuv errors? - -static int epfd_g; - -static uv_sem_t sem_g; -static uv_async_t async_g; - -static struct epoll_event event_g; -static int errno_g; - - -/* - * Watcher thread - */ -static void *watcher(void *arg) { - int count; - - while (true) { - // Wait till the event loop says it's ok to poll. The semaphore serves more - // than one purpose. - // - It synchronizing access to '1 element queue' in variables - // event_g and errno_g. - // - When level-triggered epoll is used, the default when EPOLLET isn't - // specified, the event triggered by the last call to epoll_wait may be - // trigged again and again if the condition that triggered it hasn't been - // cleared yet. Waiting prevents multiple triggers for the same event. - // - It forces a context switch from the watcher thread to the event loop - // thread. - uv_sem_wait(&sem_g); - - do { - count = epoll_wait(epfd_g, &event_g, 1, -1); - } while (count == -1 && errno == EINTR); - - errno_g = count == -1 ? errno : 0; - - // Errors returned from uv_async_send are silently ignored. - uv_async_send(&async_g); +#include + +namespace epoll +{ + // TODO - strerror isn't threadsafe, use strerror_r instead + // TODO - use uv_strerror rather than strerror_r for libuv errors? + + Epoll::Epoll(const Napi::CallbackInfo &info) + : Napi::ObjectWrap(info), + async_context_(Napi::AsyncContext(info.Env(), "Epoll")), + closed_(false) + { + Napi::Env env = info.Env(); + + if (info.Length() < 1 || !info[0].IsFunction()) + { + Napi::Error::New(env, "First argument to construtor must be a callback").ThrowAsJavaScriptException(); + return; + } + + callback_ = Napi::Persistent(info[0].As()); + }; + + Epoll::~Epoll() + { + // The destructor is not guaranteed to be called, but we can perform the same cleanup which gets triggered manually elsewhere + + if (watcher_) + { + watcher_->Forget(this); + watcher_ = nullptr; + } + }; + + Napi::FunctionReference + Epoll::Init(const Napi::Env &env, Napi::Object exports) + { + // This method is used to hook the accessor and method callbacks + Napi::Function func = DefineClass(env, "Epoll", { + + InstanceMethod<&Epoll::Add>("add", static_cast(napi_writable | napi_configurable)), + // + InstanceMethod<&Epoll::Close>("close", static_cast(napi_writable | napi_configurable)), + // + InstanceMethod<&Epoll::Remove>("remove", static_cast(napi_writable | napi_configurable)), + // + InstanceMethod<&Epoll::Modify>("modify", static_cast(napi_writable | napi_configurable)), + // + InstanceAccessor<&Epoll::GetClosed>("closed", static_cast(napi_writable | napi_configurable)), + // StaticMethod<&Example::CreateNewItem>("CreateNewItem", static_cast(napi_writable | napi_configurable)), + + StaticValue("EPOLLIN", Napi::Number::New(env, EPOLLIN), napi_default), + StaticValue("EPOLLOUT", Napi::Number::New(env, EPOLLOUT), napi_default), + StaticValue("EPOLLRDHUP", Napi::Number::New(env, EPOLLRDHUP), napi_default), + StaticValue("EPOLLPRI", Napi::Number::New(env, EPOLLPRI), napi_default), + StaticValue("EPOLLERR", Napi::Number::New(env, EPOLLERR), napi_default), + StaticValue("EPOLLHUP", Napi::Number::New(env, EPOLLHUP), napi_default), + StaticValue("EPOLLET", Napi::Number::New(env, EPOLLET), napi_default), + StaticValue("EPOLLONESHOT", Napi::Number::New(env, EPOLLONESHOT), napi_default), + }); + + exports.Set("Epoll", func); + + return Napi::Persistent(func); } - return 0; -} - - -static int start_watcher() { - static bool watcher_started = false; - pthread_t theread_id; - - if (watcher_started) - return 0; - - epfd_g = epoll_create1(0); - if (epfd_g == -1) - return errno; + Napi::Value Epoll::Add(const Napi::CallbackInfo &info) + { + Napi::Env env = info.Env(); + + if (this->closed_) + { + Napi::Error::New(env, "add can't be called after calling close").ThrowAsJavaScriptException(); + return env.Null(); + } + + // Epoll.EPOLLET is -0x8000000 on ARM and an IsUint32 check fails so + // check for IsNumber instead. + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) + { + Napi::Error::New(env, "incorrect arguments passed to add" + "(int fd, int events)") + .ThrowAsJavaScriptException(); + return env.Null(); + } + + int fd = info[0].As().Int32Value(); + int events = info[1].As().Int32Value(); + + // Take a reference or create the watcher + if (!watcher_) + { + auto data = env.GetInstanceData(); + if (!data) + { + Napi::Error::New(env, "Library is not initialised correctly").ThrowAsJavaScriptException(); + return env.Null(); + } + + watcher_ = data->watcher.lock(); + if (!watcher_) + { + watcher_ = std::make_shared(env); + data->watcher = watcher_; + } + } + + int err = watcher_->Add(fd, events, this); + if (err != 0) + { + Napi::Error::New(env, strerror(err)).ThrowAsJavaScriptException(); + return env.Null(); + } - int err = uv_sem_init(&sem_g, 1); - if (err < 0) { - close(epfd_g); - return -err; - } + fds_.push_back(fd); - err = uv_async_init(uv_default_loop(), &async_g, Epoll::HandleEvent); - if (err < 0) { - close(epfd_g); - uv_sem_destroy(&sem_g); - return -err; + return info.This(); } - // Prevent async_g from keeping event loop alive, for the time being. - uv_unref((uv_handle_t *) &async_g); + Napi::Value Epoll::Modify(const Napi::CallbackInfo &info) + { + Napi::Env env = info.Env(); + + if (this->closed_) + { + Napi::Error::New(env, "modify can't be called after calling close").ThrowAsJavaScriptException(); + return env.Null(); + } + + // Epoll.EPOLLET is -0x8000000 on ARM and an IsUint32 check fails so + // check for IsNumber instead. + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) + { + Napi::Error::New(env, "incorrect arguments passed to modify" + "(int fd, int events)") + .ThrowAsJavaScriptException(); + return env.Null(); + } + + if (!watcher_) + { + Napi::Error::New(env, "modify can't be called when not watching a fd").ThrowAsJavaScriptException(); + return env.Null(); + } + + // TODO - validate this is watching fd + + int err = watcher_->Modify( + info[0].As().Int32Value(), + info[1].As().Int32Value()); + if (err != 0) + { + Napi::Error::New(env, strerror(err)).ThrowAsJavaScriptException(); + return env.Null(); + } - err = pthread_create(&theread_id, 0, watcher, 0); - if (err != 0) { - close(epfd_g); - uv_sem_destroy(&sem_g); - uv_close((uv_handle_t *) &async_g, 0); - return err; + return info.This(); } - watcher_started = true; - - return 0; -} - + Napi::Value Epoll::Remove(const Napi::CallbackInfo &info) + { + Napi::Env env = info.Env(); -/* - * Epoll - */ -Nan::Persistent Epoll::constructor; -std::map Epoll::fd2epoll; - - -Epoll::Epoll(Nan::Callback *callback) - : callback_(callback), closed_(false) { - async_resource_ = new Nan::AsyncResource("Epoll:DispatchEvent"); -}; - - -Epoll::~Epoll() { - // v8 decides when and if destructors are called. In particular, if the - // process is about to terminate, it's highly likely that destructors will - // not be called. This is therefore not the place for calling the likes of - // uv_unref, which, in general, must be called to terminate a process - // gracefully! - Nan::HandleScope scope; - if (callback_) delete callback_; - if (async_resource_) delete async_resource_; -}; - - -NAN_MODULE_INIT(Epoll::Init) { - // Constructor - v8::Local ctor = - Nan::New(Epoll::New); - ctor->SetClassName(Nan::New("Epoll").ToLocalChecked()); - ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Prototype - Nan::SetPrototypeMethod(ctor, "add", Add); - Nan::SetPrototypeMethod(ctor, "modify", Modify); - Nan::SetPrototypeMethod(ctor, "remove", Remove); - Nan::SetPrototypeMethod(ctor, "close", Close); - - v8::Local itpl = ctor->InstanceTemplate(); - Nan::SetAccessor(itpl, Nan::New("closed").ToLocalChecked(), - GetClosed); - - Nan::SetTemplate(ctor, Nan::New("EPOLLIN").ToLocalChecked(), - Nan::New(EPOLLIN), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLOUT").ToLocalChecked(), - Nan::New(EPOLLOUT), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLRDHUP").ToLocalChecked(), - Nan::New(EPOLLRDHUP), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLPRI").ToLocalChecked(), - Nan::New(EPOLLPRI), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLERR").ToLocalChecked(), - Nan::New(EPOLLERR), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLHUP").ToLocalChecked(), - Nan::New(EPOLLHUP), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLET").ToLocalChecked(), - Nan::New(EPOLLET), v8::ReadOnly); - Nan::SetTemplate(ctor, Nan::New("EPOLLONESHOT").ToLocalChecked(), - Nan::New(EPOLLONESHOT), v8::ReadOnly); - - constructor.Reset(Nan::GetFunction(ctor).ToLocalChecked()); - Nan::Set(target, Nan::New("Epoll").ToLocalChecked(), - Nan::GetFunction(ctor).ToLocalChecked()); - - // TODO - Is it a good idea to throw an exception here? - if (int err = start_watcher()) - Nan::ThrowError(strerror(err)); // TODO - use err also -} - - -NAN_METHOD(Epoll::New) { - if (info.Length() < 1 || !info[0]->IsFunction()) - return Nan::ThrowError("First argument to construtor must be a callback"); - - Nan::Callback *callback = new Nan::Callback(info[0].As()); - - Epoll *epoll = new Epoll(callback); - epoll->Wrap(info.This()); - - info.GetReturnValue().Set(info.This()); -} - - -NAN_METHOD(Epoll::Add) { - Epoll *epoll = ObjectWrap::Unwrap(info.This()); - - if (epoll->closed_) - return Nan::ThrowError("add can't be called after calling close"); + if (this->closed_) + { + Napi::Error::New(env, "remove can't be called after calling close").ThrowAsJavaScriptException(); + return env.Null(); + } - // Epoll.EPOLLET is -0x8000000 on ARM and an IsUint32 check fails so - // check for IsNumber instead. - if (info.Length() < 2 || !info[0]->IsInt32() || !info[1]->IsNumber()) - return Nan::ThrowError("incorrect arguments passed to add" - "(int fd, int events)"); - - int err = epoll->Add( - Nan::To(info[0]).FromJust(), - Nan::To(info[1]).FromJust() - ); - if (err != 0) - return Nan::ThrowError(strerror(err)); // TODO - use err also - - info.GetReturnValue().Set(info.This()); -} - - -NAN_METHOD(Epoll::Modify) { - Epoll *epoll = ObjectWrap::Unwrap(info.This()); - - if (epoll->closed_) - return Nan::ThrowError("modify can't be called after calling close"); - - // Epoll.EPOLLET is -0x8000000 on ARM and an IsUint32 check fails so - // check for IsNumber instead. - if (info.Length() < 2 || !info[0]->IsInt32() || !info[1]->IsNumber()) - return Nan::ThrowError("incorrect arguments passed to modify" - "(int fd, int events)"); - - int err = epoll->Modify( - Nan::To(info[0]).FromJust(), - Nan::To(info[1]).FromJust() - ); - if (err != 0) - return Nan::ThrowError(strerror(err)); // TODO - use err also - - info.GetReturnValue().Set(info.This()); -} + if (info.Length() < 1 || !info[0].IsNumber()) + { + Napi::Error::New(env, "incorrect arguments passed to remove(int fd)").ThrowAsJavaScriptException(); + return env.Null(); + } + int fd = info[0].As().Int32Value(); -NAN_METHOD(Epoll::Remove) { - Epoll *epoll = ObjectWrap::Unwrap(info.This()); + int err = watcher_->Remove(fd); - if (epoll->closed_) - return Nan::ThrowError("remove can't be called after calling close"); + fds_.remove(fd); + if (fds_.empty() && watcher_) + { + watcher_->Forget(this); + watcher_ = nullptr; + } - if (info.Length() < 1 || !info[0]->IsInt32()) - return Nan::ThrowError("incorrect arguments passed to remove(int fd)"); - - int err = epoll->Remove(Nan::To(info[0]).FromJust()); - if (err != 0) - return Nan::ThrowError(strerror(err)); // TODO - use err also - - info.GetReturnValue().Set(info.This()); -} - - -NAN_METHOD(Epoll::Close) { - Epoll *epoll = ObjectWrap::Unwrap(info.This()); - - if (epoll->closed_) - return Nan::ThrowError("close can't be called more than once"); - - int err = epoll->Close(); - if (err != 0) - return Nan::ThrowError(strerror(err)); // TODO - use err also - - info.GetReturnValue().SetNull(); -} - - -NAN_GETTER(Epoll::GetClosed) { - Epoll *epoll = ObjectWrap::Unwrap(info.This()); - - info.GetReturnValue().Set(Nan::New(epoll->closed_)); -} - - -int Epoll::Add(int fd, uint32_t events) { - struct epoll_event event; - event.events = events; - event.data.fd = fd; - - if (epoll_ctl(epfd_g, EPOLL_CTL_ADD, fd, &event) == -1) - return errno; - - fd2epoll.insert(std::pair(fd, this)); - fds_.push_back(fd); - - // Keep event loop alive. uv_unref called in Remove. - uv_ref((uv_handle_t *) &async_g); - - // Prevent GC for this instance. Unref called in Remove. - Ref(); - - return 0; -} - - -int Epoll::Modify(int fd, uint32_t events) { - struct epoll_event event; - event.events = events; - event.data.fd = fd; - - if (epoll_ctl(epfd_g, EPOLL_CTL_MOD, fd, &event) == -1) - return errno; + if (err != 0) + { + Napi::Error::New(env, strerror(err)).ThrowAsJavaScriptException(); + return env.Null(); + } - return 0; -} + return info.This(); + } + Napi::Value Epoll::Close(const Napi::CallbackInfo &info) + { + Napi::Env env = info.Env(); -int Epoll::Remove(int fd) { - if (epoll_ctl(epfd_g, EPOLL_CTL_DEL, fd, 0) == -1) - return errno; + if (this->closed_) + { + Napi::Error::New(env, "close can't be called more than once").ThrowAsJavaScriptException(); + return env.Null(); + } - fd2epoll.erase(fd); - fds_.remove(fd); + closed_ = true; - if (fd2epoll.empty()) - uv_unref((uv_handle_t *) &async_g); - Unref(); + int error = 0; - return 0; -} + for (int fd : fds_) + { + int err = watcher_->Remove(fd); + if (err != 0) + error = err; // TODO - This will only return one of many errors + } + if (watcher_) + { + watcher_->Forget(this); + watcher_ = nullptr; + } -int Epoll::Close() { - closed_ = true; + if (error != 0) + { + Napi::Error::New(env, strerror(error)).ThrowAsJavaScriptException(); + return env.Null(); + } - delete callback_; - callback_ = 0; + return env.Null(); + } - delete async_resource_; - async_resource_ = 0; + Napi::Value Epoll::GetClosed(const Napi::CallbackInfo &info) + { + return Napi::Boolean::New(info.Env(), this->closed_); + } - std::list::iterator it = fds_.begin(); - for (; it != fds_.end(); it = fds_.begin()) { - int err = Remove(*it); - if (err != 0) - return err; // TODO - Returning here may leave things messed up. + void Epoll::DispatchEvent(const Napi::Env &env, int err, struct epoll_event *event) + { + Napi::HandleScope scope(env); + + try + { + if (err) + { + callback_.MakeCallback(Value(), std::initializer_list{Napi::Error::New(env, strerror(err)).Value()}, async_context_); + } + else + { + callback_.MakeCallback(Value(), std::initializer_list{env.Null(), Napi::Number::New(env, event->data.fd), Napi::Number::New(env, event->events)}, + async_context_); + } + } + catch (...) + { + // TODO - what to do with this error? + } } - return 0; -} + Napi::Object Init(Napi::Env env, Napi::Object exports) + { + auto instanceData = new EpollInstanceData; + instanceData->epollContructor = Epoll::Init(env, exports); -void Epoll::HandleEvent(uv_async_t* handle) { - // This method is executed in the event loop thread. - // By the time flow of control arrives here the original Epoll instance that - // registered interest in the event may no longer have this interest. If - // this is the case, the event will be silently ignored. + // Store the constructor as the add-on instance data. This will allow this + // add-on to support multiple instances of itself running on multiple worker + // threads, as well as multiple instances of itself running in different + // contexts on the same thread. + // + // By default, the value set on the environment here will be destroyed when + // the add-on is unloaded using the `delete` operator, but it is also + // possible to supply a custom deleter. + env.SetInstanceData(instanceData); - std::map::iterator it = fd2epoll.find(event_g.data.fd); - if (it != fd2epoll.end()) { - it->second->DispatchEvent(errno_g, &event_g); + return exports; } - uv_sem_post(&sem_g); + NODE_API_MODULE(epoll, Init) } - -void Epoll::DispatchEvent(int err, struct epoll_event *event) { - Nan::HandleScope scope; - - if (err) { - v8::Local args[1] = { - v8::Exception::Error( - Nan::New(strerror(err)).ToLocalChecked() - ) - }; - callback_->Call(1, args, async_resource_); - } else { - v8::Local args[3] = { - Nan::Null(), - Nan::New(event->data.fd), - Nan::New(event->events) - }; - callback_->Call(3, args, async_resource_); - } -} - - -NODE_MODULE(epoll, Epoll::Init) - #endif - diff --git a/src/epoll.h b/src/epoll.h index 94522a4..596ef38 100644 --- a/src/epoll.h +++ b/src/epoll.h @@ -1,36 +1,42 @@ -#ifndef EPOLL_H -#define EPOLL_H +#pragma once -class Epoll : public Nan::ObjectWrap { +#define NAPI_VERSION 8 + +#include + +#include "watcher.h" + +namespace epoll +{ + struct EpollInstanceData + { + std::weak_ptr watcher; + Napi::FunctionReference epollContructor; + }; + + class Epoll : public Napi::ObjectWrap + { public: - static NAN_MODULE_INIT(Init); - static void HandleEvent(uv_async_t* handle); + static Napi::FunctionReference Init(const Napi::Env &env, Napi::Object exports); - private: - Epoll(Nan::Callback *callback); + Epoll(const Napi::CallbackInfo &info); ~Epoll(); - static NAN_METHOD(New); - static NAN_METHOD(Add); - static NAN_METHOD(Modify); - static NAN_METHOD(Remove); - static NAN_METHOD(Close); - static NAN_GETTER(GetClosed); - - int Add(int fd, uint32_t events); - int Modify(int fd, uint32_t events); - int Remove(int fd); - int Close(); - void DispatchEvent(int err, struct epoll_event *event); - - Nan::Callback *callback_; - Nan::AsyncResource *async_resource_; - std::list fds_; - bool closed_; + void DispatchEvent(const Napi::Env &env, int err, struct epoll_event *event); + + private: + Napi::Value Add(const Napi::CallbackInfo &info); + Napi::Value Modify(const Napi::CallbackInfo &info); + Napi::Value Remove(const Napi::CallbackInfo &info); + Napi::Value Close(const Napi::CallbackInfo &info); + Napi::Value GetClosed(const Napi::CallbackInfo &info); - static Nan::Persistent constructor; - static std::map fd2epoll; -}; + Napi::FunctionReference callback_; + Napi::AsyncContext async_context_; -#endif + std::list fds_; + bool closed_; + std::shared_ptr watcher_; + }; +} diff --git a/src/watcher.cc b/src/watcher.cc new file mode 100644 index 0000000..0deabae --- /dev/null +++ b/src/watcher.cc @@ -0,0 +1,214 @@ +#ifdef __linux__ + +#include +#include +#include +#include +#include +#include +#include "watcher.h" +#include "epoll.h" + +namespace epoll +{ + // Transform native data into JS data, passing it to the provided + // `callback` -- the TSFN's JavaScript function. + void CallJs(Napi::Env env, Napi::Function callback, Context *context, + DataType *data) + { + + // Is the JavaScript environment still available to call into, eg. the TSFN is + // not aborted + if (env != nullptr && context != nullptr && data != nullptr) + { + // This method is executed in the event loop thread. + // By the time flow of control arrives here the original Epoll instance that + // registered interest in the event may no longer have this interest. If + // this is the case, the event will be silently ignored. + + std::map::iterator it = context->fd2epoll.find(data->event.data.fd); + if (it != context->fd2epoll.end()) + { + it->second->DispatchEvent(env, data->error, &data->event); + } + } + + if (data != nullptr) + { + // We're finished with the data. + delete data; + } + } + + /* + * Epoll + */ + + EpollWatcher::EpollWatcher(const Napi::Env &env) + { + + auto epfd = epoll_create1(0); + if (epfd == -1) + { + Napi::Error::New(env, strerror(errno)).ThrowAsJavaScriptException(); + return; + } + + // Create a context that can be 'leaked' to the native thread, and cleaned up when the tsfn is destroyed + auto context = new WatcherContext; + this->context = context; + + context->epfd = epfd; + + // Create a native thread + context->nativeThread = std::thread([context] + { + + int count; + + struct DataType *data = new DataType; + + while (!context->abort_) + { + count = epoll_wait(context->epfd, &data->event, 1, 50); + if (context->abort_) + break; + + if (count == 0 || (count == -1 && errno == EINTR)) + continue; + + data->error = count == -1 ? errno : 0; + + // Block until the event loop has handled the call, to ensure there isn't a long queue for processing + // Old code said: + // Wait till the event loop says it's ok to poll. The semaphore serves more + // than one purpose. + // - When level-triggered epoll is used, the default when EPOLLET isn't + // specified, the event triggered by the last call to epoll_wait may be + // trigged again and again if the condition that triggered it hasn't been + // cleared yet. Waiting prevents multiple triggers for the same event. + // - It forces a context switch from the watcher thread to the event loop + // thread. + napi_status status = context->tsfn.BlockingCall(data); + + data = new DataType; + if (status != napi_ok) + { + // Ignore error + } + } + + delete data; + + // Release the thread-safe function + context->tsfn.Release(); }); + + // Create a ThreadSafeFunction + context->tsfn = TSFN::New( + env, + // callback, // JavaScript function called asynchronously + "Epoll:DispatchEvent", // Name + 1, // Queue size + 1, // Only one thread will use this initially + context, // context, + [&](Napi::Env, FinalizerDataType *, + Context *ctx) { // Finalizer used to clean threads up + if (ctx->nativeThread.joinable()) + { + ctx->nativeThread.join(); + } + delete ctx; + }); + }; + + EpollWatcher::~EpollWatcher() + { + // The destructor is not guaranteed to be called, but we can perform the same cleanup which gets triggered manually elsewhere + Cleanup(); + }; + + void EpollWatcher::Cleanup() + { + if (context->epfd != -1) + { + close(context->epfd); + context->epfd = -1; + } + + context->abort_ = true; + + context = nullptr; + } + + int EpollWatcher::Add(int fd, uint32_t events, Epoll *epoll) + { + if (context == nullptr) + return 111; + + if (context->fd2epoll.count(fd) > 0) + { + // Already being watched somewhere + return 111; + } + + struct epoll_event event; + event.events = events; + event.data.fd = fd; + + if (epoll_ctl(context->epfd, EPOLL_CTL_ADD, fd, &event) == -1) + return errno; + + context->fd2epoll.insert(std::pair(fd, epoll)); + + return 0; + } + + int EpollWatcher::Modify(int fd, uint32_t events) + { + if (context == nullptr) + return 111; + + struct epoll_event event; + event.events = events; + event.data.fd = fd; + + if (epoll_ctl(context->epfd, EPOLL_CTL_MOD, fd, &event) == -1) + return errno; + + return 0; + } + + int EpollWatcher::Remove(int fd) + { + if (context == nullptr) + return 111; + + if (epoll_ctl(context->epfd, EPOLL_CTL_DEL, fd, 0) == -1) + return errno; + + context->fd2epoll.erase(fd); + + return 0; + } + + void EpollWatcher::Forget(Epoll *epoll) + { + if (context == nullptr) + return; + + for (std::map::iterator it = context->fd2epoll.begin(); it != context->fd2epoll.end();) + { + if (it->second == epoll) + { + epoll_ctl(context->epfd, EPOLL_CTL_DEL, it->first, 0); + it = context->fd2epoll.erase(it); + } + else + { + ++it; + } + } + } + +} +#endif \ No newline at end of file diff --git a/src/watcher.h b/src/watcher.h new file mode 100644 index 0000000..ea2380d --- /dev/null +++ b/src/watcher.h @@ -0,0 +1,59 @@ +#pragma once + +#define NAPI_VERSION 8 + +#include + +#include +#include +#include + +namespace epoll +{ + class Epoll; // Declared later + class EpollWatcher; + + struct DataType + { + struct epoll_event event; + int error; + }; + + struct WatcherContext; + + using Context = WatcherContext; // Napi::Reference; + // using DataType = struct epoll_event; + void CallJs(Napi::Env env, Napi::Function callback, Context *context, DataType *data); + using TSFN = Napi::TypedThreadSafeFunction; + using FinalizerDataType = void; + + struct WatcherContext + { + std::atomic abort_ = {false}; + + int epfd; + std::map fd2epoll; + + std::thread nativeThread; + TSFN tsfn; + }; + + class EpollWatcher : public std::enable_shared_from_this + { + public: + EpollWatcher(const Napi::Env &env); + ~EpollWatcher(); + + int Add(int fd, uint32_t events, Epoll *epoll); + int Modify(int fd, uint32_t events); + int Remove(int fd); + void Forget(Epoll *epoll); + + void HandleEvent(const Napi::Env &env, DataType *event); + + private: + void Cleanup(); + + WatcherContext *context; + }; +} \ No newline at end of file