Repository: denoland/deno
Pull request: fix(ext/node): propagate worker exit code from process.exit()

Enables `test-worker-esm-exit.js` test

Changed files:
filename: ext/node/polyfills/worker_threads.ts
patch: @@ -249,7 +249,7 @@ class NodeWorker extends EventEmitter {
           this.#status = "CLOSED";
           if (!this.#exited) {
             this.#exited = true;
-            this.emit("exit", 0);
+            this.emit("exit", data ?? 0);
           }
           return;
         }

filename: runtime/ops/web_worker.rs
patch: @@ -14,6 +14,7 @@ pub use sync_fetch::SyncFetchError;
 
 use self::sync_fetch::op_worker_sync_fetch;
 use crate::web_worker::WebWorkerInternalHandle;
+use crate::web_worker::WorkerControlEvent;
 use crate::web_worker::WorkerThreadType;
 
 deno_core::extension!(
@@ -56,8 +57,14 @@ async fn op_worker_recv_message(
 #[op2(fast)]
 fn op_worker_close(state: &mut OpState) {
   // Notify parent that we're finished
+  let exit_code = state
+    .try_borrow::<deno_os::ExitCode>()
+    .map(|e| e.get())
+    .unwrap_or(0);
   let mut handle = state.borrow_mut::<WebWorkerInternalHandle>().clone();
 
+  // Send the exit code to the parent before terminating
+  let _ = handle.post_event(WorkerControlEvent::Close(exit_code));
   handle.terminate();
 }
 

filename: runtime/ops/worker_host.rs
patch: @@ -327,7 +327,7 @@ async fn op_host_recv_ctrl(
       (handle.worker_handle.clone(), handle.cancel_handle.clone())
     } else {
       // If handle was not found it means worker has already shutdown
-      return WorkerControlEvent::Close;
+      return WorkerControlEvent::Close(0);
     }
   };
 
@@ -337,20 +337,22 @@ async fn op_host_recv_ctrl(
     .await;
   match maybe_event {
     Ok(Some(event)) => {
-      // Terminal error means that worker should be removed from worker table.
-      if let WorkerControlEvent::TerminalError(_) = &event {
-        close_channel(state, id, WorkerChannel::Ctrl);
+      // Terminal error or close means that worker should be removed from worker table.
+      match &event {
+        WorkerControlEvent::TerminalError(_) | WorkerControlEvent::Close(_) => {
+          close_channel(state, id, WorkerChannel::Ctrl);
+        }
       }
       event
     }
     Ok(None) => {
       // If there was no event from worker it means it has already been closed.
       close_channel(state, id, WorkerChannel::Ctrl);
-      WorkerControlEvent::Close
+      WorkerControlEvent::Close(0)
     }
     Err(_) => {
       // The worker was terminated.
-      WorkerControlEvent::Close
+      WorkerControlEvent::Close(0)
     }
   }
 }

filename: runtime/web_worker.rs
patch: @@ -134,7 +134,7 @@ impl<'s> WorkerThreadType {
 #[allow(clippy::large_enum_variant)]
 pub enum WorkerControlEvent {
   TerminalError(CoreError),
-  Close,
+  Close(i32),
 }
 
 use deno_core::serde::Serializer;
@@ -146,7 +146,7 @@ impl Serialize for WorkerControlEvent {
   {
     let type_id = match &self {
       WorkerControlEvent::TerminalError(_) => 1_i32,
-      WorkerControlEvent::Close => 3_i32,
+      WorkerControlEvent::Close(_) => 3_i32,
     };
 
     match self {
@@ -171,7 +171,9 @@ impl Serialize for WorkerControlEvent {
 
         Serialize::serialize(&(type_id, value), serializer)
       }
-      _ => Serialize::serialize(&(type_id, ()), serializer),
+      WorkerControlEvent::Close(exit_code) => {
+        Serialize::serialize(&(type_id, exit_code), serializer)
+      }
     }
   }
 }
@@ -573,7 +575,7 @@ impl WebWorker {
       }),
       deno_io::deno_io::init(Some(options.stdio)),
       deno_fs::deno_fs::init(services.fs.clone()),
-      deno_os::deno_os::init(None),
+      deno_os::deno_os::init(Some(deno_os::ExitCode::default())),
       deno_process::deno_process::init(services.npm_process_state_provider),
       deno_node::deno_node::init::<
         TInNpmPackageChecker,

filename: tests/node_compat/config.jsonc
patch: @@ -1545,6 +1545,7 @@
     // "parallel/test-worker-debug.js": {},
     "parallel/test-worker-dns-terminate-during-query.js": {},
     "parallel/test-worker-dispose.mjs": {},
+    "parallel/test-worker-esm-exit.js": {},
     "parallel/test-worker-esmodule.js": {},
     "parallel/test-worker-event.js": {},
     "parallel/test-worker-fs-stat-watcher.js": {},