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" ) ]
5053use std:: io;
51- # [ cfg ( target_os = "windows" ) ]
52- use std :: path :: Path ;
54+
55+ use mdo_cli :: handler ;
5356
5457#[ cfg( target_os = "windows" ) ]
5558const MDO_BIN : & str = "mdo.exe" ;
@@ -60,12 +63,16 @@ const MDO_BIN: &str = "mdo";
6063const SETUP_BIN : & str = "mdo-setup" ;
6164
6265fn 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.
139166fn 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" ) ]
150184fn 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+ }
0 commit comments