Skip to content

Commit 74c0c6d

Browse files
authored
Merge pull request #45 from maphew/agent/6w5-c3-handler-contract
Deepen mdo C3: one handler invocation contract
2 parents 7625e53 + 5f2bdf1 commit 74c0c6d

5 files changed

Lines changed: 328 additions & 89 deletions

File tree

‎src/bin/mdo-open.rs‎

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
//! but break terminal usage (no stdout to the parent shell, output races the
1111
//! returning prompt under cmd/PowerShell).
1212
//!
13-
//! Instead, `mdo-open[.exe]` is a tiny desktop wrapper that:
14-
//! 1. Locates `mdo[.exe]` next to itself.
15-
//! 2. Spawns it with `--open` and the rest of the args, using
16-
//! `CREATE_NO_WINDOW` so the child never gets a console allocated.
13+
//! Instead, `mdo-open[.exe]` is a tiny desktop wrapper that consumes the
14+
//! shared handler invocation contract (`mdo_cli::handler`):
15+
//! 1. Locates `mdo[.exe]` next to itself (the one sibling-binary
16+
//! discovery function, so a missing sibling gets one clear message).
17+
//! 2. Spawns it with the contract argv (`--open` plus the file args),
18+
//! using `CREATE_NO_WINDOW` so the child never gets a console
19+
//! allocated.
1720
//! 3. If launched directly with no file args, opens onboarding: Windows
1821
//! starts the terminal setup in Windows Terminal when available; Linux
1922
//! opens `mdo-setup` for native first-run onboarding. This step is
@@ -48,8 +51,8 @@ use std::process::{Command, ExitCode};
4851

4952
#[cfg(target_os = "windows")]
5053
use std::io;
51-
#[cfg(target_os = "windows")]
52-
use std::path::Path;
54+
55+
use mdo_cli::handler;
5356

5457
#[cfg(target_os = "windows")]
5558
const MDO_BIN: &str = "mdo.exe";
@@ -60,12 +63,16 @@ const MDO_BIN: &str = "mdo";
6063
const SETUP_BIN: &str = "mdo-setup";
6164

