From 8999327d04c5f85510bf1e00d13bafb485f3f428 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Mon, 18 May 2026 09:41:43 +0200 Subject: [PATCH 1/2] feat(cua-driver): autostart {enable|disable|status|kick} CLI verb (Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New subcommand that registers / inspects / triggers a logon-time Scheduled Task for `cua-driver serve` — the Windows-native equivalent of the macOS LaunchAgent install.sh registers. Lives in crates/cua-driver/src/autostart.rs and shells to PowerShell's Register-ScheduledTask + schtasks.exe under the hood (mirroring install.ps1 exactly so the two stay in lock-step). Four subcommands: cua-driver autostart enable Register the Scheduled Task with LogonType=Interactive so it lands in a Session 1+ logon (never Session 0). Idempotent — replaces any existing entry of the same name. cua-driver autostart disable Unregister. No-op if the entry is already absent ("does not exist" / "cannot find the file specified" schtasks.exe messages are mapped to success because the goal is "no entry registered"). cua-driver autostart status Emits one of: not-registered registered (not running) registered (running) The "running" check reuses the daemon's own is_daemon_listening probe against \\.\pipe\cua-driver — no `tasklist` round-trip. cua-driver autostart kick schtasks /Run /TN cua-driver-serve. Brings the daemon up for the current session without re-logging. macOS / Linux: stub implementations return a helpful error pointing the user at `scripts/install-local.sh --autostart` (which already writes a LaunchAgent plist on macOS and a systemd --user unit on Linux). A cross-platform native impl is tracked as a follow-up. Telemetry: a new event `cua_driver_autostart_` fires on every invocation (per-subcommand split so PostHog can show enable vs disable adoption separately). The `` segment is normalised via the existing sanitize_tool_name helper. install.ps1 + install-local.ps1: the local Register-CuaDriverAutostart helper is reduced to `& $exe autostart enable` (4 lines). The post-install hint message now points at the verb instead of a multi-line PowerShell recipe. One source of truth for the registration logic, in Rust, where it can be unit-tested if needed. Validated end-to-end on Win11 VM: status (clean) -> not-registered enable -> Registered autostart entry 'cua-driver-serve' status -> registered (not running) kick -> Started... + daemon listening status -> registered (running) disable -> Removed autostart entry disable (again) -> Removed autostart entry (no-op) enable -> Registered autostart entry schtasks /Query -> Logon Mode: Interactive only, Run As User: ... --- .../crates/cua-driver/src/autostart.rs | 283 ++++++++++++++++++ .../crates/cua-driver/src/cli.rs | 33 +- .../crates/cua-driver/src/main.rs | 9 + libs/cua-driver-rs/scripts/install-local.ps1 | 11 +- libs/cua-driver-rs/scripts/install.ps1 | 80 ++--- 5 files changed, 348 insertions(+), 68 deletions(-) create mode 100644 libs/cua-driver-rs/crates/cua-driver/src/autostart.rs diff --git a/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs b/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs new file mode 100644 index 0000000000..f4a753989c --- /dev/null +++ b/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs @@ -0,0 +1,283 @@ +//! `cua-driver autostart {enable|disable|status|kick}` — register / inspect / +//! trigger the platform-native auto-start mechanism so `cua-driver serve` +//! comes up on every interactive logon without the user pasting a +//! startup one-liner. +//! +//! ## Platform mapping +//! +//! - **Windows**: Scheduled Task `cua-driver-serve` registered with +//! `LogonType: Interactive` so it lands in a Session 1+ logon (never +//! Session 0). Equivalent to what `scripts/install.ps1 -AutoStart` +//! does — the install script can call out to this subcommand to +//! keep the registration logic in one place. +//! - **macOS / Linux**: not implemented yet. Returns an error pointing +//! the user at the manual recipe (`launchctl` / `systemctl --user`). +//! `scripts/install-local.sh --autostart` covers the manual path +//! today. +//! +//! ## Why shell out (Windows) +//! +//! The Task Scheduler 2.0 COM surface (`ITaskService`, `ITaskDefinition`, +//! `ITaskFolder`, `IPrincipal`, ...) is ~10 nested COM-wrapper calls in +//! Rust before you've even configured the principal, with multiple BSTR +//! marshalling steps and a lot of "this method takes a VARIANT, that +//! one takes a BSTR" footguns. Shelling out to PowerShell's +//! `Register-ScheduledTask` cmdlet — which itself uses Task Scheduler +//! 2.0 under the hood — gets us identical behavior in 5 lines and stays +//! exactly in lock-step with what `scripts/install.ps1` does (literally +//! the same command). When `install.ps1` evolves, this code follows it +//! for free. + +use anyhow::{anyhow, Result}; + +/// Canonical task / unit name. Used by every platform. +pub const TASK_NAME: &str = "cua-driver-serve"; + +/// Reported by `status`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// No autostart entry registered. + NotRegistered, + /// Entry registered but not currently running. + RegisteredIdle, + /// Entry registered AND a `cua-driver serve` process is live. + RegisteredRunning, +} + +impl Status { + pub fn tag(self) -> &'static str { + match self { + Status::NotRegistered => "not-registered", + Status::RegisteredIdle => "registered (not running)", + Status::RegisteredRunning => "registered (running)", + } + } +} + +// ── Public API ──────────────────────────────────────────────────────────── + +/// Register the platform-native autostart entry for `cua-driver serve`. +/// Idempotent: any existing entry with the same name is replaced. +pub fn enable() -> Result<()> { + let exe = current_exe_for_autostart()?; + platform::enable(&exe) +} + +/// Remove the autostart entry. No-op if none is registered. +pub fn disable() -> Result<()> { + platform::disable() +} + +/// Report whether the entry is registered and whether the daemon is running. +pub fn status() -> Result { + platform::status() +} + +/// Run the autostart entry immediately without waiting for a fresh logon. +/// Errors if the entry isn't registered. +pub fn kick() -> Result<()> { + platform::kick() +} + +/// Find the cua-driver executable to bake into the autostart entry. +/// Uses `std::env::current_exe`, canonicalised to its real path (resolves +/// junction / symlink chains so a versioned upgrade flipping `current` +/// stays transparent to the registered task). The resolved path is what +/// gets stored in the Scheduled Task / LaunchAgent / unit file. +fn current_exe_for_autostart() -> Result { + let exe = std::env::current_exe() + .map_err(|e| anyhow!("could not resolve current executable: {e}"))?; + let canonical = std::fs::canonicalize(&exe).unwrap_or(exe); + let path = canonical.to_string_lossy().into_owned(); + // On Windows, `canonicalize` returns a `\\?\C:\...` extended-length + // path. PowerShell + the Task Scheduler XML schema both handle it + // correctly, but it looks alarming in `schtasks /Query` output. + // Strip the prefix for readability — the unprefixed form is still + // valid as long as the path fits MAX_PATH (260 chars), which any + // realistic install will. + #[cfg(target_os = "windows")] + let path = path + .strip_prefix(r"\\?\") + .map(str::to_owned) + .unwrap_or(path); + Ok(path) +} + +// ── Windows impl ────────────────────────────────────────────────────────── + +#[cfg(target_os = "windows")] +mod platform { + use super::*; + use std::process::Command; + + /// Inline PowerShell that mirrors `install.ps1::Register-CuaDriverAutostart` + /// exactly. Kept as a single one-liner so a quick `gh-blame` diff against + /// install.ps1 surfaces any divergence; the moment install.ps1 changes + /// shape, this script needs the same edit. + const REGISTER_PS: &str = r#" +$ErrorActionPreference = 'Stop' +$user = "$env:COMPUTERNAME\$env:USERNAME" +$action = New-ScheduledTaskAction -Execute $env:CUA_DRIVER_AS_EXE -Argument 'serve' -WorkingDirectory $env:USERPROFILE +$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user +$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Hours 0) +Unregister-ScheduledTask -TaskName 'cua-driver-serve' -Confirm:$false -ErrorAction SilentlyContinue +Register-ScheduledTask -TaskName 'cua-driver-serve' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'cua-driver-rs: serve daemon, auto-start at interactive logon' | Out-Null +"#; + + pub fn enable(exe: &str) -> Result<()> { + // Pass the binary path via env var so the script doesn't need + // shell-quoting acrobatics for paths with spaces or odd chars. + let out = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", REGISTER_PS]) + .env("CUA_DRIVER_AS_EXE", exe) + .output() + .map_err(|e| anyhow!("failed to invoke powershell: {e}"))?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + return Err(anyhow!( + "PowerShell Register-ScheduledTask failed (exit {}): {}", + out.status.code().unwrap_or(-1), + stderr.trim() + )); + } + Ok(()) + } + + pub fn disable() -> Result<()> { + // schtasks /Delete returns 0 on success, 1 on "task not found" + // (which we treat as success: the goal is "no task registered" + // and it already isn't). Match on stderr text rather than exit + // code because schtasks doesn't distinguish "doesn't exist" from + // "permission denied" via exit code. + let out = Command::new("schtasks") + .args(["/Delete", "/TN", TASK_NAME, "/F"]) + .output() + .map_err(|e| anyhow!("failed to invoke schtasks: {e}"))?; + if out.status.success() { + return Ok(()); + } + let stderr = String::from_utf8_lossy(&out.stderr); + let stdout = String::from_utf8_lossy(&out.stdout); + let combined = format!("{stdout}{stderr}").to_lowercase(); + if combined.contains("does not exist") + || combined.contains("cannot find the file specified") + || combined.contains("the system cannot find") + { + return Ok(()); + } + Err(anyhow!( + "schtasks /Delete failed (exit {}): {}", + out.status.code().unwrap_or(-1), + stderr.trim() + )) + } + + pub fn status() -> Result { + // schtasks /Query exits 0 with task details on stdout, or 1 with + // "ERROR: The system cannot find the file specified." on stderr. + let out = Command::new("schtasks") + .args(["/Query", "/TN", TASK_NAME]) + .output() + .map_err(|e| anyhow!("failed to invoke schtasks: {e}"))?; + if !out.status.success() { + return Ok(Status::NotRegistered); + } + // Registered — now check whether `cua-driver serve` is running. + // Avoid invoking `tasklist` (slow ~200ms on first run); use the + // same registry the daemon's own status command uses via a + // direct check on the named pipe. + if crate::serve::is_daemon_listening(&crate::serve::default_socket_path()) { + Ok(Status::RegisteredRunning) + } else { + Ok(Status::RegisteredIdle) + } + } + + pub fn kick() -> Result<()> { + let out = Command::new("schtasks") + .args(["/Run", "/TN", TASK_NAME]) + .output() + .map_err(|e| anyhow!("failed to invoke schtasks: {e}"))?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + return Err(anyhow!( + "schtasks /Run failed (exit {}): {}", + out.status.code().unwrap_or(-1), + stderr.trim() + )); + } + Ok(()) + } +} + +// ── macOS / Linux stubs ─────────────────────────────────────────────────── + +#[cfg(not(target_os = "windows"))] +mod platform { + use super::*; + + const NOT_YET: &str = + "cua-driver autostart is currently Windows-only. macOS users: see \ + libs/cua-driver-rs/scripts/install-local.sh --autostart for the \ + LaunchAgent recipe. Linux users: same script registers a systemd \ + --user unit. A cross-platform impl is tracked as a follow-up."; + + pub fn enable(_exe: &str) -> Result<()> { + Err(anyhow!(NOT_YET)) + } + pub fn disable() -> Result<()> { + Err(anyhow!(NOT_YET)) + } + pub fn status() -> Result { + Err(anyhow!(NOT_YET)) + } + pub fn kick() -> Result<()> { + Err(anyhow!(NOT_YET)) + } +} + +// ── CLI dispatcher ──────────────────────────────────────────────────────── + +/// `cua-driver autostart ` entry point. Prints user-facing +/// output and exits the process via `std::process::exit` so the caller +/// (main) doesn't need to plumb back an exit code for every subcommand. +pub fn run_autostart_cmd(subcommand: &str) { + let (verb_result, success_text): (Result<()>, String) = match subcommand { + "enable" => (enable(), format!( + "Registered autostart entry '{TASK_NAME}'.\n \ + cua-driver serve will start at every interactive logon." + )), + "disable" => (disable(), format!( + "Removed autostart entry '{TASK_NAME}' (no-op if it was already absent)." + )), + "status" => match status() { + Ok(s) => { + println!("{}", s.tag()); + std::process::exit(0); + } + Err(e) => { + eprintln!("cua-driver autostart status: {e}"); + std::process::exit(1); + } + }, + "kick" => (kick(), format!( + "Started autostart entry '{TASK_NAME}' for the current session." + )), + other => { + eprintln!("Unknown autostart subcommand: {other:?}"); + eprintln!("Usage: cua-driver autostart {{enable|disable|status|kick}}"); + std::process::exit(64); + } + }; + match verb_result { + Ok(()) => { + println!("{success_text}"); + std::process::exit(0); + } + Err(e) => { + eprintln!("cua-driver autostart {subcommand}: {e}"); + std::process::exit(1); + } + } +} diff --git a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs index dbd8bfa03a..0ef6a53814 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs @@ -65,6 +65,13 @@ pub enum Command { /// after install. Subsequent runs see the `.installation_recorded` /// marker file and become no-ops. TelemetryInstallEvent, + /// `cua-driver autostart {enable|disable|status|kick}` — + /// platform-native auto-start so `cua-driver serve` comes up on + /// every logon. Windows: Scheduled Task with LogonType=Interactive + /// (lands in Session 1+). macOS / Linux: not yet implemented; the + /// stub returns a helpful "use install-local.sh --autostart" + /// message. See `crates/cua-driver/src/autostart.rs`. + Autostart { subcommand: String }, } /// Flags whose next token is a value (not a subcommand). @@ -90,7 +97,13 @@ pub fn parse_command() -> Command { if args.iter().any(|a| a == "--help" || a == "-h") { println!("cua-driver {} — cross-platform computer-use automation driver", env!("CARGO_PKG_VERSION")); println!("Usage: cua-driver [SUBCOMMAND] [OPTIONS]"); - println!("Subcommands: mcp, list-tools, describe, call, serve, stop, status, config, recording, update, doctor, diagnose"); + println!("Subcommands: mcp, list-tools, describe, call, serve, stop, status, config, recording, update, doctor, diagnose, autostart"); + println!(); + println!("autostart options (Windows-only today):"); + println!(" cua-driver autostart enable Register a logon Scheduled Task so serve starts at every interactive logon."); + println!(" cua-driver autostart disable Remove the autostart entry. No-op if not registered."); + println!(" cua-driver autostart status Print whether the entry is registered + whether the daemon is running."); + println!(" cua-driver autostart kick Start the entry now without re-logging."); println!(); println!("mcp options (macOS):"); println!(" --no-daemon-relaunch Stay in-process; skip auto-launching the CuaDriverRs daemon."); @@ -188,6 +201,17 @@ pub fn parse_command() -> Command { } } } + Some("autostart") => { + // No `cua-driver autostart` (no subcommand) shortcut today — + // every operation is destructive enough that we want the + // user to be explicit about which one. + let subcommand = pos.next().unwrap_or("").to_string(); + if subcommand.is_empty() { + eprintln!("Usage: cua-driver autostart {{enable|disable|status|kick}}"); + process::exit(64); + } + Command::Autostart { subcommand } + } Some(first) => { // Implicit call: unrecognised first positional → treat as tool name. let tool = first.to_string(); @@ -1448,6 +1472,13 @@ pub fn telemetry_entry_event(cmd: &Command) -> Option { Command::Update { .. } => "cua_driver_update".to_owned(), Command::Doctor { .. } => "cua_driver_doctor".to_owned(), Command::Diagnose => "cua_driver_diagnose".to_owned(), + // Per-subcommand event so dashboards can split enable / disable / + // status / kick separately — they have very different meanings + // for adoption. `sanitize_tool_name` already does what we want + // (lowercase ASCII / underscore-only / max 64 chars / fallback). + Command::Autostart { subcommand } => { + format!("cua_driver_autostart_{}", sanitize_tool_name(subcommand)) + } Command::TelemetryInstallEvent => return None, }; Some(name) diff --git a/libs/cua-driver-rs/crates/cua-driver/src/main.rs b/libs/cua-driver-rs/crates/cua-driver/src/main.rs index 1a763d3a0c..75cc52859b 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/main.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/main.rs @@ -24,6 +24,7 @@ //! //! On all other platforms `#[tokio::main]` is used directly. +mod autostart; mod bundle; mod cli; mod doctor; @@ -198,6 +199,10 @@ fn main() { cli::run_diagnose_cmd(reg); return; } + cli::Command::Autostart { subcommand } => { + autostart::run_autostart_cmd(&subcommand); + return; + } cli::Command::Config { subcommand, key, value, socket } => { let reg = Arc::new(platform_macos::register_tools()); reg.init_self_weak(); @@ -403,6 +408,10 @@ fn main() -> anyhow::Result<()> { cli::run_diagnose_cmd(reg); return Ok(()); } + cli::Command::Autostart { subcommand } => { + autostart::run_autostart_cmd(&subcommand); + return Ok(()); + } cli::Command::Config { subcommand, key, value, socket } => { let reg = Arc::new(build_registry_no_cursor()); reg.init_self_weak(); diff --git a/libs/cua-driver-rs/scripts/install-local.ps1 b/libs/cua-driver-rs/scripts/install-local.ps1 index 7b2fbf5124..fb2892a5af 100644 --- a/libs/cua-driver-rs/scripts/install-local.ps1 +++ b/libs/cua-driver-rs/scripts/install-local.ps1 @@ -98,13 +98,10 @@ function Register-CuaDriverAutostart { if (-not (Test-Path -LiteralPath $InstalledBinary)) { throw "binary not found at $InstalledBinary" } - $user = "$env:COMPUTERNAME\$env:USERNAME" - $action = New-ScheduledTaskAction -Execute $InstalledBinary -Argument 'serve' -WorkingDirectory $env:USERPROFILE - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user - $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Hours 0) - Unregister-ScheduledTask -TaskName 'cua-driver-serve' -Confirm:$false -ErrorAction SilentlyContinue - Register-ScheduledTask -TaskName 'cua-driver-serve' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'cua-driver-rs: serve daemon, auto-start at interactive logon' | Out-Null + & $InstalledBinary autostart enable + if ($LASTEXITCODE -ne 0) { + throw "cua-driver autostart enable failed (exit $LASTEXITCODE)" + } } function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } diff --git a/libs/cua-driver-rs/scripts/install.ps1 b/libs/cua-driver-rs/scripts/install.ps1 index d6e131213c..b67930fa90 100644 --- a/libs/cua-driver-rs/scripts/install.ps1 +++ b/libs/cua-driver-rs/scripts/install.ps1 @@ -465,53 +465,20 @@ function Ensure-Junction([string]$linkPath, [string]$targetPath) { # ---------- Auto-start Scheduled Task (Windows LaunchAgent equivalent) --- -# Idempotent registration of the cua-driver-serve Scheduled Task. -# -# - Trigger: at logon for the current local user. -# - Principal: LogonType=Interactive so the task lands in the user's -# Session 1+ (NOT Session 0); window-driving tools require it. -# - Settings: AllowStartIfOnBatteries, RestartCount=3 on failure with -# 1-minute backoff, ExecutionTimeLimit=0 (no time cap; serve is -# meant to live for the session). -# - On workgroup machines USERDOMAIN may be 'WORKGROUP' which won't -# resolve as a SAM account; the principal therefore uses -# "$COMPUTERNAME\$USERNAME" which is always valid for a local user. -# - The task is unregistered before re-creation so the helper is -# safe to run repeatedly (e.g. on install upgrade). +# Thin wrapper that delegates to `cua-driver autostart enable`. The binary +# itself owns the platform-specific registration logic so the install +# scripts and the runtime stay in lock-step — when the verb's behavior +# changes, this script picks it up automatically with no edit needed. function Register-CuaDriverAutostart { param([Parameter(Mandatory = $true)][string]$InstalledBinary) if (-not (Test-Path -LiteralPath $InstalledBinary)) { throw "binary not found at $InstalledBinary" } - - $user = "$env:COMPUTERNAME\$env:USERNAME" - $action = New-ScheduledTaskAction ` - -Execute $InstalledBinary ` - -Argument 'serve' ` - -WorkingDirectory $env:USERPROFILE - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user - $principal = New-ScheduledTaskPrincipal ` - -UserId $user ` - -LogonType Interactive ` - -RunLevel Limited - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -RestartCount 3 ` - -RestartInterval (New-TimeSpan -Minutes 1) ` - -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - $taskName = 'cua-driver-serve' - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'cua-driver-rs: serve daemon, auto-start at interactive logon' | Out-Null + & $InstalledBinary autostart enable + if ($LASTEXITCODE -ne 0) { + throw "cua-driver autostart enable failed (exit $LASTEXITCODE)" + } } # ---------- Concurrent-install lockfile ----------------------------------- @@ -930,17 +897,18 @@ else { if ($AutoStart) { Write-Host "" - Write-Host "Registering auto-start Scheduled Task 'cua-driver-serve'..." -ForegroundColor Cyan + Write-Host "Registering auto-start (cua-driver autostart enable)..." -ForegroundColor Cyan try { Register-CuaDriverAutostart -InstalledBinary $installedBinary - Write-Host " Registered. cua-driver serve will auto-start at every interactive logon." -ForegroundColor Green - Write-Host " Run now without re-logging: schtasks /Run /TN cua-driver-serve" - Write-Host " Remove with: schtasks /Delete /TN cua-driver-serve /F" + Write-Host " cua-driver serve will auto-start at every interactive logon." -ForegroundColor Green + Write-Host " Run now without re-logging: $installedBinary autostart kick" + Write-Host " Inspect: $installedBinary autostart status" + Write-Host " Remove: $installedBinary autostart disable" Write-Host "" } catch { - Write-Host " Failed to register Scheduled Task: $($_.Exception.Message)" -ForegroundColor Red - Write-Host " Install otherwise succeeded; re-run with -AutoStart or use the manual recipe below." + Write-Host " Failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " Install otherwise succeeded; re-run with -AutoStart or invoke '$installedBinary autostart enable' manually." Write-Host "" } } @@ -949,21 +917,13 @@ else { Auto-start at logon (Windows equivalent of macOS LaunchAgent): Run cua-driver serve automatically every time you sign in (RDP, console, etc.) - Re-run this installer with -AutoStart to register the task, OR paste: - - `$exe = '$installedBinary' - `$user = "`$env:COMPUTERNAME\`$env:USERNAME" - `$action = New-ScheduledTaskAction -Execute `$exe -Argument 'serve' -WorkingDirectory `$env:USERPROFILE - `$trigger = New-ScheduledTaskTrigger -AtLogOn -User `$user - `$principal = New-ScheduledTaskPrincipal -UserId `$user -LogonType Interactive -RunLevel Limited - `$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Hours 0) - Register-ScheduledTask -TaskName 'cua-driver-serve' -Action `$action -Trigger `$trigger -Principal `$principal -Settings `$settings - Then run it now without re-logging: - schtasks /Run /TN cua-driver-serve + Enable: $installedBinary autostart enable + Run now: $installedBinary autostart kick + Status: $installedBinary autostart status + Remove: $installedBinary autostart disable - Removal: - schtasks /Delete /TN cua-driver-serve /F + Or re-run this installer with -AutoStart for the same result. "@ Write-Host $autostartHint -ForegroundColor Cyan } From 0520200b58f8ee13a43f0f4e1dfbcda6ec69ebeb Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Mon, 18 May 2026 09:50:00 +0200 Subject: [PATCH 2/2] fix(autostart): domain-joined account format + space-safe install hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CR #1550 review feedback (both Major): 1. autostart.rs::REGISTER_PS hard-coded the principal as `$env:COMPUTERNAME\$env:USERNAME`. That works for workgroup machines (where USERDOMAIN equals "WORKGROUP" or COMPUTERNAME, neither of which resolves as a SAM principal) but breaks on domain-joined hosts where the principal must be `DOMAIN\username`. New domain selector prefers USERDOMAIN when it's a real third-party domain, falls back to COMPUTERNAME otherwise — covers both shapes. 2. install.ps1 post-install hint printed `$installedBinary autostart enable` as-is. If $installedBinary contains spaces (e.g. `C:\Program Files\...`) the resulting copy-paste fails PowerShell parsing. Wrap in `& "$installedBinary"` so the hint is copy-paste-safe regardless of install path. --- .../crates/cua-driver/src/autostart.rs | 17 ++++++++++++++++- libs/cua-driver-rs/scripts/install.ps1 | 16 ++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs b/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs index f4a753989c..cfbca497e0 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/autostart.rs @@ -114,9 +114,24 @@ mod platform { /// exactly. Kept as a single one-liner so a quick `gh-blame` diff against /// install.ps1 surfaces any divergence; the moment install.ps1 changes /// shape, this script needs the same edit. + /// + /// **Account-name format**: on domain-joined machines USERDOMAIN holds the + /// AD domain name (e.g. CORP) and the principal must be `CORP\username`. + /// On workgroup machines USERDOMAIN holds either the literal string + /// "WORKGROUP" or the COMPUTERNAME, and the principal must be + /// `COMPUTERNAME\username` — `WORKGROUP\username` errors with + /// "No mapping between account names and security IDs was done". The + /// $domain selector below picks USERDOMAIN when it's a real + /// (non-WORKGROUP, non-COMPUTERNAME) domain and falls back to + /// COMPUTERNAME otherwise, covering both shapes. const REGISTER_PS: &str = r#" $ErrorActionPreference = 'Stop' -$user = "$env:COMPUTERNAME\$env:USERNAME" +if ($env:USERDOMAIN -and $env:USERDOMAIN -ne 'WORKGROUP' -and $env:USERDOMAIN -ne $env:COMPUTERNAME) { + $domain = $env:USERDOMAIN +} else { + $domain = $env:COMPUTERNAME +} +$user = "$domain\$env:USERNAME" $action = New-ScheduledTaskAction -Execute $env:CUA_DRIVER_AS_EXE -Argument 'serve' -WorkingDirectory $env:USERPROFILE $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited diff --git a/libs/cua-driver-rs/scripts/install.ps1 b/libs/cua-driver-rs/scripts/install.ps1 index b67930fa90..0a0270dc42 100644 --- a/libs/cua-driver-rs/scripts/install.ps1 +++ b/libs/cua-driver-rs/scripts/install.ps1 @@ -901,14 +901,14 @@ if ($AutoStart) { try { Register-CuaDriverAutostart -InstalledBinary $installedBinary Write-Host " cua-driver serve will auto-start at every interactive logon." -ForegroundColor Green - Write-Host " Run now without re-logging: $installedBinary autostart kick" - Write-Host " Inspect: $installedBinary autostart status" - Write-Host " Remove: $installedBinary autostart disable" + Write-Host " Run now without re-logging: & `"$installedBinary`" autostart kick" + Write-Host " Inspect: & `"$installedBinary`" autostart status" + Write-Host " Remove: & `"$installedBinary`" autostart disable" Write-Host "" } catch { Write-Host " Failed: $($_.Exception.Message)" -ForegroundColor Red - Write-Host " Install otherwise succeeded; re-run with -AutoStart or invoke '$installedBinary autostart enable' manually." + Write-Host " Install otherwise succeeded; re-run with -AutoStart or invoke '& `"$installedBinary`" autostart enable' manually." Write-Host "" } } @@ -918,10 +918,10 @@ else { Auto-start at logon (Windows equivalent of macOS LaunchAgent): Run cua-driver serve automatically every time you sign in (RDP, console, etc.) - Enable: $installedBinary autostart enable - Run now: $installedBinary autostart kick - Status: $installedBinary autostart status - Remove: $installedBinary autostart disable + Enable: & "$installedBinary" autostart enable + Run now: & "$installedBinary" autostart kick + Status: & "$installedBinary" autostart status + Remove: & "$installedBinary" autostart disable Or re-run this installer with -AutoStart for the same result. "@