fix: robustesse v1.1.6 — timeout runner/doctor, anti-deadlock, securite migrate, win32 is_running, websocket RFC6455, keyring JSON
- runner.rs: timeout 30s sur les commandes capturees (doctor ne pend plus) + pipes stdout/stderr draines dans des threads (anti-deadlock) ; run_untimed() pour les installations longues (npm/cargo/go) — sh() des installateurs l'utilise, les sondes gardent le timeout
- migrate_cmd.rs: rejet des chemins unsafe (path traversal) dans import_bundle
- process.rs: is_running Windows via OpenProcess/GetExitCodeProcess (fiable vs tasklist)
- probe.rs: drain des pipes en threads dans detect_external_version
- web.rs: boucle de traduction i18n (curseur, plus de boucle infinie si la traduction contient {{)
- serve.rs: frames WebSocket RFC 6455 avec vraies longueurs > 125 octets (126/127)
- secrets.rs: index keyring serialise en JSON (cles contenant des virgules)
- sandbox.rs: tests sans fuite de TempDir (plus de mem::forget)
- version 1.1.6, man pages et ROADMAP maj
This commit is contained in:
+118
-7
@@ -52,6 +52,20 @@ pub trait Runner {
|
||||
capture: bool,
|
||||
) -> Result<RunOutput>;
|
||||
|
||||
/// Run without the probe timeout: used for legitimately long
|
||||
/// operations (package installs, git clones, ...). The default
|
||||
/// implementation delegates to run().
|
||||
fn run_untimed(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
) -> Result<RunOutput> {
|
||||
self.run(cmd, args, env, cwd, capture)
|
||||
}
|
||||
|
||||
/// Locate a binary on the PATH (like the which command).
|
||||
fn which(&self, bin: &str) -> Option<PathBuf>;
|
||||
}
|
||||
@@ -73,14 +87,17 @@ impl<'a> SystemRunner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Runner for SystemRunner<'a> {
|
||||
fn run(
|
||||
impl<'a> SystemRunner<'a> {
|
||||
/// Shared implementation. The timeout applies only to captured
|
||||
/// probes; long-running installs pass None via run_untimed().
|
||||
fn run_inner(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> Result<RunOutput> {
|
||||
let display = if args.is_empty() {
|
||||
cmd.to_string()
|
||||
@@ -102,12 +119,70 @@ impl<'a> Runner for SystemRunner<'a> {
|
||||
c.current_dir(dir);
|
||||
}
|
||||
if capture {
|
||||
let out = c
|
||||
.output()
|
||||
c.stdout(std::process::Stdio::piped());
|
||||
c.stderr(std::process::Stdio::piped());
|
||||
let mut child = c
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to execute: {display}"))?;
|
||||
let code = out.status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
|
||||
let stdout_pipe = child.stdout.take();
|
||||
let stderr_pipe = child.stderr.take();
|
||||
|
||||
let stdout_handle = std::thread::spawn(move || {
|
||||
let mut out = Vec::new();
|
||||
if let Some(mut pipe) = stdout_pipe {
|
||||
let _ = std::io::Read::read_to_end(&mut pipe, &mut out);
|
||||
}
|
||||
out
|
||||
});
|
||||
let stderr_handle = std::thread::spawn(move || {
|
||||
let mut err = Vec::new();
|
||||
if let Some(mut pipe) = stderr_pipe {
|
||||
let _ = std::io::Read::read_to_end(&mut pipe, &mut err);
|
||||
}
|
||||
err
|
||||
});
|
||||
|
||||
let status = if let Some(timeout) = timeout {
|
||||
use wait_timeout::ChildExt;
|
||||
match child.wait_timeout(timeout) {
|
||||
Ok(Some(s)) => s,
|
||||
Ok(None) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
self.log
|
||||
.warn(format!("command timed out after {timeout:?}: {display}"));
|
||||
let stdout = String::from_utf8_lossy(&stdout_handle.join().unwrap_or_default())
|
||||
.to_string();
|
||||
let stderr = String::from_utf8_lossy(&stderr_handle.join().unwrap_or_default())
|
||||
.to_string();
|
||||
return Ok(RunOutput {
|
||||
code: -1,
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Ok(RunOutput {
|
||||
code: -1,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
child
|
||||
.wait()
|
||||
.with_context(|| format!("failed to wait: {display}"))?
|
||||
};
|
||||
|
||||
let stdout_bytes = stdout_handle.join().unwrap_or_default();
|
||||
let stderr_bytes = stderr_handle.join().unwrap_or_default();
|
||||
let code = status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&stdout_bytes).to_string();
|
||||
let stderr = String::from_utf8_lossy(&stderr_bytes).to_string();
|
||||
self.log.result(code, &stderr);
|
||||
Ok(RunOutput {
|
||||
code,
|
||||
@@ -126,6 +201,42 @@ impl<'a> Runner for SystemRunner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> Runner for SystemRunner<'a> {
|
||||
/// Probes are bounded by a 30 s timeout so a misbehaving process can
|
||||
/// never hang doctor/version detection.
|
||||
fn run(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
) -> Result<RunOutput> {
|
||||
self.run_inner(
|
||||
cmd,
|
||||
args,
|
||||
env,
|
||||
cwd,
|
||||
capture,
|
||||
Some(std::time::Duration::from_secs(30)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Installs and other legitimately long operations run without the
|
||||
/// probe timeout (npm/cargo/go installs can take minutes).
|
||||
fn run_untimed(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
) -> Result<RunOutput> {
|
||||
self.run_inner(cmd, args, env, cwd, capture, None)
|
||||
}
|
||||
|
||||
fn which(&self, bin: &str) -> Option<PathBuf> {
|
||||
which::which(bin).ok()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user