Skip to content

fix(windows): settle scheduler registration verification - #868

Open
ventianima-lab wants to merge 1 commit into
lidge-jun:devfrom
ventianima-lab:fix/windows-scheduler-verification-settle-dev
Open

fix(windows): settle scheduler registration verification#868
ventianima-lab wants to merge 1 commit into
lidge-jun:devfrom
ventianima-lab:fix/windows-scheduler-verification-settle-dev

Conversation

@ventianima-lab

@ventianima-lab ventianima-lab commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • retry only transient post-create Task Scheduler visibility/XML health states
  • preserve fail-closed behavior for conflicts, missing assets, and unknown SCM status
  • stop late reconciliation when attempt ownership changes

Verification

  • 136 focused tests passed across scheduler, startup, service, and install-verification contracts
  • bun run typecheck passed
  • live Windows startup protection reports the service installed, viable, running, and conflict-free

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows Task Scheduler setup reliability by retrying transient registration states for a limited period.
    • Immediate failures are now handled for conflicts, missing assets, and unknown service status.
    • Prevented changes when setup ownership is lost, reducing the risk of unintended rollback or state updates.
  • Tests

    • Added coverage for retries, rollback scenarios, persistent failures, and interrupted setup ownership.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Windows Task Scheduler installation finalization now uses bounded, injectable verification retries. It stops immediately for non-transient failures and stops without mutation when attempt ownership is lost. Tests cover recovery, conflicts, exhaustion, rollback, and ownership loss.

Changes

Scheduler finalization

Layer / File(s) Summary
Ownership-aware settling verification
src/service.ts
Lines 667-668 add an injectable settle-delay hook. Lines 741-770 add bounded retries with ownership checks. Lines 800-804 replace immediate verification. Line 821 protects rollback with a final ownership check.
Reconciliation behavior coverage
tests/windows-elevation-spawn.test.ts
Lines 322-359 test transient recovery. Lines 361-395 test immediate conflict failure and rollback. Lines 397-431 test retry exhaustion. Lines 433-470 test ownership loss without rollback or state persistence.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ElevatedCreateRun
  participant SchedulerVerification
  participant InstallState
  participant ParentRollback
  ElevatedCreateRun->>SchedulerVerification: Reconcile scheduler registration
  SchedulerVerification->>SchedulerVerification: Retry transient states with bounded delays
  SchedulerVerification->>InstallState: Persist state after successful verification
  SchedulerVerification->>ParentRollback: Roll back after non-retriable failure or exhaustion
Loading

Possibly related PRs

