Skip to content

Commit 40eee11

Browse files
committed
Scheduled task WIP #2
1 parent bf68140 commit 40eee11

File tree

5 files changed

+167
-22
lines changed

5 files changed

+167
-22
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ single-instance = "0.3.3"
1616
windows-service = "0.8.0"
1717
windows-elevate = "0.1.0"
1818
nameof = "1.3.0"
19+
blake3 = "1.5"
1920

2021
[profile.release]
2122
lto = "fat" # Link-time optimization for better inlining

src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub struct Cli {
4040
#[arg(long, default_value = "https://c6caa06487e9769daccfbedcd8de6324@o504783.ingest.us.sentry.io/4509839881076736")]
4141
pub sentry_dsn: Option<String>,
4242

43-
/// Copy the assembly to the specified path and exit (must end with .exe)
43+
/// Install the executable into the specified directory and exit
4444
#[arg(long)]
4545
pub install: Option<String>,
4646

src/installer.rs

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use std::{fs, thread};
21
use std::env;
2+
use std::io::Read;
3+
use std::{fs, thread};
34
use std::path::{Path, PathBuf};
45
use std::ffi::{OsStr, OsString};
56
use 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+
54122
pub 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

123197
fn 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);

src/main.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,11 @@ async fn main() {
5656
let mut hasher = DefaultHasher::new();
5757
filtered_args[1..].join("|").hash(&mut hasher);
5858
let instance_mutex = SingleInstance::new(&format!("Global\\WallpaperController_{}", hasher.finish())).unwrap();
59-
let any_instance_mutex = SingleInstance::new("Global\\WallpaperController_Any").unwrap();
6059

6160
if !instance_mutex.is_single() {
6261
if !in_silent_mode {
6362
eprintln!("Another instance with the same arguments is already running.");
6463
drop(instance_mutex);
65-
drop(any_instance_mutex);
6664
exit_blocking(5);
6765
}
6866
return;
@@ -90,7 +88,7 @@ async fn main() {
9088

9189
tracing_subscriber::registry()
9290
.with(filter, )
93-
.with(tracing_subscriber::fmt::layer().with_ansi(ansi_colors))
91+
.with(tracing_subscriber::fmt::layer().with_ansi(ansi_colors).without_time())
9492
.with(
9593
sentry::integrations::tracing::layer().event_filter(|md|
9694
match *md.level() {
@@ -106,15 +104,9 @@ async fn main() {
106104
error!("Cannot use --add-startup-service with --add-startup-task");
107105
exit_blocking(8);
108106
}
109-
if any_instance_mutex.is_single() {
110-
drop(instance_mutex);
111-
drop(any_instance_mutex);
107+
drop(instance_mutex);
112108

113-
handle_installation(&cli);
114-
} else {
115-
error!("There are other instances of WallpaperController running. Please close them before attempting a reinstall.");
116-
exit_blocking(5);
117-
}
109+
handle_installation(&cli);
118110
}
119111

120112
// Check if we should list monitors

0 commit comments

Comments
 (0)