From 492a943b52163d985254ac9d9e59cb742f2b61d5 Mon Sep 17 00:00:00 2001 From: GiantForestStudio <119655663+GiantForestStudio@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:56:10 +0900 Subject: [PATCH 1/2] Decouple widget startup from Codex with supervised scheduled launch --- AGENTS.md | 4 +- PRIVACY.md | 1 + README.md | 6 ++- scripts/install.ps1 | 46 ++++++++++++++++---- scripts/uninstall.ps1 | 13 +++++- src/Program.cs | 8 +++- src/WeeklyUsageIndicator.csproj | 2 +- src/WidgetSupervisor.cs | 48 +++++++++++++++++++++ tests/WeeklyUsageIndicator.Tests/Program.cs | 24 +++++++++++ 9 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 src/WidgetSupervisor.cs diff --git a/AGENTS.md b/AGENTS.md index 9d399b0..86ae913 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ This repository contains a small Windows-only WinForms utility. Keep changes foc - Persist at most one credential-free Claude recovery snapshot containing only percentages, reset times, and update time. Use it only for transient cold-start failures, delete it after 24 hours, its Fable reset, or an authentication/schema failure, and never let it suppress the first live request in a new process. - Stop the app-server child process when the widget pauses or exits. - Preserve the single-instance mutex and the always-on-top tool-window behavior. +- The installer must launch the `--supervise` mode through the per-user interactive, least-privilege scheduled task, never directly from Codex. The supervisor retries nonzero widget exits/start failures only; normal Quit must end supervision. Do not substitute Task Scheduler RestartOnFailure for this loop: it did not retry an exited action in live tests. Disable/stop the task before upgrade or uninstall; filter processes by the current user's exact installed EXE path. ## Validation @@ -41,4 +42,5 @@ Then check that: 6. A temporary Claude command, network, or service error keeps the last successful value visible and reports the update delay in the tooltip. 7. A cold-start transient failure uses only a recent, unexpired sanitized snapshot and still attempts a live Claude request first. 8. The context menu works, the widget hides, and its app-server child exits when Codex closes. -9. `install.ps1` and `uninstall.ps1` only modify the current user's dedicated install directory and Startup shortcut. +9. `install.ps1` and `uninstall.ps1` only modify the current user's dedicated install directory, legacy Startup shortcut, and SID-named scheduled task. +10. The task owns the supervisor and its widget child independently of Codex. Forced termination of the widget child recovers after about one minute; normal Quit ends both processes and does not recover. Reinstall leaves one supervisor and one widget; uninstall removes the task before stopping the app. diff --git a/PRIVACY.md b/PRIVACY.md index b832ee9..c7a4c57 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -10,6 +10,7 @@ The application: - does not read, print, log, copy, or persist Claude authentication tokens; - does not collect account identifiers; - does not include telemetry; +- registers a per-user Windows logon/recovery task containing the local executable path and Windows user SID, with no stored password or elevated privileges; - makes no outbound request for Codex usage; - makes no direct Claude network request; it invokes `claude.exe` in safe mode without a shell or persistent session, caches successful `/usage` results in memory for ten minutes, and deletes the local recovery snapshot after 24 hours, its Fable reset, or an authentication/schema failure. diff --git a/README.md b/README.md index 03bc2dc..240fa51 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ An unofficial Windows widget that stays on top while Codex Desktop is running an - Supports dragging, copying the current values, toggling always-on-top, and turning the Claude panel on or off from the right-click menu. - Remembers the last dragged position and restores it on the next launch. - Hides while another foreground app is fullscreen, then returns at the saved position. -- Starts a small background watcher at Windows sign-in so it can follow future Codex launches. +- Uses a per-user Windows scheduled task at sign-in, so the widget runs independently of Codex. A lightweight supervisor restarts the widget after an abnormal exit, waiting one minute (up to 999 retries per supervisor run). The widget does not read or store login tokens, account details, or usage history. It stores only the latest successful Claude percentages, reset times, and update time for short-lived recovery. Claude Code itself owns authentication and token refresh. See [PRIVACY.md](PRIVACY.md). @@ -42,7 +42,7 @@ The widget does not read or store login tokens, account details, or usage histor .\scripts\install.ps1 ``` -The app is installed to `%LOCALAPPDATA%\CodexWeeklyUsageIndicator` and a per-user Startup shortcut is created. +The app is installed to `%LOCALAPPDATA%\CodexWeeklyUsageIndicator`. A per-user `CodexWeeklyUsageIndicator-` scheduled task starts its supervisor at sign-in, using the signed-in user's normal privileges without storing a password. The supervisor launches and watches the widget; two processes from the same EXE are expected, but only one window. Installation also starts the task immediately, checks that both processes appear, and then removes the old Startup shortcut. Windows Task Scheduler must be available; an installation error must be resolved before relying on automatic recovery. The saved window position and Claude visibility preference are kept locally in `settings.json` inside that install directory. One sanitized Claude recovery snapshot may be kept in `claude-usage-cache.json` and is ignored after 24 hours or after its Fable reset. To uninstall: @@ -53,6 +53,8 @@ To uninstall: Release binaries are currently unsigned, so Windows may display a warning. SHA-256 checksums are included with each release. +Choosing **종료** from the widget menu exits normally and also ends the supervisor. It starts again at your next Windows sign-in. To start it sooner with recovery enabled, run its `CodexWeeklyUsageIndicator-` task in Windows Task Scheduler or run `scripts\install.ps1` again. Launching the EXE directly does not enable supervision for that process. Stopping the scheduled task or killing the supervisor also stops automatic recovery until the task is started again. Uninstall removes the scheduled task before deleting the app. + ## Build from source ```powershell diff --git a/scripts/install.ps1 b/scripts/install.ps1 index e5ff6c2..8e1b38f 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,12 +6,21 @@ $installDirectory = Join-Path $env:LOCALAPPDATA 'CodexWeeklyUsageIndicator' $installedExecutable = Join-Path $installDirectory 'WeeklyUsageIndicator.exe' $startupDirectory = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup) $shortcutPath = Join-Path $startupDirectory 'Codex Weekly Usage Indicator.lnk' +$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value +$taskName = "CodexWeeklyUsageIndicator-$userSid" if (-not (Test-Path -LiteralPath $sourceExecutable -PathType Leaf)) { throw "Build output not found: $sourceExecutable. Download a release or run .\scripts\build.ps1 first." } -$runningIndicators = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue) +# Stop scheduler recovery before replacing the binary. Never stop another user's copy. +$existingTask = Get-ScheduledTask -TaskName $taskName -TaskPath '\' -ErrorAction SilentlyContinue +if ($existingTask) { + Disable-ScheduledTask -InputObject $existingTask | Out-Null + Stop-ScheduledTask -InputObject $existingTask +} +$runningIndicators = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue | + Where-Object { $_.Path -eq $installedExecutable }) if ($runningIndicators.Count -gt 0) { $runningIndicators | Stop-Process -Force foreach ($runningIndicator in $runningIndicators) { @@ -22,14 +31,33 @@ if ($runningIndicators.Count -gt 0) { New-Item -ItemType Directory -Path $installDirectory -Force | Out-Null Copy-Item -LiteralPath $sourceExecutable -Destination $installedExecutable -Force -$shell = New-Object -ComObject WScript.Shell -$shortcut = $shell.CreateShortcut($shortcutPath) -$shortcut.TargetPath = $installedExecutable -$shortcut.WorkingDirectory = $installDirectory -$shortcut.Description = 'Codex and Claude usage indicator' -$shortcut.Save() +$action = New-ScheduledTaskAction -Execute $installedExecutable -Argument '--supervise' -WorkingDirectory $installDirectory +$trigger = New-ScheduledTaskTrigger -AtLogOn -User $userSid +$principal = New-ScheduledTaskPrincipal -UserId $userSid -LogonType Interactive -RunLevel Limited +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` + -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew +Register-ScheduledTask -TaskName $taskName -TaskPath '\' -Action $action -Trigger $trigger ` + -Principal $principal -Settings $settings ` + -Description 'Starts the usage widget independently of Codex; retries abnormal exits after one minute. Normal Quit stays closed until the next logon or manual task start.' ` + -Force | Out-Null -Start-Process -FilePath $installedExecutable -WorkingDirectory $installDirectory -WindowStyle Hidden +# Scheduler launches outside the installing app's process lifetime/job. Starting +# the EXE directly here could tie it to Codex again during an app update. +Start-ScheduledTask -TaskName $taskName -TaskPath '\' +$started = $false +for ($attempt = 0; $attempt -lt 20; $attempt++) { + Start-Sleep -Milliseconds 500 + # The supervisor and its UI child must both be alive. + $started = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue | + Where-Object { $_.Path -eq $installedExecutable }).Count -eq 2 + if ($started) { break } +} +if (-not $started) { + throw "The scheduled task did not start the installed widget. Check '$taskName' in Windows Task Scheduler and rerun the installer after resolving the error." +} +if (Test-Path -LiteralPath $shortcutPath -PathType Leaf) { + Remove-Item -LiteralPath $shortcutPath -Force +} Write-Host "Installed: $installedExecutable" -Write-Host "Startup shortcut: $shortcutPath" +Write-Host "Logon and recovery task: $taskName" diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index bced380..86b4d4e 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -3,8 +3,19 @@ $ErrorActionPreference = 'Stop' $installDirectory = Join-Path $env:LOCALAPPDATA 'CodexWeeklyUsageIndicator' $startupDirectory = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup) $shortcutPath = Join-Path $startupDirectory 'Codex Weekly Usage Indicator.lnk' +$installedExecutable = Join-Path $installDirectory 'WeeklyUsageIndicator.exe' +$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value +$taskName = "CodexWeeklyUsageIndicator-$userSid" -$runningIndicators = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue) +# Remove recovery before stopping the app or deleting its files. +$existingTask = Get-ScheduledTask -TaskName $taskName -TaskPath '\' -ErrorAction SilentlyContinue +if ($existingTask) { + Disable-ScheduledTask -InputObject $existingTask | Out-Null + Stop-ScheduledTask -InputObject $existingTask + Unregister-ScheduledTask -TaskName $taskName -TaskPath '\' -Confirm:$false +} +$runningIndicators = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue | + Where-Object { $_.Path -eq $installedExecutable }) if ($runningIndicators.Count -gt 0) { $runningIndicators | Stop-Process -Force foreach ($runningIndicator in $runningIndicators) { diff --git a/src/Program.cs b/src/Program.cs index b7fc451..3ebe9c6 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -10,13 +10,16 @@ namespace WeeklyUsageIndicator; internal static class Program { [STAThread] - private static void Main(string[] args) + private static int Main(string[] args) { + if (args.Any(argument => argument.Equals("--supervise", StringComparison.OrdinalIgnoreCase))) + return WidgetSupervisor.Run(); + using var singleInstance = new Mutex( initiallyOwned: true, name: @"Local\CodexWeeklyUsageIndicator", createdNew: out var isFirstInstance); - if (!isFirstInstance) return; + if (!isFirstInstance) return 0; var previewMode = args.Any(argument => argument.Equals("--preview", StringComparison.OrdinalIgnoreCase)); @@ -24,6 +27,7 @@ private static void Main(string[] args) ApplicationConfiguration.Initialize(); Application.Run(new UsageIndicatorForm(previewMode)); GC.KeepAlive(singleInstance); + return 0; } } diff --git a/src/WeeklyUsageIndicator.csproj b/src/WeeklyUsageIndicator.csproj index 63613f2..f6aa3a2 100644 --- a/src/WeeklyUsageIndicator.csproj +++ b/src/WeeklyUsageIndicator.csproj @@ -9,7 +9,7 @@ WeeklyUsageIndicator WeeklyUsageIndicator app.manifest - 1.3.3 + 1.3.4 true none false diff --git a/src/WidgetSupervisor.cs b/src/WidgetSupervisor.cs new file mode 100644 index 0000000..0916462 --- /dev/null +++ b/src/WidgetSupervisor.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; +using System.Diagnostics; + +namespace WeeklyUsageIndicator; + +internal static class WidgetSupervisor +{ + internal static readonly TimeSpan RetryInterval = TimeSpan.FromMinutes(1); + internal const int MaximumRestarts = 999; + + internal static int Run() + { + using var singleSupervisor = new Mutex(true, + @"Local\CodexWeeklyUsageIndicator.Supervisor", out var isFirstInstance); + if (!isFirstInstance) return 0; + + var executable = Environment.ProcessPath + ?? throw new InvalidOperationException("Cannot locate the widget executable."); + var result = RunLoop(() => + { + using var child = Process.Start(new ProcessStartInfo(executable) + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = AppContext.BaseDirectory + }) ?? throw new Win32Exception("Cannot start the widget."); + child.WaitForExit(); + return child.ExitCode; + }, Thread.Sleep); + GC.KeepAlive(singleSupervisor); + return result; + } + + // A normal menu Quit returns zero and ends supervision. Do not depend on + // Task Scheduler RestartOnFailure: it did not retry an exited action in UAT. + internal static int RunLoop(Func runWidget, Action wait) + { + for (var restarts = 0; ; restarts++) + { + int exitCode; + try { exitCode = runWidget(); } + catch (Win32Exception) { exitCode = 1; } + + if (exitCode == 0 || restarts >= MaximumRestarts) return exitCode; + wait(RetryInterval); + } + } +} diff --git a/tests/WeeklyUsageIndicator.Tests/Program.cs b/tests/WeeklyUsageIndicator.Tests/Program.cs index 694648b..cad44c8 100644 --- a/tests/WeeklyUsageIndicator.Tests/Program.cs +++ b/tests/WeeklyUsageIndicator.Tests/Program.cs @@ -3,6 +3,7 @@ var tests = new (string Name, Func Run)[] { + ("supervisor retries abnormal exits but respects normal Quit", TestSupervisorAsync), ("official Claude /usage output is parsed", TestObservedUsageOutputAsync), ("usage without reset times remains valid through client and tooltip", TestUsageWithoutResetsAsync), ("optional limits fail independently", TestIndependentLimitsAsync), @@ -31,6 +32,29 @@ return; +static Task TestSupervisorAsync() +{ + var runs = 0; + var waits = 0; + var result = WidgetSupervisor.RunLoop(() => ++runs < 3 ? -1 : 0, interval => + { + Assert(interval == TimeSpan.FromMinutes(1), "recovery must not tight-loop"); + waits++; + }); + Assert(result == 0 && runs == 3 && waits == 2, "nonzero exits retry; normal Quit stops"); + runs = waits = 0; + result = WidgetSupervisor.RunLoop(() => { runs++; return 0; }, _ => waits++); + Assert(result == 0 && runs == 1 && waits == 0, "normal Quit must never relaunch"); + runs = waits = 0; + result = WidgetSupervisor.RunLoop(() => + { + runs++; + throw new System.ComponentModel.Win32Exception("test start failure"); + }, _ => waits++); + Assert(result != 0 && runs == 1000 && waits == 999, "failed starts have a bounded retry budget"); + return Task.CompletedTask; +} + static Task TestObservedUsageOutputAsync() { var now = new DateTimeOffset(2026, 9, 1, 4, 25, 0, TimeSpan.Zero); From f43ed2dc1ce38a047aaf3b316aeb7690eb7626e2 Mon Sep 17 00:00:00 2001 From: GiantForestStudio <119655663+GiantForestStudio@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:04:18 +0900 Subject: [PATCH 2/2] Guard lifecycle scripts against packaged AppData redirection --- .github/workflows/release.yml | 2 +- AGENTS.md | 1 + README.md | 4 ++- scripts/install-environment.ps1 | 47 +++++++++++++++++++++++++++++++++ scripts/install.ps1 | 8 ++++-- scripts/uninstall.ps1 | 2 ++ 6 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 scripts/install-environment.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09d6127..6f2fcdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: New-Item -ItemType Directory -Path 'package\scripts' -Force | Out-Null New-Item -ItemType Directory -Path 'package\dist' -Force | Out-Null Copy-Item -LiteralPath 'dist\WeeklyUsageIndicator.exe','dist\SHA256SUMS.txt' -Destination 'package\dist' - Copy-Item -LiteralPath 'scripts\install.ps1','scripts\uninstall.ps1' -Destination 'package\scripts' + Copy-Item -LiteralPath 'scripts\install.ps1','scripts\uninstall.ps1','scripts\install-environment.ps1' -Destination 'package\scripts' Copy-Item -LiteralPath 'README.md','PRIVACY.md','SECURITY.md','LICENSE' -Destination 'package' Compress-Archive -Path 'package\*' -DestinationPath "CodexWeeklyUsageIndicator-$env:GITHUB_REF_NAME-win-x64.zip" - name: Publish release diff --git a/AGENTS.md b/AGENTS.md index 86ae913..a6c4a8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ This repository contains a small Windows-only WinForms utility. Keep changes foc - Stop the app-server child process when the widget pauses or exits. - Preserve the single-instance mutex and the always-on-top tool-window behavior. - The installer must launch the `--supervise` mode through the per-user interactive, least-privilege scheduled task, never directly from Codex. The supervisor retries nonzero widget exits/start failures only; normal Quit must end supervision. Do not substitute Task Scheduler RestartOnFailure for this loop: it did not retry an exited action in live tests. Disable/stop the task before upgrade or uninstall; filter processes by the current user's exact installed EXE path. +- Check the install directory's final handle path before stopping tasks or processes: packaged shells can redirect AppData even without reporting a package identity. Package `install-environment.ps1` with both lifecycle scripts. ## Validation diff --git a/README.md b/README.md index 240fa51..5dea2db 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The widget does not read or store login tokens, account details, or usage histor 1. Download and extract the Windows zip from [Releases](https://github.com/GiantForestStudio/codex-weekly-usage-indicator/releases). 2. Review the included PowerShell scripts. -3. Run: +3. Open a standalone Windows PowerShell window (outside packaged apps such as Codex), change to the extracted directory, and run: ```powershell .\scripts\install.ps1 @@ -45,6 +45,8 @@ The widget does not read or store login tokens, account details, or usage histor The app is installed to `%LOCALAPPDATA%\CodexWeeklyUsageIndicator`. A per-user `CodexWeeklyUsageIndicator-` scheduled task starts its supervisor at sign-in, using the signed-in user's normal privileges without storing a password. The supervisor launches and watches the widget; two processes from the same EXE are expected, but only one window. Installation also starts the task immediately, checks that both processes appear, and then removes the old Startup shortcut. Windows Task Scheduler must be available; an installation error must be resolved before relying on automatic recovery. The saved window position and Claude visibility preference are kept locally in `settings.json` inside that install directory. One sanitized Claude recovery snapshot may be kept in `claude-usage-cache.json` and is ignored after 24 hours or after its Fable reset. +Run installation and removal outside packaged app terminals: Windows can redirect their AppData writes into an app-private folder that Task Scheduler cannot see, causing `0x80070002` even when that terminal reports the EXE exists. Both scripts check the real directory path and refuse redirected locations before changing tasks or running widgets. + To uninstall: ```powershell diff --git a/scripts/install-environment.ps1 b/scripts/install-environment.ps1 new file mode 100644 index 0000000..d94e9eb --- /dev/null +++ b/scripts/install-environment.ps1 @@ -0,0 +1,47 @@ +# AppData redirection can survive even when the shell reports no package identity. +# Check the actual directory handle before touching tasks, processes, or app data. +function Assert-WidgetInstallPath { + param([Parameter(Mandatory = $true)][string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return } + if (-not ('WeeklyUsageIndicator.InstallPath' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; +namespace WeeklyUsageIndicator { + public static class InstallPath { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern SafeFileHandle CreateFile(string path, uint access, + uint share, IntPtr security, uint disposition, uint flags, IntPtr template); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern uint GetFinalPathNameByHandle(SafeFileHandle handle, + StringBuilder path, uint length, uint flags); + public static string Resolve(string path) { + using (var handle = CreateFile(path, 0, 7, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero)) { + if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error()); + var resolved = new StringBuilder(32768); + var length = GetFinalPathNameByHandle(handle, resolved, (uint)resolved.Capacity, 0); + if (length == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + if (length >= resolved.Capacity) throw new InvalidOperationException("Install path is too long."); + var value = resolved.ToString(); + return value.StartsWith(@"\\?\") ? value.Substring(4) : value; + } + } + } +} +'@ + } + # MSIX may merge a real directory with app-private files. Checking only the + # directory would miss those redirected existing files during upgrades. + $candidates = @($Path) + @(Get-ChildItem -LiteralPath $Path -Recurse -Force | ForEach-Object { $_.FullName }) + foreach ($candidate in $candidates) { + $expected = [IO.Path]::GetFullPath($candidate).TrimEnd('\') + $actual = [WeeklyUsageIndicator.InstallPath]::Resolve($expected).TrimEnd('\') + if (-not $actual.Equals($expected, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Windows redirected the install folder. No tasks or running widgets were changed. Open Windows PowerShell from the Start menu (outside Codex or another packaged app), then run this script again.' + } + } +} diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 8e1b38f..73485c0 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,4 +1,5 @@ $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'install-environment.ps1') $repositoryRoot = Split-Path -Parent $PSScriptRoot $sourceExecutable = Join-Path $repositoryRoot 'dist\WeeklyUsageIndicator.exe' @@ -13,12 +14,16 @@ if (-not (Test-Path -LiteralPath $sourceExecutable -PathType Leaf)) { throw "Build output not found: $sourceExecutable. Download a release or run .\scripts\build.ps1 first." } +New-Item -ItemType Directory -Path $installDirectory -Force | Out-Null +Assert-WidgetInstallPath -Path $installDirectory + # Stop scheduler recovery before replacing the binary. Never stop another user's copy. $existingTask = Get-ScheduledTask -TaskName $taskName -TaskPath '\' -ErrorAction SilentlyContinue if ($existingTask) { Disable-ScheduledTask -InputObject $existingTask | Out-Null Stop-ScheduledTask -InputObject $existingTask } + $runningIndicators = @(Get-Process -Name 'WeeklyUsageIndicator' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $installedExecutable }) if ($runningIndicators.Count -gt 0) { @@ -28,7 +33,6 @@ if ($runningIndicators.Count -gt 0) { } } -New-Item -ItemType Directory -Path $installDirectory -Force | Out-Null Copy-Item -LiteralPath $sourceExecutable -Destination $installedExecutable -Force $action = New-ScheduledTaskAction -Execute $installedExecutable -Argument '--supervise' -WorkingDirectory $installDirectory @@ -53,7 +57,7 @@ for ($attempt = 0; $attempt -lt 20; $attempt++) { if ($started) { break } } if (-not $started) { - throw "The scheduled task did not start the installed widget. Check '$taskName' in Windows Task Scheduler and rerun the installer after resolving the error." + throw "The scheduled task did not start the installed widget. Check '$taskName' in Windows Task Scheduler. If running inside a packaged app such as Codex, rerun from a standalone Windows PowerShell window to avoid AppData redirection." } if (Test-Path -LiteralPath $shortcutPath -PathType Leaf) { Remove-Item -LiteralPath $shortcutPath -Force diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 86b4d4e..ab47bd4 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -1,4 +1,5 @@ $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'install-environment.ps1') $installDirectory = Join-Path $env:LOCALAPPDATA 'CodexWeeklyUsageIndicator' $startupDirectory = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup) @@ -6,6 +7,7 @@ $shortcutPath = Join-Path $startupDirectory 'Codex Weekly Usage Indicator.lnk' $installedExecutable = Join-Path $installDirectory 'WeeklyUsageIndicator.exe' $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $taskName = "CodexWeeklyUsageIndicator-$userSid" +Assert-WidgetInstallPath -Path $installDirectory # Remove recovery before stopping the app or deleting its files. $existingTask = Get-ScheduledTask -TaskName $taskName -TaskPath '\' -ErrorAction SilentlyContinue