6265
fn main() -> ExitCode {
63-
let mut exe_path = match env::current_exe() {
66+
// A GUI-launched binary often has no console attached, so this message
67+
// may still go nowhere on Windows — but silent failure is worse
68+
// everywhere, and on Linux it reaches the file manager's stderr log.
69+
let exe_path = match env::current_exe() {
6470
Ok(p) => p,
65-
Err(_) => return ExitCode::from(1),
71+
Err(e) => {
72+
eprintln!("mdo-open: cannot locate its own executable: {e}");
73+
return ExitCode::from(1);
74+
}
6675
};
67-
exe_path.pop(); // strip the file name, keep the directory
68-
6976
let args = env::args_os().skip(1).collect::<Vec<_>>();
7077

7178
#[cfg(target_os = "windows")]
@@ -77,7 +84,10 @@ fn main() -> ExitCode {
7784
if args.is_empty() {
7885
return match spawn_windows_onboarding(&exe_path) {
7986
Ok(()) => ExitCode::SUCCESS,
80-
Err(_) => ExitCode::from(1),
87+
Err(e) => {
88+
eprintln!("mdo-open: {e}");
89+
ExitCode::from(1)
90+
}
8191
};
8292
}
8393

@@ -90,33 +100,47 @@ fn main() -> ExitCode {
90100
// instead of always reporting success.
91101
match spawn_windows_open(&exe_path, args) {
92102
Ok(status) => exit_code_from_status(status),
93-
Err(_) => ExitCode::from(1),
103+
Err(e) => {
104+
eprintln!("mdo-open: {e}");
105+
ExitCode::from(1)
106+
}
94107
}
95108
}
96109

97110
#[cfg(not(target_os = "windows"))]
98111
{
99112
#[cfg(target_os = "linux")]
100-
let mut cmd = if args.is_empty() {
101-
let setup = exe_path.join(SETUP_BIN);
102-
if !setup.exists() {
103-
return ExitCode::from(1);
113+
if args.is_empty() {
114+
// No-file launch is onboarding, not open: hand off to the
115+
// sibling `mdo-setup`.
116+
let setup = match handler::sibling_binary(&exe_path, SETUP_BIN) {
117+
Ok(path) => path,
118+
Err(e) => {
119+
eprintln!("mdo-open: {e}");
120+
return ExitCode::from(1);
121+
}
122+
};
123+
let mut cmd = Command::new(setup);
124+
match cmd.status() {
125+
Ok(status) => return exit_code_from_status(status),
126+
Err(e) => {
127+
eprintln!("mdo-open: {e}");
128+
return ExitCode::from(1);
129+
}
104130
}
105-
Command::new(setup)
106-
} else {
107-
let mut cmd = Command::new(exe_path.join(MDO_BIN));
108-
cmd.arg("--open");
109-
cmd.args(args);
110-
cmd
111-
};
131+
}
112132

113-
#[cfg(not(target_os = "linux"))]
114-
let mut cmd = {
115-
let mut cmd = Command::new(exe_path.join(MDO_BIN));
116-
cmd.arg("--open");
117-
cmd.args(args);
118-
cmd
133+
// Everything else is the forward: sibling mdo with the contract
134+
// argv (`--open` plus the files as received).
135+
let mdo_bin = match handler::sibling_binary(&exe_path, MDO_BIN) {
136+
Ok(path) => path,
137+
Err(e) => {
138+
eprintln!("mdo-open: {e}");
139+
return ExitCode::from(1);
140+
}
119141
};
142+
let mut cmd = Command::new(mdo_bin);
143+
cmd.args(handler::open_argv(&args));
120144

121145
// Non-Windows binaries never had a console-flash problem to begin
122146
// with (that's Windows Explorer + console-subsystem-exe behavior),
@@ -126,7 +150,10 @@ fn main() -> ExitCode {
126150
// in an already-attached terminal) and propagate its exit status.
127151
match cmd.status() {
128152
Ok(status) => exit_code_from_status(status),
129-
Err(_) => ExitCode::from(1),
153+
Err(e) => {
154+
eprintln!("mdo-open: {e}");
155+
ExitCode::from(1)
156+
}
130157
}
131158
}
132159
}
@@ -137,39 +164,36 @@ fn main() -> ExitCode {
137164
/// a failure whose low byte is zero (e.g. 0x100) must not truncate to
138165
/// "success"; any failing status maps to a non-zero code.
139166
fn exit_code_from_status(status: std::process::ExitStatus) -> ExitCode {
140-
if status.success() {
167+
exit_code_from_parts(status.success(), status.code())
168+
}
169+
170+
/// The truncation rule of [`exit_code_from_status`], split from
171+
/// `ExitStatus` so it is unit-testable portably (Unix cannot even produce a
172+
/// failed status with a zero low byte; Windows can).
173+
fn exit_code_from_parts(success: bool, code: Option<i32>) -> ExitCode {
174+
if success {
141175
return ExitCode::SUCCESS;
142176
}
143-
match status.code() {
177+
match code {
144178
Some(code) if code as u8 != 0 => ExitCode::from(code as u8),
145179
_ => ExitCode::FAILURE,
146180
}
147181
}
148182

149183
#[cfg(target_os = "windows")]
150184
fn spawn_windows_open(
151-
exe_dir: &Path,
185+
anchor_exe: &std::path::Path,
152186
args: Vec<std::ffi::OsString>,
153187
) -> io::Result<std::process::ExitStatus> {
154-
let mut cmd = Command::new(exe_dir.join(MDO_BIN));
155-
cmd.arg("--open");
156-
cmd.args(args);
188+
let mdo_bin = handler::sibling_binary(anchor_exe, MDO_BIN)?;
189+
let mut cmd = Command::new(mdo_bin);
190+
cmd.args(handler::open_argv(&args));
157191
spawn_without_console(cmd)
158192
}
159193

160194
#[cfg(target_os = "windows")]
161-
fn spawn_windows_onboarding(exe_dir: &Path) -> io::Result<()> {
162-
let mdo = exe_dir.join(MDO_BIN);
163-
if !mdo.exists() {
164-
return Err(io::Error::new(
165-
io::ErrorKind::NotFound,
166-
format!(
167-
"expected {MDO_BIN} next to mdo-open.exe at {}",
168-
mdo.display()
169-
),
170-
));
171-
}
172-
195+
fn spawn_windows_onboarding(anchor_exe: &std::path::Path) -> io::Result<()> {
196+
let mdo = handler::sibling_binary(anchor_exe, MDO_BIN)?;
173197
mdo_cli::windows_setup::spawn_terminal_setup(&mdo)
174198
}
175199

@@ -189,3 +213,51 @@ fn spawn_without_console(mut cmd: Command) -> io::Result<std::process::ExitStatu
189213
let mut child = cmd.spawn()?;
190214
child.wait()
191215
}
216+
217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
221+
#[test]
222+
fn exit_code_success_is_success() {
223+
assert_eq!(exit_code_from_parts(true, Some(0)), ExitCode::SUCCESS);
224+
assert_eq!(exit_code_from_parts(true, None), ExitCode::SUCCESS);
225+
}
226+
227+
#[test]
228+
fn small_exit_codes_pass_through() {
229+
assert_eq!(exit_code_from_parts(false, Some(1)), ExitCode::from(1));
230+
assert_eq!(exit_code_from_parts(false, Some(7)), ExitCode::from(7));
231+
assert_eq!(exit_code_from_parts(false, Some(255)), ExitCode::from(255));
232+
}
233+
234+
#[test]
235+
fn failure_with_zero_low_byte_must_not_read_as_success() {
236+
// The truncation trap: 0x100 has a zero low byte, but the child
237+
// FAILED. Truncating to `ExitCode::from(0)` would report success.
238+
assert_eq!(exit_code_from_parts(false, Some(0x100)), ExitCode::FAILURE);
239+
assert_eq!(exit_code_from_parts(false, Some(0x200)), ExitCode::FAILURE);
240+
assert_eq!(exit_code_from_parts(false, Some(0xFF00)), ExitCode::FAILURE);
241+
}
242+
243+
#[test]
244+
fn unknown_termination_falls_back_to_failure() {
245+
// Signal deaths (and nested nested process terminations) offer no
246+
// exit code at all: a generic failure keeps mdo-open honest.
247+
assert_eq!(exit_code_from_parts(false, None), ExitCode::FAILURE);
248+
}
249+
250+
// Windows-only end-to-end of the truncation rule: only Windows can
251+
// actually produce a failed child whose exit code has a zero low byte.
252+
#[cfg(target_os = "windows")]
253+
#[test]
254+
fn real_windows_exit_code_256_means_failure() {
255+
let status = Command::new("cmd.exe")
256+
.args(["/C", "exit", "256"])
257+
.status()
258+
.expect("cmd.exe should be spawnable on Windows");
259+
assert!(!status.success());
260+
assert_eq!(status.code(), Some(256));
261+
assert_eq!(exit_code_from_status(status), ExitCode::FAILURE);
262+
}
263+
}

‎src/bin/mdo-setup.rs‎

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,7 @@ mod linux_setup {
9393

9494
pub fn run() -> io::Result<()> {
9595
continue_after_launcher_registration(register_launcher());
96-
let mdo = sibling_binary("mdo")?;
97-
if !mdo.exists() {
98-
return Err(io::Error::new(
99-
io::ErrorKind::NotFound,
100-
format!("expected mdo next to mdo-setup at {}", mdo.display()),
101-
));
102-
}
96+
let mdo = mdo_cli::handler::sibling_binary(&std::env::current_exe()?, "mdo")?;
10397

10498
// Already attached to a terminal (e.g. run from a shell): show guided setup
10599
// right here instead of spawning a second window. The `?` still
@@ -254,12 +248,6 @@ mod linux_setup {
254248
.unwrap_or(false)
255249
}
256250

257-
fn sibling_binary(name: &str) -> io::Result<PathBuf> {
258-
let mut path = std::env::current_exe()?;
259-
path.pop();
260-
Ok(path.join(name))
261-
}
262-
263251
#[cfg(test)]
264252
mod tests {
265253
use super::*;
@@ -338,16 +326,7 @@ mod windows_setup {
338326
};
339327

340328
pub fn run() -> io::Result<()> {
341-
let mdo = sibling_binary("mdo.exe")?;
342-
if !mdo.exists() {
343-
return Err(io::Error::new(
344-
io::ErrorKind::NotFound,
345-
format!(
346-
"expected mdo.exe next to mdo-setup.exe at {}",
347-
mdo.display()
348-
),
349-
));
350-
}
329+
let mdo = mdo_cli::handler::sibling_binary(&std::env::current_exe()?, "mdo.exe")?;
351330

352331
// Match the no-file `mdo-open.exe` onboarding path: prefer Windows
353332
// Terminal for a styled, centered setup, then fall back to a plain new
@@ -375,12 +354,6 @@ mod windows_setup {
375354
Ok(button)
376355
}
377356

378-
fn sibling_binary(name: &str) -> io::Result<PathBuf> {
379-
let mut path = std::env::current_exe()?;
380-
path.pop();
381-
Ok(path.join(name))
382-
}
383-
384357
fn wide(value: &str) -> Vec<u16> {
385358
value.encode_utf16().chain(iter::once(0)).collect()
386359
}

0 commit comments

Comments
 (0)