Suggested reviewers: wibias, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the fix for Windows Task Scheduler registration verification, which is the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Aug 1, 2026
@ventianima-lab
ventianima-lab marked this pull request as ready for review August 1, 2026 18:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/service.ts`:
- Line 821: Keep the attemptStillOwned guard in the post-verification path, and
add a concise comment explaining that queueMicrotask may change ownership
between the earlier check and the await resumption. Add a focused test in
windows-elevation-spawn.test.ts that changes ownership from the final verify
hook, then asserts rollback and writeInstallState are not invoked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2bc2d8c9-79b7-46f5-b3fc-94bff5ffc856

📥 Commits

Reviewing files that changed from the base of the PR and between aae9426 and 5309bf4.

📒 Files selected for processing (2)
  • src/service.ts
  • tests/windows-elevation-spawn.test.ts

Comment thread src/service.ts
"Installation state was not written.",
]);
}
if (!attemptStillOwned(options)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Bun's async/await and Promise microtask scheduling follow the same ordering guarantees as V8/Node.js (at least one microtask tick between an async function's internal return and the caller's await resuming)?

💡 Result:

Bun's async/await and Promise microtask scheduling is fundamentally driven by the JavaScriptCore (JSC) engine's microtask queue, which necessitates a microtask tick between an async function's await suspension and its resumption [1][2][3]. While Bun is designed to be highly compatible with Node.js, it does not guarantee identical microtask ordering guarantees in all edge cases because it must integrate Node.js-compatible concepts (like process.nextTick) into the JSC event loop model [3][4]. In Node.js (V8), the await keyword effectively wraps the awaited value in a promise and schedules the function's continuation as a microtask [1][5]. This ensures that the remainder of the async function runs after the current synchronous execution has completed and the microtask queue begins to drain [1][2][5]. Bun follows this core mechanism: await suspends the function, and resumption occurs via the microtask queue [2][3]. However, discrepancies exist in how Bun and Node.js manage the broader queue priority: 1. Queue Priority: In Node.js, process.nextTick callbacks are generally executed before microtasks (Promise reactions) [2][6]. Bun has made significant efforts to align with this behavior, including implementing specialized hooks to ensure process.nextTick and microtask ordering matches expectations in most common scenarios [7][8][3]. 2. Engine Differences: Bun uses JSC, while Node.js uses V8 [3]. V8 has specific optimizations for await (e.g., reducing the number of microtask ticks required for already-resolved promises) [1]. While Bun's JSC integration also manages microtasks, the underlying implementation and timing of "draining" these queues (often involving vm.drainMicrotasks() or specific event loop task wrappers) can differ from V8's internal task scheduling [9][10][11]. Developers should not rely on strict cross-runtime microtask ordering, as the exact sequence—especially involving the interaction between process.nextTick and promise reactions—can vary between V8 and JSC, and even across different versions of Bun [6][4]. While Bun aims for drop-in compatibility, it maintains a unique event loop architecture [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- service.ts relevant region ---'
sed -n '740,835p' src/service.ts

printf '%s\n' '--- focused test region ---'
sed -n '300,485p' tests/windows-elevation-spawn.test.ts

printf '%s\n' '--- ownership and verification references ---'
rg -n -C 3 'attemptStillOwned|verifyWindowsSchedulerInstallAfterSettle|stillOwnsAttempt|rollbackElevatedSchedulerTask|writeInstallState' src/service.ts tests/windows-elevation-spawn.test.ts

Repository: lidge-jun/opencodex

Length of output: 29411


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- available runtimes ---'
command -v bun || true
bun --version 2>/dev/null || true
command -v node || true
node --version 2>/dev/null || true

printf '%s\n' '--- async/await microtask ordering probe ---'
probe='
const events = [];
let owned = true;

async function verifyAfterSettle() {
  const verify = () => {
    events.push("verify");
    queueMicrotask(() => {
      owned = false;
      events.push("flip");
    });
    return { ok: false };
  };
  const value = verify();
  events.push(`inner-check:${owned}`);
  return owned ? value : null;
}

async function apply() {
  const verification = await verifyAfterSettle();
  events.push(`outer-resume:${owned}:${verification !== null}`);
  if (!verification || owned) return "no-rollback";
  events.push("rollback");
  return "rollback";
}

apply().then(result => {
  events.push(`result:${result}`);
  console.log(JSON.stringify(events));
});
'
if command -v bun >/dev/null 2>&1; then
  bun -e "$probe"
fi
if command -v node >/dev/null 2>&1; then
  node -e "$probe"
fi

printf '%s\n' '--- relevant test setup and later ownership test ---'
sed -n '1,120p' tests/windows-elevation-spawn.test.ts
sed -n '700,755p' tests/windows-elevation-spawn.test.ts

Repository: lidge-jun/opencodex

Length of output: 6815


🌐 Web query:

site:bun.sh/docs queueMicrotask Promise microtask await Bun JavaScriptCore

💡 Result:

In Bun, which is powered by the JavaScriptCore engine, the execution order of microtasks is consistent with standard ECMAScript specifications and the requirements of the Web-standard APIs it implements [1][2][3]. When working with queueMicrotask, Promise resolutions, and async/await, the following principles apply: 1. Microtask Queue: Both Promise callbacks (e.g., .then, .catch, .finally) and tasks queued via queueMicrotask() are placed into the microtask queue [2][4]. 2. Execution Order: Microtasks are executed after the currently executing script and before control is returned to the event loop [1]. When an await expression is encountered, the remainder of the async function is effectively scheduled as a microtask when the awaited promise resolves. 3. Bun and JavaScriptCore: Bun uses the Apple-developed JavaScriptCore engine [5][3]. Because Bun implements queueMicrotask() as a standard Web API, its behavior is designed to be fully compatible with environments where these mechanisms are expected [1][4]. The engine processes the microtask queue exhaustively before moving on to the next macrotask (such as timers, I/O callbacks, or DOM events). In practice, this means that if you have a mix of await expressions and queueMicrotask calls, they will all be processed in the order they were scheduled as part of the same microtask checkpoint [1][2]. There are no specialized deviations in Bun's handling of these primitives compared to other modern JavaScript runtimes [3][4].

Citations:


Document and test the post-verification ownership race.

At src/service.ts:821, queueMicrotask can change ownership after the check at line 768 but before the await at line 803 resumes. Keep this guard and add a comment explaining this microtask boundary. Add a focused test in tests/windows-elevation-spawn.test.ts that flips ownership from the final verify hook and asserts that rollback and writeInstallState are skipped.

Suggested comment
+    // Ownership can change during the microtask that resumes this function
+    // after the await above, so keep this check before rollback.
     if (!attemptStillOwned(options)) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service.ts` at line 821, Keep the attemptStillOwned guard in the
post-verification path, and add a concise comment explaining that queueMicrotask
may change ownership between the earlier check and the await resumption. Add a
focused test in windows-elevation-spawn.test.ts that changes ownership from the
final verify hook, then asserts rollback and writeInstallState are not invoked.

Source: Path instructions

@lidge-jun

Copy link
Copy Markdown
Owner

Thanks for tightening up the Windows scheduler verification path. Two remaining asks before this is ready:

  1. Please add the ownership-loss edge test from the open CodeRabbit thread. Keep the post-verification attemptStillOwned guard, add the focused coverage in tests/windows-elevation-spawn.test.ts that changes ownership from the final verify hook, and assert that neither rollback nor writeInstallState is invoked. The current ownership-loss test flips ownership from settleDelay, so it does not cover this await-resumption edge.
  2. Please run the full platform CI matrix on the current head (5309bf4). At the moment, only the target/label checks and CodeRabbit have run.

happy to re-review quickly once both are in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants