Skip to content

Commit 03ed3d3

Browse files
committed
Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev
2 parents a111de1 + 1ce58b9 commit 03ed3d3

4 files changed

Lines changed: 487 additions & 160 deletions

File tree

packages/tauri-app/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ url = "2"
2828
tauri-plugin-notification = "2"
2929

3030
[target.'cfg(windows)'.dependencies]
31-
windows-sys = { version = "0.59", features = ["Win32_UI_Shell"] }
31+
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] }

packages/tauri-app/src-tauri/src/cli_manager.rs

Lines changed: 145 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ use serde::{Deserialize, Serialize};
55
use serde_json::json;
66
use std::collections::VecDeque;
77
use std::env;
8+
#[cfg(windows)]
9+
use std::ffi::c_void;
810
use std::ffi::OsStr;
911
use std::fs;
1012
use std::io::{BufRead, BufReader, Read, Write};
13+
#[cfg(windows)]
14+
use std::mem::{size_of, zeroed};
1115
use std::net::TcpStream;
1216
#[cfg(unix)]
1317
use std::os::unix::process::CommandExt;
@@ -19,12 +23,95 @@ use std::thread;
1923
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
2024
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
2125

26+
#[cfg(windows)]
27+
use std::os::windows::io::AsRawHandle;
2228
#[cfg(windows)]
2329
use std::os::windows::process::CommandExt;
30+
#[cfg(windows)]
31+
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
32+
#[cfg(windows)]
33+
use windows_sys::Win32::System::JobObjects::{
34+
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
35+
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
36+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
37+
};
2438

2539
#[cfg(windows)]
2640
const CREATE_NO_WINDOW: u32 = 0x08000000;
2741

