1- use std:: { fs, thread} ;
21use std:: env;
2+ use std:: io:: Read ;
3+ use std:: { fs, thread} ;
34use std:: path:: { Path , PathBuf } ;
45use std:: ffi:: { OsStr , OsString } ;
56use std:: sync:: mpsc;
@@ -51,6 +52,73 @@ pub fn exit_blocking(code: i32) {
5152 std:: process:: exit ( code) ;
5253}
5354
55+ fn kill_other_instances ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
56+ // Determine the image name of the current executable
57+ let this_exe = env:: current_exe ( ) ?;
58+ let image_name = this_exe. file_name ( )
59+ . and_then ( |s| s. to_str ( ) )
60+ . ok_or ( "Failed to determine current executable name" ) ?
61+ . to_string ( ) ;
62+
63+ let this_pid = std:: process:: id ( ) ;
64+ info ! ( "Attempting to terminate other running instances of {}..." , image_name) ;
65+
66+ // Query tasklist for processes with the same image name, in CSV for easier parsing -- somewhat hacky but works
67+ let output = Command :: new ( "tasklist" )
68+ . args ( [ "/FI" , & format ! ( "IMAGENAME eq {}" , image_name) , "/FO" , "CSV" ] )
69+ . output ( ) ?;
70+
71+ if !output. status . success ( ) {
72+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
73+ warn ! ( "tasklist failed while searching for other instances: {}" , stderr) ;
74+ return Ok ( ( ) ) ;
75+ }
76+
77+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
78+ let mut killed_any = false ;
79+
80+ for ( i, line) in stdout. lines ( ) . enumerate ( ) {
81+ if i == 0 { continue ; } // skip header
82+ let trimmed = line. trim ( ) ;
83+ if trimmed. is_empty ( ) { continue ; }
84+ // CSV fields quoted, expect: "Image Name","PID","Session Name","Session#","Mem Usage"
85+ // We'll split commas and trim surrounding quotes.
86+ let parts: Vec < String > = trimmed. split ( ',' )
87+ . map ( |s| s. trim ( ) . trim_matches ( '"' ) . to_string ( ) )
88+ . collect ( ) ;
89+ if parts. len ( ) < 2 { continue ; }
90+ let pid_str = & parts[ 1 ] ;
91+ if let Ok ( pid) = pid_str. parse :: < u32 > ( ) {
92+ if pid == this_pid {
93+ continue ; // skip self
94+ }
95+ // Attempt to kill this PID
96+ let kill = Command :: new ( "taskkill" ) . args ( [ "/PID" , & pid. to_string ( ) , "/F" ] ) . output ( ) ;
97+ match kill {
98+ Ok ( res) => {
99+ if res. status . success ( ) {
100+ info ! ( "Terminated process PID {} ({})" , pid, image_name) ;
101+ killed_any = true ;
102+ } else {
103+ let stderr = String :: from_utf8_lossy ( & res. stderr ) ;
104+ // If the process exited between list and kill, ignore the error.
105+ warn ! ( "Failed to terminate PID {}: {}" , pid, stderr) ;
106+ }
107+ }
108+ Err ( e) => warn ! ( "taskkill failed for PID {}: {}" , pid, e) ,
109+ }
110+ }
111+ }
112+
113+ if killed_any {
114+ // Allow a brief moment for the OS to release file handles
115+ thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
116+ }
117+
118+ Ok ( ( ) )
119+ }
120+
121+
54122pub fn handle_installation ( args : & Cli ) {
55123 if !check_elevated ( ) . unwrap_or ( false ) {
56124 info ! ( "Requesting administrator privileges..." ) ;
@@ -63,6 +131,12 @@ pub fn handle_installation(args: &Cli) {
63131 std:: process:: exit ( 0 ) ; // Exit the non-elevated process
64132 }
65133
134+ // We are elevated here; proactively terminate any other running instances to avoid file-in-use errors.
135+ if let Err ( e) = kill_other_instances ( ) {
136+ warn ! ( "Failed to terminate other instances automatically: {}" , e) ;
137+ warn ! ( "Continuing with installation; this may fail if files are locked." ) ;
138+ }
139+
66140 let mut install_path = None ;
67141 if let Some ( path_str) = & args. install {
68142 info ! ( "Starting installation..." ) ;
@@ -72,7 +146,7 @@ pub fn handle_installation(args: &Cli) {
72146 install_path = Some ( path) ;
73147 }
74148 Err ( e) => {
75- error ! ( "Installation failed (does the file already exist and a process running?) : {:}" , e) ;
149+ error ! ( "Installation failed: {:}" , e) ;
76150 exit_blocking ( 1 ) ;
77151 }
78152 }
@@ -122,16 +196,61 @@ pub fn handle_installation(args: &Cli) {
122196
123197fn install_executable ( target : & str ) -> Result < PathBuf , Box < dyn std:: error:: Error > > {
124198 let current_exe = env:: current_exe ( ) ?;
125- let target_path = PathBuf :: from ( target) ;
199+ let input_path = PathBuf :: from ( target) ;
200+
201+ fn compute_file_hash ( path : & Path ) -> Result < blake3:: Hash , Box < dyn std:: error:: Error > > {
202+ let mut file = fs:: File :: open ( path) ?;
203+ let mut hasher = blake3:: Hasher :: new ( ) ;
204+ let mut buf = [ 0u8 ; 8192 ] ;
205+ loop {
206+ let read = file. read ( & mut buf) ?;
207+ if read == 0 { break ; }
208+ hasher. update ( & buf[ ..read] ) ;
209+ }
210+ Ok ( hasher. finalize ( ) )
211+ }
126212
127- // TODO: Check if the path is a directory, if so append the assembly name
213+ // Ensure the target is a directory (existing or to be created). We do not accept file paths.
214+ if fs:: exists ( & input_path) ? {
215+ let meta = fs:: metadata ( & input_path) ?;
216+ if meta. is_file ( ) {
217+ return Err ( format ! ( "Install target '{}' is a file; expected a directory" , input_path. display( ) ) . into ( ) ) ;
218+ }
219+ // It exists and is a directory
220+ fs:: create_dir_all ( & input_path) ?;
221+ } else {
222+ // If the user passed a path that looks like a file (e.g., ends with .exe), reject it
223+ if input_path. extension ( ) . is_some_and ( |ext| ext. to_string_lossy ( ) . eq_ignore_ascii_case ( "exe" ) ) {
224+ return Err ( format ! ( "Install target '{}' appears to be a file path; please specify a directory" , input_path. display( ) ) . into ( ) ) ;
225+ }
226+ fs:: create_dir_all ( & input_path) ?;
227+ }
128228
229+ // Construct the final target file path using the fixed executable name
230+ let target_path = input_path. join ( "wallpaper-controller.exe" ) ;
231+
232+ // If target exists, compare hashes before copying
129233 if fs:: exists ( & target_path) ? {
234+ match ( compute_file_hash ( & current_exe) , compute_file_hash ( & target_path) ) {
235+ ( Ok ( src_hash) , Ok ( dst_hash) ) => {
236+ if src_hash == dst_hash {
237+ info ! ( "Same version already present at {} (hash {})." , target_path. display( ) , src_hash. to_hex( ) ) ;
238+ info ! ( "Skipping copy." ) ;
239+ return Ok ( target_path) ;
240+ } else {
241+ info ! ( "Different contents detected at {}." , target_path. display( ) ) ;
242+ info ! ( "Updating..." ) ;
243+ }
244+ }
245+ ( e1, e2) => {
246+ warn ! ( "Failed to compute hash for comparison (src: {:?}, dst: {:?})." , e1. err( ) , e2. err( ) ) ;
247+ info ! ( "Proceeding to replace file." ) ;
248+ }
249+ }
250+ // Remove old file before copy
130251 fs:: remove_file ( & target_path) ?;
131- }
132-
133- if let Some ( parent) = target_path. parent ( ) {
134- fs:: create_dir_all ( parent) ?;
252+ } else {
253+ info ! ( "Installing new copy to {}" , target_path. display( ) ) ;
135254 }
136255
137256 fs:: copy ( & current_exe, & target_path) ?;
@@ -144,7 +263,7 @@ fn setup_startup_service(exe_path: &Path, launch_args: Vec<OsString>) -> Result<
144263 ensure_wallpaper_engine_service_present ( ) ?;
145264
146265 // If switching from scheduled task to service, remove the scheduled task first
147- info ! ( "Setting up as a Windows Service. If a scheduled task exists, it will be removed. " ) ;
266+ info ! ( "Setting up as a Windows Service." ) ;
148267 if let Err ( e) = remove_existing_task_if_any ( ) {
149268 warn ! ( "Failed while attempting to remove existing scheduled task '{}': {}" , TASK_NAME , e) ;
150269 }
@@ -191,7 +310,8 @@ fn setup_startup_scheduled_task(exe_path: &Path, launch_args: Vec<OsString>) ->
191310 let username = std:: env:: var ( "USERNAME" ) . unwrap_or_else ( |_| String :: from ( "%USERNAME%" ) ) ;
192311
193312 // If switching from service to scheduled task, remove the service first
194- info ! ( "Setting up as a Scheduled Task. If a Windows Service exists, it will be removed." ) ;
313+ info ! ( "Setting up as a Scheduled Task." ) ;
314+ info ! ( "If a startup service installation exists, it will be removed." ) ;
195315 let manager = ServiceManager :: local_computer ( None :: < & OsStr > , ServiceManagerAccess :: all ( ) ) ?;
196316 if let Err ( e) = remove_existing_service_if_any ( & manager, SERVICE_NAME , Duration :: from_secs ( 6 ) ) {
197317 warn ! ( "Failed while attempting to remove existing service '{}': {}" , SERVICE_NAME , e) ;
0 commit comments