diff options
author | 2023-07-08 14:26:19 -0700 | |
---|---|---|
committer | 2023-07-08 14:26:19 -0700 | |
commit | aa8b832ef61ada31176d248e716074ff22bb9dee (patch) | |
tree | ef125622db8bb593a6b9860c7c00437a98d93733 | |
parent | fa632c33312ad3b4dca9e4bddaa2af1c7db07597 (diff) | |
download | bun-aa8b832ef61ada31176d248e716074ff22bb9dee.tar.gz bun-aa8b832ef61ada31176d248e716074ff22bb9dee.tar.zst bun-aa8b832ef61ada31176d248e716074ff22bb9dee.zip |
Implement `process.on("beforeExit", cb)` and `process.on("exit", cb)` (#3576)
* Support `process.on('beforeExit')` and `process.on('exit')`
* [bun:sqlite] Always call sqlite3_close on exit
* Update process.test.js
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | src/bun.js/bindings/Process.cpp | 189 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 86 | ||||
-rw-r--r-- | src/bun.js/bindings/sqlite/JSSQLStatement.h | 15 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 50 | ||||
-rw-r--r-- | src/bun.js/node/types.zig | 4 | ||||
-rw-r--r-- | src/bun_js.zig | 14 | ||||
-rw-r--r-- | test/js/node/process/process-exit-fixture.js | 16 | ||||
-rw-r--r-- | test/js/node/process/process-exitCode-fixture.js | 7 | ||||
-rw-r--r-- | test/js/node/process/process-exitCode-with-exit.js | 8 | ||||
-rw-r--r-- | test/js/node/process/process-onBeforeExit-fixture.js | 7 | ||||
-rw-r--r-- | test/js/node/process/process-onBeforeExit-keepAlive.js | 18 | ||||
-rw-r--r-- | test/js/node/process/process.test.js | 64 |
13 files changed, 402 insertions, 78 deletions
diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 6320deaf1..1d6b5d33a 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -42,6 +42,35 @@ static JSC_DECLARE_CUSTOM_GETTER(Process_getPID); static JSC_DECLARE_CUSTOM_GETTER(Process_getPPID); static JSC_DECLARE_HOST_FUNCTION(Process_functionCwd); +static bool processIsExiting = false; + +extern "C" uint8_t Bun__getExitCode(void*); +extern "C" uint8_t Bun__setExitCode(void*, uint8_t); +extern "C" void* Bun__getVM(); +extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); + +static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* process, int exitCode) +{ + + if (processIsExiting) + return; + processIsExiting = true; + auto& emitter = process->wrapped(); + auto& vm = globalObject->vm(); + + if (vm.hasTerminationRequest() || vm.hasExceptionsAfterHandlingTraps()) + return; + + auto event = Identifier::fromString(vm, "exit"_s); + if (!emitter.hasEventListeners(event)) { + return; + } + process->putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(true), 0); + + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + emitter.emit(event, arguments); +} static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int fd) { @@ -324,6 +353,29 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, extern "C" uint64_t Bun__readOriginTimer(void*); extern "C" double Bun__readOriginTimerStart(void*); +// https://github.com/nodejs/node/blob/1936160c31afc9780e4365de033789f39b7cbc0c/src/api/hooks.cc#L49 +extern "C" void Process__dispatchOnBeforeExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast<Process*>(globalObject->processObject()); + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + process->wrapped().emit(Identifier::fromString(globalObject->vm(), "beforeExit"_s), arguments); +} + +extern "C" void Process__dispatchOnExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast<Process*>(globalObject->processObject()); + dispatchExitInternal(globalObject, process, exitCode); +} + JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -336,14 +388,38 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, JSC_DEFINE_HOST_FUNCTION(Process_functionExit, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - if (callFrame->argumentCount() == 0) { - // TODO: exitCode - Bun__Process__exit(globalObject, 0); + auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); + uint8_t exitCode = 0; + JSValue arg0 = callFrame->argument(0); + if (arg0.isNumber()) { + if (!arg0.isInt32()) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int extiCode32 = arg0.toInt32(globalObject); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); + + if (extiCode32 < 0 || extiCode32 > 127) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer between 0 and 127"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + exitCode = static_cast<uint8_t>(extiCode32); + } else if (!arg0.isUndefinedOrNull()) { + throwTypeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); } else { - Bun__Process__exit(globalObject, callFrame->argument(0).toInt32(globalObject)); + exitCode = Bun__getExitCode(Bun__getVM()); + } + + auto* zigGlobal = jsDynamicCast<Zig::GlobalObject*>(globalObject); + if (UNLIKELY(!zigGlobal)) { + zigGlobal = Bun__getDefaultGlobal(); } - return JSC::JSValue::encode(JSC::jsUndefined()); + Process__dispatchOnExit(zigGlobal, exitCode); + Bun__Process__exit(zigGlobal, exitCode); } extern "C" uint64_t Bun__readOriginTimer(void*); @@ -391,18 +467,15 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, array->setIndexQuickly(vm, 1, JSC::jsNumber(nanoseconds)); return JSC::JSValue::encode(JSC::JSValue(array)); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionHRTimeBigInt); -static JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, +JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject_); return JSC::JSValue::encode(JSValue(JSC::JSBigInt::createFrom(globalObject, Bun__readOriginTimer(globalObject->bunVM())))); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionChdir); - -static JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, +JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); @@ -611,6 +684,46 @@ JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecArgvGetter, (JSC::JSGlobalObject * glob return ret; } +JSC_DEFINE_CUSTOM_GETTER(Process__getExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) +{ + Process* process = jsDynamicCast<Process*>(JSValue::decode(thisValue)); + if (!process) { + return JSValue::encode(jsUndefined()); + } + + return JSValue::encode(jsNumber(Bun__getExitCode(jsCast<Zig::GlobalObject*>(process->globalObject())->bunVM()))); +} +JSC_DEFINE_CUSTOM_SETTER(Process__setExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) +{ + Process* process = jsDynamicCast<Process*>(JSValue::decode(thisValue)); + if (!process) { + return false; + } + + auto throwScope = DECLARE_THROW_SCOPE(process->vm()); + JSValue exitCode = JSValue::decode(value); + if (!exitCode.isNumber()) { + throwTypeError(lexicalGlobalObject, throwScope, "exitCode must be a number"_s); + return false; + } + + if (!exitCode.isInt32()) { + throwRangeError(lexicalGlobalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int exitCodeInt = exitCode.toInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, false); + if (exitCodeInt < 0 || exitCodeInt > 127) { + throwRangeError(lexicalGlobalObject, throwScope, "exitCode must be between 0 and 127"_s); + return false; + } + + void* ptr = jsCast<Zig::GlobalObject*>(process->globalObject())->bunVM(); + Bun__setExitCode(ptr, static_cast<uint8_t>(exitCodeInt)); + return true; +} + JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecPathGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) { JSC::JSObject* thisObject = JSValue::decode(thisValue).getObject(); @@ -677,39 +790,42 @@ void Process::finishCreation(JSC::VM& vm) vm, clientData->builtinNames().versionsPublicName(), JSC::CustomGetterSetter::create(vm, Process_getVersionsLazy, Process_setVersionsLazy), 0); // this should be transpiled out, but just incase - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "browser"_s), - JSC::JSValue(false)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "browser"_s), + JSC::JSValue(false), PropertyAttribute::DontEnum | 0); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "exitCode"_s), - JSC::JSValue(JSC::jsNumber(0))); + this->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "exitCode"_s), + JSC::CustomGetterSetter::create(vm, + Process__getExitCode, + Process__setExitCode), + 0); - this->putDirect(this->vm(), clientData->builtinNames().versionPublicName(), - JSC::jsString(this->vm(), makeString("v", REPORTED_NODE_VERSION))); + this->putDirect(vm, clientData->builtinNames().versionPublicName(), + JSC::jsString(vm, makeString("v", REPORTED_NODE_VERSION))); // this gives some way of identifying at runtime whether the SSR is happening in node or not. // this should probably be renamed to what the name of the bundler is, instead of "notNodeJS" // but it must be something that won't evaluate to truthy in Node.js - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "isBun"_s), JSC::JSValue(true)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "isBun"_s), JSC::JSValue(true)); #if defined(__APPLE__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("darwin"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("darwin"))); #else - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("linux"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("linux"))); #endif #if defined(__x86_64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x64"))); #elif defined(__i386__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x86"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x86"))); #elif defined(__arm__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm"))); #elif defined(__aarch64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm64"))); #endif JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, @@ -719,7 +835,7 @@ void Process::finishCreation(JSC::VM& vm) MAKE_STATIC_STRING_IMPL("bigint"), Process_functionHRTimeBigInt, ImplementationVisibility::Public); hrtime->putDirect(vm, JSC::Identifier::fromString(vm, "bigint"_s), hrtimeBigInt); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "hrtime"_s), hrtime); + this->putDirect(vm, JSC::Identifier::fromString(vm, "hrtime"_s), hrtime); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "release"_s)), JSC::CustomGetterSetter::create(vm, Process_getterRelease, Process_setterRelease), 0); @@ -733,7 +849,10 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "stdin"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyStdinGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "abort"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), + 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); + + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "argv0"_s)), @@ -745,13 +864,13 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "execArgv"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyExecArgvGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "uptime"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "uptime"_s), 0, Process_functionUptime, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "umask"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "umask"_s), 1, Process_functionUmask, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "binding"_s), + this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(vm, "binding"_s), processObjectInternalsBindingCodeGenerator(vm), 0); @@ -788,7 +907,7 @@ void Process::finishCreation(JSC::VM& vm) config->putDirect(vm, JSC::Identifier::fromString(vm, "variables"_s), variables, 0); this->putDirect(vm, JSC::Identifier::fromString(vm, "config"_s), config, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "emitWarning"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "emitWarning"_s), 1, Process_emitWarning, ImplementationVisibility::Public, NoIntrinsic, 0); JSC::JSFunction* requireDotMainFunction = JSFunction::create( diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index da6ba92a0..f44212da1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -270,6 +270,8 @@ public: JSWeakMap* vmModuleContextMap() { return m_vmModuleContextMap.getInitializedOnMainThread(this); } + bool hasProcessObject() const { return m_processObject.isInitialized(); } + JSC::JSObject* processObject() { return m_processObject.getInitializedOnMainThread(this); diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index a6855fd19..61ac91ba7 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -107,6 +107,50 @@ static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementDeserialize); return JSValue::encode(jsUndefined()); \ } +class VersionSqlite3 { +public: + explicit VersionSqlite3(sqlite3* db) + : db(db) + , version(0) + { + } + sqlite3* db; + std::atomic<uint64_t> version; +}; + +class SQLiteSingleton { +public: + Vector<VersionSqlite3*> databases; + Vector<std::atomic<uint64_t>> schema_versions; +}; + +static SQLiteSingleton* _instance = nullptr; + +static Vector<VersionSqlite3*>& databases() +{ + if (!_instance) { + _instance = new SQLiteSingleton(); + _instance->databases = Vector<VersionSqlite3*>(); + _instance->databases.reserveInitialCapacity(4); + _instance->schema_versions = Vector<std::atomic<uint64_t>>(); + } + + return _instance->databases; +} + +extern "C" void Bun__closeAllSQLiteDatabasesForTermination() +{ + if (!_instance) { + return; + } + auto& dbs = _instance->databases; + + for (auto& db : dbs) { + if (db->db) + sqlite3_close_v2(db->db); + } +} + namespace WebCore { using namespace JSC; @@ -272,10 +316,6 @@ void JSSQLStatement::destroy(JSC::JSCell* cell) void JSSQLStatementConstructor::destroy(JSC::JSCell* cell) { - JSSQLStatementConstructor* thisObject = static_cast<JSSQLStatementConstructor*>(cell); - for (auto version_db : thisObject->databases) { - delete version_db; - } } static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3_stmt* stmt, int i, JSC::JSValue value, JSC::ThrowScope& scope, bool clone) @@ -547,8 +587,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexic return JSValue::encode(JSC::jsUndefined()); } - auto count = thisObject->databases.size(); - thisObject->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -565,12 +605,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexical } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -606,7 +646,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } @@ -620,7 +660,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje auto extensionString = extension.toWTFString(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -661,11 +701,11 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l } int32_t handle = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (thisObject->databases.size() < handle) { + if (databases().size() < handle) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -724,7 +764,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l rc = sqlite3_step(statement); if (!sqlite3_stmt_readonly(statement)) { - thisObject->databases[handle]->version++; + databases()[handle]->version++; } while (rc == SQLITE_ROW) { @@ -765,12 +805,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction, (JSC::JSGlobalOb int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -803,12 +843,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO } int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (!db) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Cannot use a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -848,7 +888,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO auto* structure = JSSQLStatement::createStructure(vm, lexicalGlobalObject, lexicalGlobalObject->objectPrototype()); // auto* structure = JSSQLStatement::createStructure(vm, globalObject(), thisObject->getDirect(vm, vm.propertyNames->prototype)); JSSQLStatement* sqlStatement = JSSQLStatement::create( - structure, reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject), statement, thisObject->databases[handle]); + structure, reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject), statement, databases()[handle]); if (bindings.isObject()) { auto* castedThis = sqlStatement; DO_REBIND(bindings) @@ -924,8 +964,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction, (JSC::JSGlobalObje status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); assert(status == SQLITE_OK); - auto count = constructor->databases.size(); - constructor->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -956,12 +996,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj int dbIndex = dbNumber.toInt32(lexicalGlobalObject); - if (dbIndex < 0 || dbIndex >= constructor->databases.size()) { + if (dbIndex < 0 || dbIndex >= databases().size()) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(jsUndefined()); } - sqlite3* db = constructor->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; // no-op if already closed if (!db) { return JSValue::encode(jsUndefined()); @@ -973,7 +1013,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj return JSValue::encode(jsUndefined()); } - constructor->databases[dbIndex]->db = nullptr; + databases()[dbIndex]->db = nullptr; return JSValue::encode(jsUndefined()); } diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.h b/src/bun.js/bindings/sqlite/JSSQLStatement.h index e63b99fbb..8566fcdd9 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.h +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.h @@ -47,17 +47,6 @@ namespace WebCore { -class VersionSqlite3 { -public: - explicit VersionSqlite3(sqlite3* db) - : db(db) - , version(0) - { - } - sqlite3* db; - std::atomic<uint64_t> version; -}; - class JSSQLStatementConstructor final : public JSC::JSFunction { public: using Base = JSC::JSFunction; @@ -82,13 +71,9 @@ public: return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - Vector<VersionSqlite3*> databases; - Vector<std::atomic<uint64_t>> schema_versions; - private: JSSQLStatementConstructor(JSC::VM& vm, NativeExecutable* native, JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, native, globalObject, structure) - , databases() { } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index b696c6cf2..7d2435823 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -334,6 +334,33 @@ pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSG jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; } +pub const ExitHandler = struct { + exit_code: u8 = 0, + + pub export fn Bun__getExitCode(vm: *VirtualMachine) u8 { + return vm.exit_handler.exit_code; + } + + pub export fn Bun__setExitCode(vm: *VirtualMachine, code: u8) void { + vm.exit_handler.exit_code = code; + } + + extern fn Process__dispatchOnBeforeExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Process__dispatchOnExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Bun__closeAllSQLiteDatabasesForTermination() void; + + pub fn dispatchOnExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnExit(vm.global, this.exit_code); + Bun__closeAllSQLiteDatabasesForTermination(); + } + + pub fn dispatchOnBeforeExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnBeforeExit(vm.global, this.exit_code); + } +}; + /// TODO: rename this to ScriptExecutionContext /// This is the shared global state for a single JS instance execution /// Today, Bun is one VM per thread, so the name "VirtualMachine" sort of makes sense @@ -376,6 +403,7 @@ pub const VirtualMachine = struct { plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, last_reported_error_for_dedupe: JSValue = .zero, + exit_handler: ExitHandler = .{}, /// Do not access this field directly /// It exists in the VirtualMachine struct so that @@ -620,7 +648,29 @@ pub const VirtualMachine = struct { loop.run(); } + pub fn onBeforeExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnBeforeExit(); + var dispatch = false; + while (true) { + while (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) : (dispatch = true) { + this.tick(); + this.eventLoop().autoTickActive(); + } + + if (dispatch) { + this.exit_handler.dispatchOnBeforeExit(); + dispatch = false; + + if (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) continue; + } + + break; + } + } + pub fn onExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnExit(); + var rare_data = this.rare_data orelse return; var hook = rare_data.cleanup_hook orelse return; hook.execute(); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 96d04636e..553b292d6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -2202,7 +2202,9 @@ pub const Process = struct { } } - pub fn exit(_: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + pub fn exit(globalObject: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + globalObject.bunVM().onExit(); + std.os.exit(@truncate(u8, @intCast(u32, @max(code, 0)))); } diff --git a/src/bun_js.zig b/src/bun_js.zig index 63ffe0611..72b7f8de9 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -248,6 +248,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -279,6 +281,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -315,6 +319,8 @@ pub const Run = struct { vm.eventLoop().autoTickActive(); } + vm.onBeforeExit(); + if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { prev_promise = this.vm.pending_internal_promise; vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); @@ -332,6 +338,8 @@ pub const Run = struct { vm.tick(); vm.eventLoop().autoTickActive(); } + + vm.onBeforeExit(); } if (vm.log.msgs.items.len > 0) { @@ -347,10 +355,14 @@ pub const Run = struct { vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; vm.global.handleRejectedPromises(); + if (this.any_unhandled and this.vm.exit_handler.exit_code == 0) { + this.vm.exit_handler.exit_code = 1; + } + const exit_code = this.vm.exit_handler.exit_code; vm.onExit(); if (!JSC.is_bindgen) JSC.napi.fixDeadCodeElimination(); - Global.exit(@intFromBool(this.any_unhandled)); + Global.exit(exit_code); } }; diff --git a/test/js/node/process/process-exit-fixture.js b/test/js/node/process/process-exit-fixture.js new file mode 100644 index 000000000..c5a492285 --- /dev/null +++ b/test/js/node/process/process-exit-fixture.js @@ -0,0 +1,16 @@ +process.on("beforeExit", () => { + throw new Error("process.on('beforeExit') called"); +}); + +if (process._exiting) { + throw new Error("process._exiting should be undefined"); +} + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("PASS"); +}); + +process.exit(0); diff --git a/test/js/node/process/process-exitCode-fixture.js b/test/js/node/process/process-exitCode-fixture.js new file mode 100644 index 000000000..2d5182d93 --- /dev/null +++ b/test/js/node/process/process-exitCode-fixture.js @@ -0,0 +1,7 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); diff --git a/test/js/node/process/process-exitCode-with-exit.js b/test/js/node/process/process-exitCode-with-exit.js new file mode 100644 index 000000000..610975bc2 --- /dev/null +++ b/test/js/node/process/process-exitCode-with-exit.js @@ -0,0 +1,8 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); +process.exit(); diff --git a/test/js/node/process/process-onBeforeExit-fixture.js b/test/js/node/process/process-onBeforeExit-fixture.js new file mode 100644 index 000000000..8cbdcebf0 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-fixture.js @@ -0,0 +1,7 @@ +process.on("beforeExit", () => { + console.log("beforeExit"); +}); + +process.on("exit", () => { + console.log("exit"); +}); diff --git a/test/js/node/process/process-onBeforeExit-keepAlive.js b/test/js/node/process/process-onBeforeExit-keepAlive.js new file mode 100644 index 000000000..45b20b763 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-keepAlive.js @@ -0,0 +1,18 @@ +let counter = 0; +process.on("beforeExit", () => { + if (process._exiting) { + throw new Error("process._exiting should be undefined"); + } + + console.log("beforeExit:", counter); + if (!counter++) { + setTimeout(() => {}, 1); + } +}); + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("exit:", counter); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 61ac3839c..c4701f664 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1,8 +1,8 @@ -import { resolveSync, which } from "bun"; +import { resolveSync, spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, realpathSync } from "fs"; -import { bunExe } from "harness"; -import { basename, resolve } from "path"; +import { bunEnv, bunExe } from "harness"; +import { basename, join, resolve } from "path"; it("process", () => { // this property isn't implemented yet but it should at least return a string @@ -233,3 +233,61 @@ it("process.argv in testing", () => { // assert we aren't creating a new process.argv each call expect(process.argv).toBe(process.argv); }); + +describe("process.exitCode", () => { + it("validates int", () => { + expect(() => (process.exitCode = "potato")).toThrow("exitCode must be a number"); + expect(() => (process.exitCode = 1.2)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = NaN)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -1)).toThrow("exitCode must be between 0 and 127"); + }); + + it("works with implicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-with-exit.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-fixture.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); +}); + +it("process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("PASS"); +}); + +describe("process.onBeforeExit", () => { + it("emitted", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit\nexit"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-keepAlive.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit: 0\nbeforeExit: 1\nexit: 2"); + }); +}); |