42+
#[cfg(windows)]
43+
#[derive(Debug)]
44+
struct WindowsJobObject {
45+
// The desktop wrapper may observe only a short-lived Node wrapper PID while the real
46+
// server and workspace descendants continue running below it. KILL_ON_JOB_CLOSE gives
47+
// Tauri an OS-owned handle for the whole subtree instead of relying on a single PID.
48+
handle: HANDLE,
49+
}
50+
51+
#[cfg(windows)]
52+
impl WindowsJobObject {
53+
fn create() -> anyhow::Result<Self> {
54+
let handle = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
55+
if handle.is_null() {
56+
return Err(anyhow::anyhow!(
57+
"CreateJobObjectW failed: {}",
58+
std::io::Error::last_os_error()
59+
));
60+
}
61+
62+
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
63+
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
64+
65+
let ok = unsafe {
66+
SetInformationJobObject(
67+
handle,
68+
JobObjectExtendedLimitInformation,
69+
&mut info as *mut _ as *mut c_void,
70+
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
71+
)
72+
};
73+
if ok == 0 {
74+
let err = std::io::Error::last_os_error();
75+
unsafe {
76+
CloseHandle(handle);
77+
}
78+
return Err(anyhow::anyhow!("SetInformationJobObject failed: {}", err));
79+
}
80+
81+
Ok(Self { handle })
82+
}
83+
84+
fn assign_child(&self, child: &Child) -> anyhow::Result<()> {
85+
let process_handle = child.as_raw_handle() as HANDLE;
86+
let ok = unsafe { AssignProcessToJobObject(self.handle, process_handle) };
87+
if ok == 0 {
88+
return Err(anyhow::anyhow!(
89+
"AssignProcessToJobObject failed: {}",
90+
std::io::Error::last_os_error()
91+
));
92+
}
93+
94+
Ok(())
95+
}
96+
}
97+
98+
#[cfg(windows)]
99+
impl Drop for WindowsJobObject {
100+
fn drop(&mut self) {
101+
if !self.handle.is_null() {
102+
unsafe {
103+
CloseHandle(self.handle);
104+
}
105+
}
106+
}
107+
}
108+
109+
#[cfg(windows)]
110+
unsafe impl Send for WindowsJobObject {}
111+
112+
#[cfg(windows)]
113+
unsafe impl Sync for WindowsJobObject {}
114+
28115
fn log_line(message: &str) {
29116
println!("[tauri-cli] {message}");
30117
}
@@ -363,6 +450,8 @@ impl Default for CliStatus {
363450
pub struct CliProcessManager {
364451
status: Arc<Mutex<CliStatus>>,
365452
child: Arc<Mutex<Option<Child>>>,
453+
#[cfg(windows)]
454+
job: Arc<Mutex<Option<WindowsJobObject>>>,
366455
ready: Arc<AtomicBool>,
367456
bootstrap_token: Arc<Mutex<Option<String>>>,
368457
}
@@ -372,6 +461,8 @@ impl CliProcessManager {
372461
Self {
373462
status: Arc::new(Mutex::new(CliStatus::default())),
374463
child: Arc::new(Mutex::new(None)),
464+
#[cfg(windows)]
465+
job: Arc::new(Mutex::new(None)),
375466
ready: Arc::new(AtomicBool::new(false)),
376467
bootstrap_token: Arc::new(Mutex::new(None)),
377468
}
@@ -394,13 +485,17 @@ impl CliProcessManager {
394485

395486
let status_arc = self.status.clone();
396487
let child_arc = self.child.clone();
488+
#[cfg(windows)]
489+
let job_arc = self.job.clone();
397490
let ready_flag = self.ready.clone();
398491
let token_arc = self.bootstrap_token.clone();
399492
thread::spawn(move || {
400493
if let Err(err) = Self::spawn_cli(
401494
app.clone(),
402495
status_arc.clone(),
403496
child_arc,
497+
#[cfg(windows)]
498+
job_arc,
404499
ready_flag,
405500
token_arc,
406501
dev,
@@ -420,11 +515,12 @@ impl CliProcessManager {
420515
}
421516

422517
pub fn stop(&self) -> anyhow::Result<()> {
518+
#[cfg(windows)]
519+
let _job = self.job.lock().take();
520+
423521
let mut child_opt = self.child.lock();
424522
if let Some(mut child) = child_opt.take() {
425523
log_line(&format!("stopping CLI pid={}", child.id()));
426-
#[cfg(windows)]
427-
let mut forced_tree_shutdown = false;
428524
#[cfg(unix)]
429525
unsafe {
430526
let pid = child.id() as i32;
@@ -446,18 +542,16 @@ impl CliProcessManager {
446542
Ok(Some(_)) => break,
447543
Ok(None) => {
448544
#[cfg(windows)]
449-
if !forced_tree_shutdown
450-
&& start.elapsed() > Duration::from_millis(CLI_WINDOWS_FORCE_GRACE_MS)
451-
{
545+
if start.elapsed() > Duration::from_millis(CLI_WINDOWS_FORCE_GRACE_MS) {
452546
log_line(&format!(
453547
"regular Windows shutdown still running after {}ms; escalating pid={}",
454548
CLI_WINDOWS_FORCE_GRACE_MS,
455549
child.id()
456550
));
457-
forced_tree_shutdown = true;
458551
if !kill_process_tree_windows(child.id(), true) {
459552
let _ = child.kill();
460553
}
554+
break;
461555
}
462556

463557
if start.elapsed() > Duration::from_secs(CLI_STOP_GRACE_SECS) {
@@ -476,11 +570,7 @@ impl CliProcessManager {
476570
}
477571
#[cfg(windows)]
478572
{
479-
if !forced_tree_shutdown
480-
&& !kill_process_tree_windows(child.id(), true)
481-
{
482-
let _ = child.kill();
483-
} else if forced_tree_shutdown {
573+
if !kill_process_tree_windows(child.id(), true) {
484574
let _ = child.kill();
485575
}
486576
}
@@ -491,6 +581,9 @@ impl CliProcessManager {
491581
Err(_) => break,
492582
}
493583
}
584+
} else {
585+
#[cfg(windows)]
586+
log_line("tracked CLI process already exited; dropping Windows job object to reap descendants");
494587
}
495588

496589
let mut status = self.status.lock();
@@ -511,6 +604,7 @@ impl CliProcessManager {
511604
app: AppHandle,
512605
status: Arc<Mutex<CliStatus>>,
513606
child_holder: Arc<Mutex<Option<Child>>>,
607+
#[cfg(windows)] job_holder: Arc<Mutex<Option<WindowsJobObject>>>,
514608
ready: Arc<AtomicBool>,
515609
bootstrap_token: Arc<Mutex<Option<String>>>,
516610
dev: bool,
@@ -592,6 +686,22 @@ impl CliProcessManager {
592686

593687
let pid = child.id();
594688
log_line(&format!("spawned pid={pid}"));
689+
#[cfg(windows)]
690+
match WindowsJobObject::create().and_then(|job| {
691+
job.assign_child(&child)?;
692+
Ok(job)
693+
}) {
694+
Ok(job) => {
695+
log_line(&format!("attached pid={pid} to Windows job object"));
696+
*job_holder.lock() = Some(job);
697+
}
698+
Err(err) => {
699+
log_line(&format!(
700+
"failed to attach pid={pid} to Windows job object; falling back to taskkill-only cleanup: {err}"
701+
));
702+
}
703+
}
704+
595705
{
596706
let mut locked = status.lock();
597707
locked.pid = Some(pid);
@@ -665,6 +775,8 @@ impl CliProcessManager {
665775
let status_clone = status.clone();
666776
let ready_clone = ready.clone();
667777
let child_holder_clone = child_holder.clone();
778+
#[cfg(windows)]
779+
let job_holder_clone = job_holder.clone();
668780
thread::spawn(move || {
669781
let timeout = Duration::from_secs(60);
670782
thread::sleep(timeout);
@@ -719,6 +831,10 @@ impl CliProcessManager {
719831
// Drop the handle after the process exits so other callers
720832
// don't attempt to stop/kill a finished process.
721833
*guard = None;
834+
#[cfg(windows)]
835+
{
836+
let _ = job_holder_clone.lock().take();
837+
}
722838
Some(status)
723839
}
724840
None => None,
@@ -776,7 +892,8 @@ impl CliProcessManager {
776892
auth_cookie_name: &str,
777893
) {
778894
let mut buffer = String::new();
779-
let local_url_regex = Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)\s*$").ok();
895+
let local_url_regex =
896+
Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)\s*$").ok();
780897
let token_prefix = "CODENOMAD_BOOTSTRAP_TOKEN:";
781898

782899
loop {
@@ -818,7 +935,6 @@ impl CliProcessManager {
818935
);
819936
continue;
820937
}
821-
822938
}
823939
}
824940
Err(_) => break,
@@ -1022,15 +1138,23 @@ fn resolve_tsx(_app: &AppHandle) -> Option<String> {
10221138
let cwd = std::env::current_dir().ok();
10231139
let workspace = workspace_root();
10241140
let mut candidates = vec![
1025-
cwd.as_ref().map(|p| p.join("node_modules/tsx/dist/cli.mjs")),
1026-
cwd.as_ref().map(|p| p.join("node_modules/tsx/dist/cli.cjs")),
1141+
cwd.as_ref()
1142+
.map(|p| p.join("node_modules/tsx/dist/cli.mjs")),
1143+
cwd.as_ref()
1144+
.map(|p| p.join("node_modules/tsx/dist/cli.cjs")),
10271145
cwd.as_ref().map(|p| p.join("node_modules/tsx/dist/cli.js")),
1028-
cwd.as_ref().map(|p| p.join("../node_modules/tsx/dist/cli.mjs")),
1029-
cwd.as_ref().map(|p| p.join("../node_modules/tsx/dist/cli.cjs")),
1030-
cwd.as_ref().map(|p| p.join("../node_modules/tsx/dist/cli.js")),
1031-
cwd.as_ref().map(|p| p.join("../../node_modules/tsx/dist/cli.mjs")),
1032-
cwd.as_ref().map(|p| p.join("../../node_modules/tsx/dist/cli.cjs")),
1033-
cwd.as_ref().map(|p| p.join("../../node_modules/tsx/dist/cli.js")),
1146+
cwd.as_ref()
1147+
.map(|p| p.join("../node_modules/tsx/dist/cli.mjs")),
1148+
cwd.as_ref()
1149+
.map(|p| p.join("../node_modules/tsx/dist/cli.cjs")),
1150+
cwd.as_ref()
1151+
.map(|p| p.join("../node_modules/tsx/dist/cli.js")),
1152+
cwd.as_ref()
1153+
.map(|p| p.join("../../node_modules/tsx/dist/cli.mjs")),
1154+
cwd.as_ref()
1155+
.map(|p| p.join("../../node_modules/tsx/dist/cli.cjs")),
1156+
cwd.as_ref()
1157+
.map(|p| p.join("../../node_modules/tsx/dist/cli.js")),
10341158
workspace
10351159
.as_ref()
10361160
.map(|p| p.join("node_modules/tsx/dist/cli.mjs")),

0 commit comments

Comments
 (0)