fix: guard JobObject setup with Once and check return values

CreateJobObjectA / SetInformationJobObject / AssignProcessToJobObject
were called inside the per-start thread and their return values were
ignored. Two consequences:

1. Each server crash-restart created a fresh kernel JobObject HANDLE
   that was never CloseHandle'd. The HANDLE went out of scope when the
   spawned thread exited, leaking a kernel object every crash.
2. On Win 7/8 (single-job systems) and inside parent jobs that disallow
   breakaway, AssignProcessToJobObject silently failed, so
   stremio-runtime could survive the shell's death and hold port 11470.

Hoist the setup into ensure_parent_job_object() guarded by sync::Once
so it runs exactly once per shell process, and check each return value
explicitly with a clear log message when the OS-level safety net is
degraded. The HANDLE is intentionally not closed: closing it while
KILL_ON_JOB_CLOSE is set would terminate the shell itself.

Closes #47
Closes #48
This commit is contained in:
Claude 2026-05-10 12:34:09 +00:00
parent a9d9673f2a
commit 2294f52407
No known key found for this signature in database

View file

@ -2,13 +2,13 @@ use crate::stremio_app::constants::{SRV_BUFFER_SIZE, SRV_LOG_SIZE, STREMIO_SERVE
use native_windows_gui::{self as nwg, PartialUi}; use native_windows_gui::{self as nwg, PartialUi};
use std::io::Write; use std::io::Write;
use std::{ use std::{
env, fs, env, fs, io,
io::Read, io::Read,
ops::Deref, ops::Deref,
os::windows::process::CommandExt, os::windows::process::CommandExt,
path, path,
process::{Command, Stdio}, process::{Command, Stdio},
sync::{Arc, Mutex}, sync::{Arc, Mutex, Once},
thread, thread,
}; };
use winapi::um::{ use winapi::um::{
@ -21,6 +21,50 @@ use winapi::um::{
}, },
}; };
// Guarded by Once: avoids HANDLE leak per crash and re-assignment failure on Win 7/8.
fn ensure_parent_job_object() {
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
let job = CreateJobObjectA(std::ptr::null_mut(), std::ptr::null_mut());
if job.is_null() {
eprintln!(
"CreateJobObjectA failed: {}; child stremio-runtime may outlive the shell on crash",
io::Error::last_os_error()
);
return;
}
let jeli = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
| JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
| JOB_OBJECT_LIMIT_BREAKAWAY_OK,
..std::mem::zeroed()
},
..std::mem::zeroed()
};
if winapi::um::jobapi2::SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&jeli as *const _ as *mut _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
) == 0
{
eprintln!(
"SetInformationJobObject failed: {}",
io::Error::last_os_error()
);
return;
}
if winapi::um::jobapi2::AssignProcessToJobObject(job, GetCurrentProcess()) == 0 {
eprintln!(
"AssignProcessToJobObject failed: {}; child stremio-runtime may outlive the shell",
io::Error::last_os_error()
);
}
// Don't CloseHandle: KILL_ON_JOB_CLOSE would terminate the shell itself.
});
}
#[derive(Default)] #[derive(Default)]
pub struct StremioServer { pub struct StremioServer {
development: bool, development: bool,
@ -38,31 +82,9 @@ impl StremioServer {
let logs = self.logs.clone(); let logs = self.logs.clone();
let sender = self.crash_notice.sender(); let sender = self.crash_notice.sender();
ensure_parent_job_object();
thread::spawn(move || { thread::spawn(move || {
// Use Win32JobObject to kill the child process when the parent process is killed
// With the JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK and JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flags
unsafe {
let job_main_process = CreateJobObjectA(std::ptr::null_mut(), std::ptr::null_mut());
let jeli = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
| JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
| JOB_OBJECT_LIMIT_BREAKAWAY_OK,
..std::mem::zeroed()
},
..std::mem::zeroed()
};
winapi::um::jobapi2::SetInformationJobObject(
job_main_process,
JobObjectExtendedLimitInformation,
&jeli as *const _ as *mut _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
);
winapi::um::jobapi2::AssignProcessToJobObject(
job_main_process,
GetCurrentProcess(),
);
}
let mut path = env::current_exe() let mut path = env::current_exe()
.and_then(fs::canonicalize) .and_then(fs::canonicalize)
.expect("Cannot get the current executable path"); .expect("Cannot get the current executable path");