aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-07-08 14:26:19 -0700
committerGravatar GitHub <noreply@github.com> 2023-07-08 14:26:19 -0700
commitaa8b832ef61ada31176d248e716074ff22bb9dee (patch)
treeef125622db8bb593a6b9860c7c00437a98d93733
parentfa632c33312ad3b4dca9e4bddaa2af1c7db07597 (diff)
downloadbun-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.cpp189
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.h2
-rw-r--r--src/bun.js/bindings/sqlite/JSSQLStatement.cpp86
-rw-r--r--src/bun.js/bindings/sqlite/JSSQLStatement.h15
-rw-r--r--src/bun.js/javascript.zig50
-rw-r--r--src/bun.js/node/types.zig4
-rw-r--r--src/bun_js.zig14
-rw-r--r--test/js/node/process/process-exit-fixture.js16
-rw-r--r--test/js/node/process/process-exitCode-fixture.js7
-rw-r--r--test/js/node/process/process-exitCode-with-exit.js8
-rw-r--r--test/js/node/process/process-onBeforeExit-fixture.js7
-rw-r--r--test/js/node/process/process-onBeforeExit-keepAlive.js18
-rw-r--r--test/js/node/process/process.test.js64
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");
+ });
+});