stremio-shell-ng/src/stremio_app/stremio_server/server.rs
Claude 51d75cfe6e
fix: search accumulated log buffer for server readiness line
The stdout reader matched 'EngineFS server started at' against
string_data (the bytes from the most recent read only). If server.js
flushed in such a way that the readiness line straddled two reads,
neither chunk's .lines() yielded a full match, the endpoint channel
never received, recv() timed out, and the WebUI loaded against the
fallback URL.

Search the accumulated *lines buffer instead so a line split across
reads matches once the second chunk lands. Track endpoint_sent so we
do not resend on every subsequent chunk after a match.

Also preserve trailing newlines when trimming the retained buffer to
SRV_LOG_SIZE lines so a later chunk cannot be concatenated onto the
last unterminated line and corrupt a parser. Same trim treatment for
the stderr reader for consistency.

Closes #53
2026-05-10 14:43:48 +00:00

252 lines
9.9 KiB
Rust

use crate::stremio_app::constants::{SRV_BUFFER_SIZE, SRV_LOG_SIZE, STREMIO_SERVER_DEV_MODE};
use native_windows_gui::{self as nwg, PartialUi};
use std::io::Write;
use std::{
env, fs, io,
io::Read,
ops::Deref,
os::windows::process::CommandExt,
path,
process::{Command, Stdio},
sync::{Arc, Mutex, Once},
thread,
};
use winapi::um::{
processthreadsapi::GetCurrentProcess,
winbase::{CreateJobObjectA, CREATE_NO_WINDOW},
winnt::{
JobObjectExtendedLimitInformation, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
// 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)]
pub struct StremioServer {
development: bool,
parent: nwg::ControlHandle,
crash_notice: nwg::Notice,
logs: Arc<Mutex<String>>,
}
impl StremioServer {
pub fn start(&self) {
if self.development {
return;
}
let (tx, rx) = flume::unbounded();
let logs = self.logs.clone();
let sender = self.crash_notice.sender();
ensure_parent_job_object();
thread::spawn(move || {
let mut path = env::current_exe()
.and_then(fs::canonicalize)
.expect("Cannot get the current executable path");
path.pop();
let lines = Arc::new(Mutex::new(String::new()));
let runtime_path = path.clone().join(path::Path::new("stremio-runtime"));
let server_path = path.clone().join(path::Path::new("server.js"));
let child = Command::new(runtime_path)
.arg(server_path)
.creation_flags(CREATE_NO_WINDOW)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
match child {
Ok(mut child) => {
let mut stdout = child.stdout.take().unwrap();
let out_lines = lines.clone();
let tx = tx.clone();
let out_thread = thread::spawn(move || {
let mut endpoint_sent = false;
loop {
let mut buffer = [0; SRV_BUFFER_SIZE];
let on = match stdout.read(&mut buffer[..]) {
Ok(0) => break,
Ok(n) => n,
Err(err) => {
eprintln!("server stdout read error: {err}");
break;
}
};
std::io::stdout().write_all(&buffer).ok();
let string_data = String::from_utf8_lossy(&buffer[..on]);
{
let lines = &mut *out_lines.lock().unwrap();
*lines += string_data.deref();
if !endpoint_sent {
if let Some(line) = lines
.lines()
.find(|line| line.starts_with("EngineFS server started at"))
{
if let Some(endpoint) = line.split_whitespace().last() {
println!("HTTP endpoint: {endpoint}");
tx.send(endpoint.to_string()).ok();
endpoint_sent = true;
}
}
}
// Preserve trailing newline so the next chunk can't glue onto an unterminated line.
let had_trailing_newline = lines.ends_with('\n');
let mut trimmed = lines
.lines()
.rev()
.take(SRV_LOG_SIZE)
.collect::<Vec<&str>>()
.into_iter()
.rev()
.collect::<Vec<&str>>()
.join("\n");
if had_trailing_newline {
trimmed.push('\n');
}
*lines = trimmed;
};
}
});
let mut stderr = child.stderr.take().unwrap();
let err_lines = lines.clone();
let err_thread = thread::spawn(move || {
let mut buffer = [0; SRV_BUFFER_SIZE];
loop {
let en = match stderr.read(&mut buffer[..]) {
Ok(0) => break,
Ok(n) => n,
Err(err) => {
eprintln!("server stderr read error: {err}");
break;
}
};
std::io::stderr().write_all(&buffer).ok();
let string_data = String::from_utf8_lossy(&buffer[..en]);
// eprint!("{:?}", &buffer);
{
let lines = &mut *err_lines.lock().unwrap();
*lines += string_data.deref();
let had_trailing_newline = lines.ends_with('\n');
let mut trimmed = lines
.lines()
.rev()
.take(SRV_LOG_SIZE)
.collect::<Vec<&str>>()
.into_iter()
.rev()
.collect::<Vec<&str>>()
.join("\n");
if had_trailing_newline {
trimmed.push('\n');
}
*lines = trimmed;
};
}
});
out_thread.join().ok();
err_thread.join().ok();
// Drop on Windows neither kills nor waits, so reap explicitly.
child.kill().ok();
child.wait().ok();
}
Err(err) => {
nwg::error_message(
"Stremio server",
format!("Cannot execute stremio-runtime: {}", &err).as_str(),
);
}
};
{
let mut logs = logs.lock().unwrap();
*logs = lines.lock().unwrap().deref().to_string();
}
println!("Server terminated.");
sender.notice();
});
// Wait for the server to start
rx.recv().unwrap();
}
}
impl PartialUi for StremioServer {
fn build_partial<W: Into<nwg::ControlHandle>>(
data: &mut Self,
parent: Option<W>,
) -> Result<(), nwg::NwgError> {
if std::env::var(STREMIO_SERVER_DEV_MODE).unwrap_or("false".to_string()) == "true" {
data.development = true;
}
data.parent = parent.expect("No parent window").into();
nwg::Notice::builder()
.parent(data.parent)
.build(&mut data.crash_notice)
.ok();
data.start();
println!("Stremio server started");
Ok(())
}
fn process_event<'a>(
&self,
evt: nwg::Event,
_evt_data: &nwg::EventData,
handle: nwg::ControlHandle,
) {
use nwg::Event as E;
if evt == E::OnNotice && handle == self.crash_notice.handle {
nwg::modal_error_message(
self.parent,
"Stremio server crash log",
self.logs.lock().unwrap().deref(),
);
self.start();
}
}
}