Repository navigation
Follow up script-install updates with pruning and a Windows daemon restart #1496
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2536,7 +2536,10 @@ kcap feedback --feedback # send feedback; prompts | |
| > Releases and checks its sha256 against the manifest, installing nothing on a | ||
| > mismatch. Each release gets its own `versions/<version>` directory and the | ||
| > `current` link is switched to it, so running processes keep the files they | ||
| > started from; then the new binary runs `kcap refresh`. A daemon service | ||
| > started from; then the new binary runs `kcap refresh`. The version it replaced | ||
| > stays; older ones are deleted unless something still runs from them. A daemon | ||
| > started from a script install restarts on the new version once it is idle, | ||
| > Windows included. A daemon service | ||
|
Comment on lines
+2541
to
+2542
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Qualify the Windows auto-restart guidance. If a Windows daemon started on an earlier release, this statement promises an automatic restart that will not occur. State that the daemon must run a release with the new monitoring behavior; otherwise, the user must run 🤖 Prompt for AI Agents |
||
| > installed from a script install is moved onto the `current` path, so it | ||
| > follows updates. Switching from npm, a daemon service installed under npm keeps | ||
| > running the npm binary: run `kcap daemon service install` again once the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,11 @@ npm, downloads this platform's archive from GitHub Releases and checks its | |
| sha256 against the manifest; any mismatch installs nothing. The release | ||
| goes into its own versions/<version> directory and the `current` link is | ||
| switched to it, so running processes keep the files they started from. | ||
| The new binary then runs `kcap refresh`. A daemon service installed from a | ||
| script install is moved onto the `current` path, so it follows updates. | ||
| The new binary then runs `kcap refresh`. The version it replaced stays; | ||
| older ones are deleted unless something still runs from them. A daemon | ||
| started from a script install restarts on the new version once it is idle, | ||
| Windows included. A daemon service installed from a script install is moved | ||
|
Comment on lines
+17
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. The merge subject fails commit policy The PR title and supplied commit subjects do not match the required single-clause subject format ending in (#digits), with hyphenated words and Windows also violating the prescribed word pattern. The merge subject can therefore fail the repository's commit-subject compliance check even though the change description and commits identify the work. Agent Prompt
|
||
| onto the `current` path, so it follows updates. | ||
| A daemon service installed under npm keeps running the npm binary after a | ||
| switch to the script install: run `kcap daemon service install` again. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,8 @@ internal enum RestartRequestResult { | |
| /// Polls the running binary; a size/mtime change queues a restart-after-update that | ||
| /// fires the chosen <see cref="IRestartStrategy"/> the moment the daemon is idle | ||
| /// (no hosted agents, no in-flight eval). Also handles explicit requests from the | ||
| /// control socket. Self-detection is macOS/Linux only (the poll is a no-op on Windows, | ||
| /// where a running binary can't be replaced); the explicit request path works anywhere. | ||
| /// control socket. On Windows self-detection covers script installs only; the explicit | ||
| /// request path works anywhere. | ||
| /// </summary> | ||
| internal sealed partial class RestartCoordinator : BackgroundService { | ||
| readonly TimeProvider _time; | ||
|
|
@@ -78,18 +78,20 @@ internal void PrimeBaseline() { | |
| lock (_gate) _baseline = StatBinary(); | ||
| } | ||
|
|
||
| static BinaryStat? StatProcessBinary() { | ||
| // Windows can't replace a running binary in place, so self-detection is a no-op | ||
| // there: returning null means BinaryChanged() never sees a change. Explicit | ||
| // restart requests still work (they set _pending directly). Keeping the OS check | ||
| // here — rather than in Tick() — leaves the gate logic OS-agnostic and testable. | ||
| if (OperatingSystem.IsWindows()) return null; | ||
| static BinaryStat? StatProcessBinary() => StatBinaryAt(Environment.ProcessPath, OperatingSystem.IsWindows(), File.Exists); | ||
|
|
||
| /// <summary> | ||
| /// What the poll compares: the binary reached through a script install's <c>current</c>, which an | ||
| /// update switches rather than overwriting this file. Outside a script install Windows never replaces | ||
| /// a running binary in place, so there it returns null and BinaryChanged() never fires; explicit | ||
| /// restart requests still work. | ||
| /// </summary> | ||
| internal static BinaryStat? StatBinaryAt(string? processPath, bool isWindows, Func<string, bool> fileExists) { | ||
| if (processPath is null) return null; | ||
| if (isWindows && ScriptInstallLayout.FromBinary(processPath, fileExists) is null) return null; | ||
|
|
||
| try { | ||
| // A script install flips `current` rather than replacing this file, so watch the path through it. | ||
| var p = Environment.ProcessPath is { } exe ? ScriptInstallLayout.Stabilize(exe) : null; | ||
| if (p is null) return null; | ||
| var fi = new FileInfo(p); | ||
| var fi = new FileInfo(ScriptInstallLayout.Stabilize(processPath, fileExists)); | ||
| return fi.Exists ? new BinaryStat(fi.Length, fi.LastWriteTimeUtc.Ticks) : null; | ||
|
Comment on lines
+94
to
95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Windows daemons miss some script updates StatBinaryAt reduces the executable reached through current to its size and modification time, without recording which version the junction targets. If an update switches to a different daemon binary with the same size and modification time, BinaryChanged sees no difference and the running Windows daemon never queues its restart. Agent Prompt
|
||
| } catch { | ||
| return null; // transient (file being swapped mid-install) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,7 +13,7 @@ namespace Capacitor.Cli.Commands; | |||||||||||||||||||||||||||||||||||||||||||||||||
| /// Installs one release into a script install, the way the install script does: download the RID's | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// archive, verify it against the release manifest's sha256, extract it into its own | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <c>versions/<v></c> and switch <c>current</c> to it. A running kcap keeps its own files, because | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// no version directory that is in use is ever written. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// no version directory that is in use is ever written or pruned. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <para>Verification fails closed: a manifest that cannot be read, has no valid checksum for this RID, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// or does not match the downloaded bytes installs nothing.</para> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,6 +23,9 @@ public sealed partial class ScriptUpdater(KcapReleaseClient releases) { | |||||||||||||||||||||||||||||||||||||||||||||||||
| /// a test cannot make one off Windows.</summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal Action<ScriptInstallLayout, string> SwitchCurrent { get; init; } = DefaultSwitch; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary>Whether something runs from a version directory, so pruning must keep it.</summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal Func<string, bool> InUse { get; init; } = AnyFileInUse; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| internal async Task<bool> InstallAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ScriptInstallLayout layout, string version, string channel, string rid, TextWriter stdout, TextWriter stderr, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| CancellationToken ct) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,8 +56,9 @@ internal async Task<bool> InstallAsync( | |||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| var dest = layout.VersionDir(version); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ActiveVersionDir(layout) is { } active && PathsEqual(active, dest)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var dest = layout.VersionDir(version); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var previous = ActiveVersionDir(layout); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (previous is not null && PathsEqual(previous, dest)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await stdout.WriteLineAsync($"kcap {version} is already the active version."); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| TryWriteMarker(layout, channel, stderr); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -117,6 +121,9 @@ internal async Task<bool> InstallAsync( | |||||||||||||||||||||||||||||||||||||||||||||||||
| TryWriteMarker(layout, channel, stderr); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await stdout.WriteLineAsync($"Installed kcap {version} to {layout.Root}"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| var pruned = PruneVersions(layout, keep: [dest, previous]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pruned > 0) await stdout.WriteLineAsync($"Removed {pruned} unused kcap version{(pruned == 1 ? "" : "s")}."); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { Directory.Delete(work, recursive: true); } catch { /* best-effort */ } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,6 +162,57 @@ internal static void TryWriteMarker(ScriptInstallLayout layout, string channel, | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Deletes the version directories and moved-aside copies an update leaves behind, keeping | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <paramref name="keep"/>: the new version and the one it replaced, which a daemon or agent started | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// before the switch is likely still running. Anything else in use stays too. Returns how many went. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal int PruneVersions(ScriptInstallLayout layout, IEnumerable<string?> keep) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var kept = keep.OfType<string>().ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var pruned = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<DirectoryInfo> candidates; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| candidates = new DirectoryInfo(layout.Versions).EnumerateDirectories() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .Where(d => d.LinkTarget is null) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .Where(d => ReleaseManifest.IsValidVersion(d.Name) || d.Name.StartsWith(".replaced-", StringComparison.Ordinal)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .Where(d => !kept.Any(k => PathsEqual(k, d.FullName))) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var dir in candidates) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (InUse(dir.FullName)) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| dir.Delete(recursive: true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pruned++; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Started after the probe, or not ours to delete: the next update tries again. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+185
to
+194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: set -eu
printf '%s\n' '--- diff ---'
git diff --unified=80 49872ae5b356fd7f96c06a6e0312a9ab0e193ec0 f9b3edb3a83b2f814a1fc599d90ac2e93e5da99c -- src/Capacitor.Cli/Commands/ScriptUpdater.cs
printf '%s\n' '--- file outline/search ---'
rg -n -F -e 'class ScriptUpdater' -e 'AnyFileInUse' -e 'InUse' -e 'LinkTarget' -e 'Delete(recursive' src/Capacitor.Cli/Commands/ScriptUpdater.cs
printf '%s\n' '--- source ---'
nl -ba src/Capacitor.Cli/Commands/ScriptUpdater.cs | sed -n '1,280p'
printf '%s\n' '--- relevant project guidance ---'
find . -maxdepth 3 -type f \( -iname 'contributing*' -o -iname '*guideline*' -o -iname 'Directory.Build.*' -o -name '*.editorconfig' \) -printRepository: kurrent-io/kcap-cli Length of output: 30743 🏁 Script executed: set -eu
printf '%s\n' '--- target frameworks ---'
rg -n -F -e '<TargetFramework' -e '<TargetFrameworks' -e 'TargetFramework' src/Capacitor.Cli/Capacitor.Cli.csproj Directory.Build.props src/Directory.Build.props
printf '%s\n' '--- link/prune tests ---'
rg -n -F -e 'PruneVersions' -e 'AnyFileInUse' -e 'SymbolicLink' -e 'junction' -e 'symlink' -e 'replaced-' test src/Capacitor.Cli.Tests.Unit || test "$?" -eq 1Repository: kurrent-io/kcap-cli Length of output: 41407 🌐 Web query:
💡 Result: Skip reparse points in
🐛 Suggested fix- foreach (var file in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) {
+ var options = new EnumerationOptions {
+ RecurseSubdirectories = true,
+ AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint,
+ };
+ foreach (var file in Directory.EnumerateFiles(dir, "*", options)) {🤖 Prompt for AI AgentsSource: Learnings |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return pruned; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// True when a file in <paramref name="dir"/> cannot be opened exclusively for writing. A running | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// executable refuses that on Windows (sharing violation) and Linux (ETXTBSY); macOS does not, but | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// deleting a running binary there leaves the process its file. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal static bool AnyFileInUse(string dir) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var file in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using var _ = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+204
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '155,220p' src/Capacitor.Cli/Commands/ScriptUpdater.cs
sed -n '10,35p' src/Capacitor.Cli/Commands/ScriptUpdater.csRepository: kurrent-io/kcap-cli Length of output: 4682 🏁 Script executed: set -eu
printf '%s\n' '--- references and tests ---'
rg -n -F --glob '*.cs' -e 'AnyFileInUse' -e 'PruneVersions' -e 'InUse' -e 'ScriptUpdaterTests' src tests || true
printf '%s\n' '--- project targets ---'
rg -n -F --glob '*.{csproj,props,targets}' -e 'TargetFramework' -e 'TargetFrameworks' -e 'RuntimeIdentifier' . || true
printf '%s\n' '--- focused diff ---'
git diff 49872ae5b356fd7f96c06a6e0312a9ab0e193ec0 f9b3edb3a83b2f814a1fc599d90ac2e93e5da99c -- src/Capacitor.Cli/Commands/ScriptUpdater.cs tests src | sed -n '1,260p'Repository: kurrent-io/kcap-cli Length of output: 17707 🌐 Web query:
💡 Result: 🏁 Script executed: set -eu
printf '%s\n' '--- focused test files ---'
rg -n -F --glob '*.cs' -e 'ScriptUpdater' -e 'PruneVersions' -e 'AnyFileInUse' -e 'InUse' test/Capacitor.Cli.Tests.Unit test/Capacitor.Cli.Tests.Integration || true
printf '%s\n' '--- updater flow ---'
nl -ba src/Capacitor.Cli/Commands/ScriptUpdater.cs | sed -n '1,235p'
printf '%s\n' '--- test source excerpts ---'
for f in $(rg -l -F --glob '*.cs' -e 'PruneVersions' -e 'AnyFileInUse' -e 'ScriptUpdater' test/Capacitor.Cli.Tests.Unit test/Capacitor.Cli.Tests.Integration | head -20); do
echo "### $f"
nl -ba "$f" | sed -n '1,280p'
doneRepository: kurrent-io/kcap-cli Length of output: 36699 🏁 Script executed: set -eu
printf '%s\n' '--- runtime file loading symbols ---'
rg -n -F --glob '*.cs' \
-e 'AppContext.BaseDirectory' \
-e 'Environment.ProcessPath' \
-e 'AssemblyLoadContext' \
-e 'Assembly.LoadFrom' \
-e 'File.OpenRead' \
-e 'File.ReadAll' \
-e 'FileStream(' \
-e 'Directory.EnumerateFiles' \
-e 'Directory.GetFiles' \
-e 'LoadFromAssemblyPath' \
src/Capacitor.Cli src/Capacitor.Cli.Daemon src/Capacitor.App src/Capacitor.Cli.Core || true
printf '%s\n' '--- plugin/resource loading terms ---'
rg -n -i --glob '*.{cs,csproj,json,md}' \
-e 'plugin' -e 'resource' -e 'lazy' -e 'base directory' \
src/Capacitor.Cli src/Capacitor.Cli.Daemon src/Capacitor.App src/Capacitor.Cli.Core | sed -n '1,240p'Repository: kurrent-io/kcap-cli Length of output: 41764 Disable pruning on macOS when using the default probe. On macOS, a running executable can pass Suggested fix--- "a/src/Capacitor.Cli/Commands/ScriptUpdater.cs"
+++ "b/src/Capacitor.Cli/Commands/ScriptUpdater.cs"
@@ -201,10 +201,12 @@
/// executable refuses that on Windows (sharing violation) and Linux (ETXTBSY); macOS does not, but
/// deleting a running binary there leaves the process its file.
/// </summary>
internal static bool AnyFileInUse(string dir) {
+ if (OperatingSystem.IsMacOS()) return true;
+
try {
foreach (var file in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) {
using var _ = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
return false;Permission failures should remain conservative and retain the directory. Opening and closing the file does not establish an mtime change, and the inspected source does not establish a lazy-load failure. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary>The version directory <c>current</c> points at, or null when it is not a link.</summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal static string? ActiveVersionDir(ScriptInstallLayout layout) => | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new DirectoryInfo(layout.Current).LinkTarget is { } target | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,4 +120,31 @@ public async Task Request_restart_when_busy_queues_then_fires_when_idle() { | |
| c.Tick(); | ||
| await Assert.That(spy.Calls).IsEqualTo(1); | ||
| } | ||
|
|
||
| /// <summary>On Windows only a script install is watched: an update switches its <c>current</c>, so the | ||
| /// binary reached through it changes while the file the daemon runs from does not.</summary> | ||
| [Test] | ||
| public async Task On_Windows_a_script_install_is_watched_through_current() { | ||
| using var tmp = new TempDir(); | ||
| tmp.CreateFile("kcap/install.json", "{}"); | ||
| var running = tmp.CreateFile("kcap/versions/1.0.0/bin/kcap-daemon", "old"); | ||
| var viaCurrent = tmp.CreateFile("kcap/current/bin/kcap-daemon", "old"); | ||
|
|
||
| var before = RestartCoordinator.StatBinaryAt(running, isWindows: true, File.Exists); | ||
| File.WriteAllText(viaCurrent, "the new version"); | ||
| var after = RestartCoordinator.StatBinaryAt(running, isWindows: true, File.Exists); | ||
|
Comment on lines
+133
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Test cleanup misses direct writes On_Windows_a_script_install_is_watched_through_current rewrites viaCurrent with File.WriteAllText instead of using the temporary-directory helper for the file operation. Because the path belongs to tmp, this bypasses the helper's filesystem-state conventions and can leave future changes inconsistent with its cleanup model. Agent Prompt
|
||
|
|
||
| await Assert.That(before).IsNotNull(); | ||
| await Assert.That(RestartDecision.BinaryChanged(before, after)).IsTrue(); | ||
| await Assert.That(File.ReadAllText(running)).IsEqualTo("old"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task On_Windows_a_binary_outside_a_script_install_is_not_watched() { | ||
| using var tmp = new TempDir(); | ||
| var exe = tmp.CreateFile("npm/bin/kcap-daemon", "x"); | ||
|
|
||
| await Assert.That(RestartCoordinator.StatBinaryAt(exe, isWindows: true, File.Exists)).IsNull(); | ||
| await Assert.That(RestartCoordinator.StatBinaryAt(exe, isWindows: false, File.Exists)).IsNotNull(); | ||
| } | ||
| } | ||
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.
2. The pr lacks a closing issue reference
📘 Rule violation§ ComplianceAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation toolsDismiss ↗ | View ↗