-
Notifications
You must be signed in to change notification settings - Fork 0
Keep widget independent of Codex updates (v1.3.4) #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.' | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,67 @@ | ||
| $ErrorActionPreference = 'Stop' | ||
| . (Join-Path $PSScriptRoot 'install-environment.ps1') | ||
|
|
||
| $repositoryRoot = Split-Path -Parent $PSScriptRoot | ||
| $sourceExecutable = Join-Path $repositoryRoot 'dist\WeeklyUsageIndicator.exe' | ||
| $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) | ||
| 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) { | ||
| $runningIndicators | Stop-Process -Force | ||
| foreach ($runningIndicator in $runningIndicators) { | ||
| try { [void]$runningIndicator.WaitForExit(5000) } catch { } | ||
| } | ||
| } | ||
|
|
||
| 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. 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 | ||
| } | ||
|
|
||
| Write-Host "Installed: $installedExecutable" | ||
| Write-Host "Startup shortcut: $shortcutPath" | ||
| Write-Host "Logon and recovery task: $taskName" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<int> runWidget, Action<TimeSpan> wait) | ||
| { | ||
| for (var restarts = 0; ; restarts++) | ||
| { | ||
| int exitCode; | ||
| try { exitCode = runWidget(); } | ||
| catch (Win32Exception) { exitCode = 1; } | ||
|
|
||
| if (exitCode == 0 || restarts >= MaximumRestarts) return exitCode; | ||
| wait(RetryInterval); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a widget is already running without its supervisor—for example, after the supervisor is killed and the scheduled task is started again—the new supervisor's child reaches this branch. Returning 0 makes
RunLoopinterpret the mutex collision as a normal menu Quit and terminate the supervisor, so restarting the task does not restore recovery and a later widget crash remains unrecovered. Return a distinct retryable status for this case or have the supervisor wait for the existing widget.AGENTS.md reference: AGENTS.md:L25-L25
Useful? React with 👍 / 👎.