Skip to content

fix(vscode): self-heal a provisioned-but-unrunnable func Core Tools before blocking F5 - #9478

Merged
Brian Lam (lambrianmsft) merged 27 commits into
Azure:mainfrom
lambrianmsft:lambrianmsft-extract-func-self-heal
Aug 10, 2026
Merged

fix(vscode): self-heal a provisioned-but-unrunnable func Core Tools before blocking F5#9478
Brian Lam (lambrianmsft) merged 27 commits into
Azure:mainfrom
lambrianmsft:lambrianmsft-extract-func-self-heal

Conversation

@lambrianmsft

@lambrianmsft Brian Lam (lambrianmsft) commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Extracted from #9462. That PR is now fully superseded: the codeful design-time DLL-lock fix and the cross-platform codeful E2E coverage merged as #9463, and the Azurite startup-race fix is already on main (waitForAzuriteReady, ValidateEmulatorOptions, warnMissingAzureWebJobsStorage). The func Core Tools self-heal was the only piece left, so it ships here on its own, rebased on current main. #9462 is being closed as superseded.

Commit Type

  • feature - New functionality
  • fix - Bug fix
  • refactor - Code restructuring without behavior change
  • perf - Performance improvement
  • docs - Documentation update
  • test - Test-related changes
  • chore - Maintenance/tooling

Risk Level

  • Low - Minor changes, limited scope
  • Medium - Moderate changes, some user impact
  • High - Major changes, significant user/system impact

Why Medium: the code sits on preDebugValidate, which runs for every Logic Apps project (codeful and codeless), and it can trigger a managed-binaries reinstall on that path. It is tightly gated — see below — but reviewers should focus on the gating conditions.

What & Why

Nothing on the F5 path distinguished "func.exe exists on disk" from "func.exe actually runs".

preDebugValidate -> validateFuncCoreToolsInstalled -> isFuncToolsInstalled() spawns func --version. When that fails — because the extension-managed binaries are half-extracted, mid-reinstall, restored from a poisoned runtime-deps cache, or hang on Windows — the old fallback was the interactive modal:

You must have the Azure Functions Core Tools installed to debug your local functions.

That is a dead end for a user who is mid-provision (the binaries are being installed, they just aren't runnable yet), and it is unanswerable headlessly, so debug aborts or stalls.

1. Self-heal inside the managed-binaries validation path

validateFuncCoreToolsInstalled now keeps one branch per install path:

  1. azureLogicAppsStandard.validateFuncCoreTools disabled -> installed,
  2. existing runnable func -> installed,
  3. extension-managed binaries -> validateFuncCoreToolsInstalledBinaries(...),
  4. system/PATH install -> validateFuncCoreToolsInstalledSystem(...).

The managed-binaries validation branch first attempts a silent repair. It either waits for an existing in-flight install or reinstalls the managed binaries via installFuncCoreToolsBinaries, re-verifies func --version, and only falls back to the interactive modal when the repair still cannot produce a runnable func.

The self-heal is reached only when all of these hold, i.e. only in a state where debug was already going to dead-end:

  1. azureLogicAppsStandard.validateFuncCoreTools is enabled, and
  2. isFuncToolsInstalled() already returned false, and
  3. useBinariesDependencies() is true — the extension owns these binaries.

System/PATH installs are deliberately untouched: we don't own those binaries and should not silently reinstall over them. That path still goes straight to the existing prompt.

2. Repair is silent through the existing telemetry/error-handling pattern

Andrew's review feedback pointed out that the first version added too much branch and option plumbing. The repair now runs in its own nested callWithTelemetryAndErrorHandling('azureLogicAppsStandard.repairFuncCoreTools', ...) scope with context.errorHandling.rethrow = true and context.errorHandling.suppressDisplay = true.

That removed the bespoke { suppressUi: true } path from installFuncCoreTools.ts and binaries.ts. The installer is now install-only: it logs to the output channel and preserves in-flight coalescing/serialization, but it no longer reveals the Output panel by itself. Output reveal and manual-install follow-up prompts are owned by explicit user-selected install branches.

downloadAndExtractDependency also no longer owns download/checksum/extract toasts. It logs, records telemetry, cleans up partials, and throws; callers decide whether the surrounding action should surface the error. Activation/background dependency failures therefore remain log/telemetry-only under their existing suppressDisplay=true scopes, while user-initiated func installs still get the manual-install prompt if the install fails.

3. The repair must not race a concurrent install

installFuncCoreToolsBinaries had no in-flight guard, and extractDependency deletes and recreates the shared target directory. A failing func --version is exactly what you observe while another install is mid-extract, so the repair could start a second install on top of the first and re-corrupt the tree.

installFuncCoreTools.ts now tracks the active install (isFuncCoreToolsInstallInFlight() / waitForFuncCoreToolsInstall(), mirroring the existing inFlightBundleWork precedent in bundleFeed.ts), coalesces an identical concurrent request, and serializes otherwise. The repair awaits an install that is already running instead of starting another.

4. A hanging func must not stall F5 either (found by the Windows E2E leg)

tryExecuteCommand only settles on 'close'/'error'. A managed func that hangs instead of exiting therefore stalled validateFuncCoreToolsInstalled indefinitely: the self-heal never ran, no modal appeared, nothing was logged, and F5 silently did nothing — strictly worse than the failure mode above. The spawned process also kept a handle on the binary, so the next attempt to replace it failed with EBUSY.

This is not hypothetical: it is what the new Windows E2E leg hit on its first run. The gate was reached, the deliberately corrupted func was never repaired, and the retry could not overwrite func.exe because it was still locked — which only something spawned from it could do, since the debug task never started.

New executeCommandWithTimeout in cpUtils terminates a command that outlives its budget. Commands are spawned with shell: true, so the tracked pid is the shell; on Windows the whole tree is taken down with taskkill (matching startDesignTimeApi / funcHostTask), because killing only the shell leaves the real command running and still holding its file handles. The func --version probe is bounded at 60s — generous enough that a slow first run (on-access AV scanning a freshly extracted binary) is not mistaken for a broken one — and a hang is treated exactly like a failure so the repair runs.

The probe failure reason is now logged. It was the only signal distinguishing "func is broken" from "the gate never ran" when debug appears to do nothing, and its absence is what made the first Windows failure undiagnosable from CI artifacts.

Telemetry records funcRepairAttempted, funcRepairSucceeded, funcRepairAwaitedExistingInstall, and funcRepairError on the nested repair telemetry scope.

Impact of Change

  • Users: a partially-installed, not-yet-runnable, or hanging extension-managed func repairs itself instead of dead-ending F5 on a modal or stalling silently. When the repair succeeds, debug proceeds with no prompt at all. When it fails, behavior falls back to the same install/learn-more modal; user-selected install failures still show the manual-install follow-up.
  • Developers: no public API changes. The repair is module-private and now lives in the managed-binaries validation flow. installFuncCoreToolsBinaries and downloadAndExtractDependency no longer expose bespoke UI-suppression options; callers own UI surfacing through telemetry/error-handling scopes and branch-local prompts.
  • System: one additional installFuncCoreToolsBinaries call, only on the managed-binaries path and only when func --version has already failed. No new dependencies. .github/workflows/vscode-e2e.yml gains the two self-heal shards described below.

Test Plan

  • Unit tests added/updated — validateFuncCoreToolsInstalled (repair succeeds -> returns true without prompting; repair runs but func still won't run -> falls back to the prompt; the reinstall itself throws -> telemetry records it and falls back; an in-flight install is awaited rather than duplicated; a timed-out probe still triggers the repair; the validateFuncCoreTools-disabled opt-out; nested repair telemetry sets rethrow=true and suppressDisplay=true; silent repair does not reveal the Output panel; user-selected install does reveal it and still prompts for manual install on failure). cpUtils covers Windows taskkill tree-kill and non-Windows SIGKILL, clean exits, non-zero exits, and that tryExecuteCommand stays unbounded. installRuntimeDependencies and binaries cover install coalescing/serialization and the new caller-owned UI behavior.
  • E2E tests added/updated — new Phase 4.14 p414-funcrepair (funcRepair.test.ts): corrupts the managed func binary (containment-guarded, only ever writing under the resolved LA_E2E_RUNTIME_DEPS_ROOT), presses F5, and asserts the extension silently repairs it instead of showing the install modal, then restores. Runs on two CI shards — Linux (func-selfheal) and vscode-e2e-funcselfheal (windows-latest). A CI manifest guard fails the job rather than letting the test this.skip() into a vacuous pass.
  • Tested in: pnpm exec biome check --write <changed files>; focused unit run (4 files / 130 tests passed); full apps/vs-code-designer unit suite (127 files / 1563 tests passed); E2E TypeScript compile (npx tsup --config tsup.e2e.test.config.ts). A repo-local tsc --noEmit -p apps/vs-code-designer/tsconfig.json run is not a clean validation signal in this worktree because it reports pre-existing unrelated extension typing errors outside the touched files.

E2E evidence

Linux (func-selfheal) Windows (vscode-e2e-funcselfheal)
Result repaired=true, pastConnectionKeys=true repaired=true, pastConnectionKeys=true, attempt 1/2
Time from gate to repaired 29s 76s

pastConnectionKeys=true is what proves the test is not passing vacuously: it confirms execution actually reached preDebugValidate (the gate under test) rather than succeeding somewhere upstream.

The Windows timing is itself the confirmation of part 4: 76s ~= the 60s bounded probe firing, plus the reinstall. Linux, where the corrupt binary fails fast rather than hanging, takes 29s. Before the bound existed, the same Windows scenario never repaired at all within a 300s budget.

Contributors

Brian Lam (@lambrianmsft) — reported the codeful F5 regression that surfaced this defect and directed splitting it out of #9462 once the design-time, Azurite, and cross-platform E2E work had merged, asked for Windows coverage, and requested Andrew's review feedback be addressed by simplifying the flow.
Andrew Eldridge (@andrew-eldridge) — review feedback that folded repair into managed-binaries validation and replaced bespoke UI-suppression options with the established telemetry/error-handling pattern.

Screenshots/Videos

N/A — no UI changes. The only user-visible difference is the absence of a modal when the silent repair succeeds.

…efore blocking F5

Extracted from Azure#9462, which is now down to this one change: the codeful
design-time DLL-lock fix and the cross-platform codeful E2E coverage landed in
Azure#9463, and the Azurite startup-race fix is already on main.

Nothing on the F5 path distinguished "func.exe exists on disk" from "func.exe
actually runs". `preDebugValidate` -> `validateFuncCoreToolsInstalled` ->
`isFuncToolsInstalled()` spawns `func --version`; when that throws because the
managed binaries are half-extracted, mid-reinstall, or restored from a poisoned
runtime-deps cache, the only fallback was the interactive "You must have the
Azure Functions Core Tools installed" modal. That modal is a dead end for a user
mid-provision and is unanswerable headlessly, so debug just aborts.

`validateFuncCoreToolsInstalled` now calls `attemptManagedFuncCoreToolsRepair()`
first on the managed-binaries path: it silently reinstalls the managed binaries
and re-verifies `func --version`, and only falls back to the interactive modal
when the repair still can't produce a runnable func. Scoped to the managed
(`useBinariesDependencies`) path only - system installs are untouched, since we
don't own those binaries. Telemetry records `funcRepairAttempted`,
`funcRepairSucceeded`, and `funcRepairError`.

The E2E side of this work (`funcVersionRuns` executable-aware dependency gate)
already merged with Azure#9463, so this is product-only.

Unit coverage: repair succeeds, repair runs but func still won't run, and the
reinstall itself throwing - plus the `validateFuncCoreTools` opt-out path, which
brings the changed file to 82% line coverage (over the pr-coverage gate).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🤖 AI PR Validation Report

PR Review Results

Thank you for your submission! Here's detailed feedback on your PR title and body compliance:

PR Title

  • Current: fix(vscode): self-heal a provisioned-but-unrunnable func Core Tools before blocking F5
  • Issue: None — valid fix: prefix with a scope, and clearly descriptive of the behavior change.
  • Recommendation: No change needed.

Commit Type

  • Exactly one type selected: fix - Bug fix.
  • Matches the title prefix and the nature of the change (self-heal for a broken F5 path).

Risk Level

  • Declared Medium in the body and labeled risk:medium — they agree. This matches my advised estimate: the change is confined to the VS Code extension distribution (apps/vs-code-designer) and sits on the preDebugValidate/F5 path with an added managed-binaries reinstall, which is Medium (shared/extension runtime, moderate user impact) — not High (no security/auth, credentials, breaking API, or core logic-apps-shared changes) and not Low (more than a single isolated component/config/CI-only tweak). Correct as declared.

What & Why

  • Current: Detailed explanation of the provisioned-but-unrunnable func failure mode and the four-part self-heal/gating/in-flight/timeout design.
  • Issue: None — clear context and rationale, not a placeholder.
  • Recommendation: No change needed.

Impact of Change

  • All three audiences addressed: Users (silent repair vs. dead-end modal), Developers (no public API changes, removed bespoke UI-suppression options), System (one extra install call on the managed path only, new CI shards).
  • Recommendation:
    • Users: Already covered.
    • Developers: Already covered.
    • System: Already covered.

Test Plan

  • Both unit and E2E coverage present and confirmed in the diff: new/updated validateFuncCoreToolsInstalled, cpUtils, installRuntimeDependencies, binaries, and bundleFeed unit tests, plus the Phase 4.14 funcRepair.test.ts E2E and CI shards. Passes CHECK TESTS.

⚠️ Contributors


Screenshots/Videos

  • N/A is appropriate: the diff touches only apps/vs-code-designer (extension host + CI + tests), not libs/designer-ui/src, libs/designer/src, or apps/vs-code-react UI components, and there is no visual change. No screenshot required.

Summary Table

Section Status Recommendation
Title No change needed
Commit Type No change needed
Risk Level Medium is correct (extension/runtime, F5 path)
What & Why No change needed
Impact of Change No change needed
Test Plan No change needed
Contributors ⚠️ Already credited — optional
Screenshots/Videos N/A is appropriate (no UI change)

All checks pass. Title, commit type, risk level (declared Medium matches the advised estimate and the risk:medium label), What & Why, Impact, and Test Plan are all compliant. Approved for merge.


Powered by: Copilot CLI (claude-opus-4.8) | Last updated: Mon, 10 Aug 2026 20:15:56 GMT

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds automatic repair of extension-managed Azure Functions Core Tools before blocking F5 debugging.

Changes:

  • Reinstalls and revalidates unrunnable managed Core Tools.
  • Records repair telemetry and preserves prompt fallback behavior.
  • Adds unit coverage for repair outcomes and validation opt-out.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
validateFuncCoreToolsInstalled.ts Adds managed Core Tools self-repair.
validateFuncCoreToolsInstalled.test.ts Tests repair and disabled-validation paths.

Copilot AI added 2 commits July 29, 2026 23:19
…silent

Addresses review feedback on the extension-managed Functions Core Tools
self-heal introduced in this PR.

Concurrent-install race:
`installFuncCoreToolsBinaries` had no in-flight guard, while
`downloadAndExtractDependency` stages into a shared temp folder and
`extractDependency` deletes and recreates the target folder. A failing
`func --version` is exactly what you observe while another install is
mid-extract, so the repair could start a second install on top of the
activation-time one (`validateAndInstallBinaries` ->
`validateFuncCoreToolsIsLatest`) and re-corrupt the tree.

`installFuncCoreToolsBinaries` now tracks the in-flight install: a
concurrent request for the same major version joins it, and a request
for a different version waits for it to finish first. The repair path
additionally checks `isFuncCoreToolsInstallInFlight()` and awaits the
running install before re-probing instead of starting its own.

Silent repair really is silent:
the repair no longer reveals the output channel, and
`downloadAndExtractDependency` accepts `suppressUi` so its three direct
`showErrorMessage` calls don't fire for an automatic repair that already
surfaces its own actionable prompt on failure. Output-channel logging
and telemetry are unchanged, and the two download/checksum failures now
log their message so suppressed failures stay diagnosable.

User-initiated installs keep their existing UI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
…-heal

Adds scenario p414-funcrepair (Phase 4.14), the first end-to-end proof that a
provisioned-but-unrunnable Azure Functions Core Tools binary is repaired by the
pre-debug gate instead of dead-ending on the blocking install modal.

Nothing exercised this path before: funcVersionRuns() in runtimeBinaryCheck.ts
is a harness precondition gate that waits for the self-heal rather than proving
it, and no test deliberately broke func and asserted recovery.

funcRepair.test.ts:
- Waits for the managed install and for two consecutive successful
  `func --version` probes. The second probe is the point: it proves
  activation-time validation has settled, so the repair observed later cannot
  be attributed to it.
- Backs up and overwrites every managed func executable in place with a short
  marker line, so each file still exists and keeps its .exe extension and
  execute bit but no longer runs -- the real poisoned runtime-dependency cache
  state, not a simulation. The marker is deliberately non-empty and has no
  leading '#': glibc execvp falls back to /bin/sh on ENOEXEC, so an empty or
  comment-only file would exit 0 on Linux and the repro would prove nothing.
  The test asserts `func --version` actually fails before continuing.
- Presses F5, which routes through pickFuncProcessInternal -> preDebugValidate
  -> validateFuncCoreToolsInstalled, the only caller of the gate.
- Asserts, hard: the blocking "You must have the Azure Functions Core Tools
  installed" prompt never appears; `func --version` runs again; and the
  repaired file no longer holds the marker bytes (catching a probe that
  resolved some other binary). A liveness assertion requires the workbench
  scrape to have returned text at least once, so the negative assertion cannot
  pass vacuously. The "no suppressed-by-design error toast" check is soft,
  matching waitForRepairNotification in bundleRepair.test.ts.

Writes are guarded by a containment check against the managed FuncCoreTools
directory, and afterEach restores the original bytes wherever the repair did
not already replace them, so a mid-test failure cannot poison the shared
runtime-dependency cache for sibling scenarios.

run-e2e.ts publishes the resolved dependency root as LA_E2E_RUNTIME_DEPS_ROOT,
the same mechanism lspeperm already uses, so the test targets what the harness
actually wrote instead of guessing a path and risking a developer's global func
install.

designerHelpers.ts exports and parameterizes the existing func path resolution
(getManagedFuncCoreToolsDir / getFuncCoreToolsCandidatePaths /
getFuncCoreToolsPath) instead of duplicating the layout. Default arguments keep
every existing call site behaviourally identical.

CI runs this on its own func-selfheal shard: it is the only scenario that
downloads the Func Core Tools package twice, so folding it into suite-and-bundle
risked pushing a retried shard past that job's 35 minute budget.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
@lambrianmsft

Copy link
Copy Markdown
Contributor Author

Added E2E coverage for the self-heal

Up to now this PR was proven only by unit tests. There was no end-to-end coverage of the behavior at all — funcVersionRuns() in runtimeBinaryCheck.ts is a harness precondition gate that quietly waits for the self-heal, so it consumes the fix rather than proving it, and nothing deliberately broke func and asserted recovery.

953fb1be4 adds scenario p414-funcrepair (Phase 4.14, apps/vs-code-designer/src/test/ui/funcRepair.test.ts).

What it does

  1. Waits for the managed install and for two consecutive successful func --version probes. The second probe is the point — it proves activation-time validation has settled, so the repair observed later can't be attributed to it.
  2. Backs up and overwrites every managed func executable in place with a short marker line. Each file still exists and keeps its .exe extension and execute bit, but no longer runs — the actual poisoned runtime-dependency cache state this PR exists to handle, not a mock.
  3. Presses F5, which routes through pickFuncProcessInternalpreDebugValidatevalidateFuncCoreToolsInstalled — the only caller of the gate.
  4. Asserts recovery.

Assertions

Kind Assertion
Hard The blocking "You must have the Azure Functions Core Tools installed" prompt never appears. The watch loop breaks early if it does, so a real regression fails in seconds rather than burning the timeout.
Hard (primary) func --version runs again — disk-level, modeled on waitForBundleRepaired.
Hard The repaired file no longer holds the marker bytes, catching a probe that resolved some other func.
Hard (liveness) The workbench scrape returned text at least once, so the negative assertion above can't pass vacuously.
Soft No Error downloading the / Checksum verification failed / could not be installed at toast — the user-visible half of { suppressUi: true }. Non-fatal for the same reason waitForRepairNotification in bundleRepair.test.ts is: toast timing is racy and the disk signal is authoritative.

Two details worth calling out

The corruption marker is deliberately non-empty and has no leading #. glibc's execvp falls back to /bin/sh on ENOEXEC, so an empty or comment-only file would exit 0 on Linux and the whole scenario would pass while proving nothing. The test asserts func --version actually fails after the write, so a platform that ever disagrees fails loudly instead of silently going green.

Writes are guarded by a containment check against the managed FuncCoreTools directory, and afterEach restores the original bytes wherever the repair didn't already replace them — a mid-test failure can't poison the shared runtime-dependency cache for sibling scenarios. run-e2e.ts now publishes the resolved dependency root as LA_E2E_RUNTIME_DEPS_ROOT (the same mechanism lspeperm already uses) so the test targets what the harness actually wrote rather than guessing a path.

What this does not cover: the concurrent-install guard from #discussion_r3680448234. Two installs racing isn't deterministically reproducible through the UI — forcing the interleaving would mean driving the internals directly, at which point it's a unit test wearing an E2E costume. The 5 tests in installRuntimeDependencies.test.ts remain the proof there.

CI runs this on its own func-selfheal shard rather than folding it into suite-and-bundle: it's the only scenario that downloads the Func Core Tools package twice, so with LA_E2E_SCENARIO_RETRIES=1 it risked pushing a retried shard past that job's 35-minute budget.

Copilot AI added 3 commits July 30, 2026 07:52
…e never runs

The first CI run of p414-funcrepair failed for a harness reason, not a product
one. The runner auto-opens a markdown preview, and it still held editor focus at
F5 time:

  [depValidation] VS Code title at start:
    "Preview WBD-hybrid announcement.md - testws_ms7l55v8 (Workspace) - ..."

startDebugging() -> focusEditor() focuses the active editor group, so
"Debug: Start Debugging" ran with a non-project preview active. VS Code never
resolved the Logic App folder's .vscode/launch.json, the pickProcess command was
never evaluated, and preDebugValidate never ran. startDebugging() does not throw
in that case, so the test sailed on and blamed the product for a repair it had
never actually asked for. The tell was that the repair AND the blocking modal
were both absent, and the whole job log contained no pickProcess or
preDebugValidate evidence at all.

Two fixes.

anchorDebugContextOnWorkflow() gives F5 a Logic App debug context: clear stray
editors (defaultContent -> closeAllEditors -> settle, SKILL.md rule 6), open the
project's own workflow.json, then poll BOTH the window title and the active tab
until each references it, and hard-fail otherwise. The assertion keys off the
same window title whose value diagnosed the bug, so a silent no-op cannot get
past it. Anchoring happens after the corruption so nothing can steal focus in
between. workflow.json has no customEditors contribution, so this is a plain
text editor and cannot reintroduce the webview-focus hazard it exists to fix.

This uses the existing Quick Open helper rather than VSBrowser.openResources,
which goes through `code -r` CLI IPC and is a silent no-op on headless Linux CI
(designerHelpers.ts:499) -- the same class of failure being fixed here.

waitForPreDebugGateEvidence() then proves F5 actually reached the product before
committing to the 300 s repair wait, so "the harness never triggered the
product" can no longer masquerade as "the product failed to repair". It watches
four independent signals, all watermarked or pre-checked so they cannot match
pre-F5 state: new output-channel lines from pickFuncProcessInternal's first two
steps (activateAzurite, refreshConnectionKeys) or the repair's own download
line; Azurite's blob port opening; the corruption marker disappearing; and the
blocking prompt appearing, which counts as reached on purpose so the existing
assertion reports it. On no evidence it fails immediately, names itself a
harness failure, and dumps the window title and output tail.

The debug toolbar, debug terminals and port 7071 are deliberately not used as
liveness signals: they only appear after preDebugValidate returns, i.e. on the
far side of a repair that can take minutes, so they cannot distinguish "not yet"
from "never".

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
The Phase 4.14 func self-heal scenario failed twice in CI because the debug
session was never actually started. Both failures were harness-side, not
product-side.

Run 30549543233: the active editor at F5 time was an auto-opened markdown
preview ("Preview WBD-hybrid announcement.md"), so VS Code never resolved the
Logic App folder's launch.json.

Run 30554041131: after closing all editors and re-opening workflow.json through
Quick Open three times, the active editor was still settings.json. In both cases
`startDebugging()` does not throw — it logs "Selecting: Debug: Start Debugging"
and returns — so the test fell through and blamed the product for a repair it
had never asked for.

The command-palette F5 path resolves whichever launch configuration the ACTIVE
EDITOR belongs to, and a headless CI session does not settle editor focus
reliably. Replace it with the existing codeful task recorder extension, which
calls vscode.debug.startDebugging(folder, configName) with an explicit folder:
no editor focus, no Quick Open index, no palette. The product path is unchanged
— that call runs the same resolution pipeline as F5, including the
${command:azureLogicAppsStandard.pickProcess} substitution that invokes
pickFuncProcessInternal -> preDebugValidate -> validateFuncCoreToolsInstalled.

- funcRepair.test.ts: drop anchorDebugContextOnWorkflow and the palette
  startDebugging call; add the recorder marker-file trigger plus a
  "debug start was never dispatched" fast-fail gated on the recorder's
  debugInvoke event. Gate on debugInvoke rather than debugStarted: for the
  codeless `attach` config, startDebugging only resolves after pickProcess
  finishes, which includes the repair being measured. Keep the existing
  gate-evidence check as a second-level harness/product discriminator, now
  also bailing on a late debugStartFailed. Stop the session via the recorder's
  stop-debug marker, with the palette path as fallback.
- codefulTaskRecorderExtension/main.js: pick the debug folder and configuration
  by what launch.json actually contains (pickProcess -> logicapp -> first)
  instead of assuming workspaceFolders[0], and log the choice. generateLaunchJson
  emits exactly one configuration and the codeful shapes have no processId, so
  Phase 4.10 selects the same config as before.
- run-e2e.ts: add a generic `recorder` scenario flag and opt p414-funcrepair in.
  Install once per scenario (prepareFreshSession only clears userDir) and reset
  the events/trigger pair per attempt so a retry never reads stale events.
- SKILL.md: rule 18 plus §16 wire-up notes recording the failure signatures.

Corruption, containment guard, restore logic and the assertion set are
unchanged — the CI logs confirmed all of them behaved correctly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
…heal E2E

The Phase 4.14 scenario reached the product correctly (the recorder logged
debugInvoke with the right folder and config) but still timed out waiting for
the repair. The screenshot artifact showed why: an unanswered QuickPick,
"Enable connectors in Azure for Logic App <name>", parked execution inside
refreshConnectionKeys (pickFuncProcess.ts:114) -- which runs BEFORE
preDebugValidate (:119). validateFuncCoreToolsInstalled was therefore never
called, so there was neither a repair nor the install modal. Azurite was
healthy throughout.

- Poll dismissConnectorsPromptIfVisible() on every iteration of both
  waitForPreDebugGateEvidence and watchForFuncRepair. The prompt appears
  asynchronously after the debug start, so a one-shot pre-dismissal would race.
  Escape is a safe answer: azureConnectorWizard.ts catches isUserCancelledError
  and resolves to 'no', the same as "Skip for now".
- Scope the dismissal by text so it cannot swallow the assertion under test.
  The func prompt is showWarningMessage({ modal: true }) -> .monaco-dialog-box,
  while the shared helper only queries .quick-input-widget; the wrapper also
  text-matches first, and both call sites refuse to run it once the blocking
  prompt has been seen. Non-matching QuickPicks are logged, not dismissed.
- Pin readOutputSinceWatermark to watermark.filePath. It previously re-ran the
  "newest log by mtime" search per call, so an unrelated file could become
  newest and its entire contents were reported as new output -- that is why the
  failure dump printed a node tarball listing and hid this stall for a full CI
  cycle. logOutputSinceWatermark now dumps the last 8 KB of the new region only.
- Track PAST_CONNECTION_KEYS_MARKERS so a timeout says whether the flow reached
  the gate at all, distinguishing a stall before the gate from a real repair
  failure.
- SKILL.md rules 19 and 20.

Test-only change; no product code touched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
The func self-heal had zero Windows signal: test.yml, pr-coverage.yml and
private-vsix-build.yml are all runs-on: ubuntu-latest, so neither the unit
tests nor the p414-funcrepair E2E ever ran there. That gap skews wrong for
this feature -- the real-world causes of an unrunnable managed func (AV
quarantine, partial extraction, an orphan func.exe holding a file lock) are
disproportionately Windows, and binaries.ts:224-243 reasons explicitly about
EPERM/EBUSY from a locked func.exe.

New blocking job vscode-e2e-funcselfheal-windows (windows-latest), wired into
the vscode-e2e-summary gate with no continue-on-error:

- Depends on setup-runtime-deps-windows. A cold la-runtime-deps-Windows-v1
  cache made the codeful Windows leg fail 100% of the time by hydrating
  underneath the running test, and this scenario downloads Func Core Tools
  twice, so it is the most cold-cache-sensitive job in the workflow.
- Creates fixtures on the runner via p41a-fixtures rather than consuming the
  workspace-fixtures artifact. That artifact is ubuntu-bound: the manifest
  stores absolute paths (workspaceManifest.ts:27-58) written under the ubuntu
  runner's RUNNER_TEMP, and nothing rebases them, so on Windows every entry
  points at a nonexistent Linux directory. No existing Windows job consumes it.
- Cache is restore-only. This job deliberately corrupts the managed func; a
  crashed session could otherwise save a poisoned tree under an exact-key hit
  that later runs can never repair.
- Adds a manifest guard step. funcRepair.test.ts calls this.skip() when there
  is no Standard/Stateful entry, which would report a passing job with zero
  coverage -- the likeliest bad outcome on a brand-new leg.
- timeout-minutes: 45, sized from setup-runtime-deps-windows's observed 11.3
  min max for p41a-fixtures plus ~12 min for the scenario, doubled for the
  LA_E2E_SCENARIO_RETRIES retry.

Harness fix (genuine bug, not wiring): prepareFreshSession()'s Windows branch
left the VS Code kill unguarded while Linux guards every pkill individually,
so a PowerShell hiccup or the 10s timeout threw past the func kill into the
outer catch, which only logs "kill failed - continuing". Invisible on Linux,
load-bearing here: p41a-fixtures runs with autoStartDesignTime: true and
leaves a live design-time func.exe, and p414-funcrepair then overwrites
func.exe in place. Each kill is now guarded, plus a bounded 15s
waitForWindowsFuncProcessesToExit() detection poll, since Stop-Process only
signals. Log-only, and it reports a failed probe distinctly from "all clear".

Also adds describeRunningFuncProcesses() to the overwrite-failure message
(Windows-only, failure-path only) so an EPERM names the process holding the
handle, and SKILL.md sections 18 (closing a dangling "rule 18" reference this
PR left in two files) and 19 (Windows CI porting rules).

Test-only change; no product code touched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
…agnosable

attemptManagedFuncCoreToolsRepair runs silently in the middle of F5 and, with
suppressUi on, wrote nothing anywhere. When it fails the user sees a debug
session that simply does nothing, and there is no trace to explain why.

The Windows E2E leg hit exactly that: the pre-debug gate was reached
(refreshConnectionKeys logged its skip) but the managed func was never repaired,
no install modal appeared, and the extension wrote zero further bytes to its
output channel. With no breadcrumbs it is impossible to tell whether the repair
ran and failed, or was never entered at all.

Add output-channel breadcrumbs at the repair's entry, the "an install is already
in flight, wait for it" branch, and both exits. These are the same lines a user
would need when reporting "F5 does nothing", so they earn their place
independently of the test.

Test-side, the failure path now also lists every "Azure Logic Apps" output log
with sizes and dumps the newest non-pinned one. The liveness gate pins a single
log file on purpose, but that pinning is blind to VS Code starting a new
output_logging_* folder, which would make "the extension wrote NOTHING" a false
statement. Running func processes are reported on the repair-timeout path too,
not just on the overwrite-failure path.

Unit tests mock ext.outputChannel; 14/14 pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
… stall F5

The pre-debug gate probes `func --version` through `tryExecuteCommand`, which only settles on
'close'/'error'. A managed func that hangs instead of exiting therefore stalls
`validateFuncCoreToolsInstalled` indefinitely: the self-heal never runs, no install modal appears,
nothing is logged, and F5 silently does nothing. The spawned process also keeps a handle on the
binary, so a later attempt to replace it fails with EBUSY.

That is exactly the state the Windows E2E leg reproduced. The gate was reached (the connector
refresh logged its completion immediately before it), the deliberately corrupted func was never
repaired, no modal appeared, the output channel went completely silent, and the retry could not
overwrite func.exe because it was still locked — which only something spawned from it could do,
since the debug task never started.

- Add `executeCommandWithTimeout` to cpUtils, which terminates a command that outlives its budget.
  Commands are spawned with `shell: true`, so the tracked pid is the shell; on Windows the whole
  tree is taken down with taskkill (matching startDesignTimeApi/funcHostTask) because killing only
  the shell leaves the real command running and still holding its file handles.
- Probe func with a 60s bound and treat a hang exactly like a failure, so the existing repair runs.
  The bound is generous so a genuinely slow first run (on-access AV scanning a freshly extracted
  binary) is not mistaken for a broken one.
- Log why the probe failed. It is the only signal distinguishing "func is broken" from "the gate
  never ran" when debug appears to do nothing.

Unit tests cover the Windows and non-Windows kill paths, the clean-exit path, non-zero exits, that
tryExecuteCommand stays unbounded, and that a timed-out probe still triggers the repair.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
@lambrianmsft

Copy link
Copy Markdown
Contributor Author

Windows E2E coverage — and the bug it found

Following up on the Linux-only coverage above: p414-funcrepair now also runs as its own blocking shard, vscode-e2e-funcselfheal (windows-latest). That mattered more than expected — it failed on its first run, and the failure was real, not harness flake.

The gate was reached (refreshConnectionKeys returned), but the corrupted func was never repaired, no install modal appeared, and the extension logged nothing at all for the full 300s budget. The decisive clue was the retry: its corruption step failed EBUSY because func.exe was still locked. The debug task never started, so the only thing that could have created that lock was the func --version probe itself — i.e. the probe spawned a process that never exited.

Root cause: tryExecuteCommand only settles on 'close'/'error', so a func that hangs instead of exiting stalls validateFuncCoreToolsInstalled forever. The self-heal never gets a chance to run, and F5 silently does nothing — strictly worse than the failure mode this PR set out to fix, and a mode that skews Windows-heavy (AV quarantine / partial extraction leaving a non-PE func.exe).

Fixed in 5ca5fddc9 by bounding the probe (executeCommandWithTimeout, killing the process tree on Windows since shell: true means the tracked pid is only the shell) and treating a hang exactly like a failure. Both platforms are green now:

Linux (func-selfheal) Windows (vscode-e2e-funcselfheal)
Result repaired=true, pastConnectionKeys=true repaired=true, pastConnectionKeys=true, attempt 1/2
Gate → repaired 29s 76s

The Windows timing is itself the confirmation: 76s ≈ the 60s bound firing plus the reinstall, versus 29s on Linux where the corrupt binary fails fast. pastConnectionKeys=true is what rules out a vacuous pass — it proves execution actually reached preDebugValidate rather than succeeding upstream.

Also added the log line that made this diagnosable at all. Its absence is why the first Windows failure was invisible in CI artifacts, and it is the same signal a user needs when debug "does nothing".

One scope note for reviewers: the concurrency guard from #discussion_r3680448234 is still proven by unit tests only — it is a same-process race that the E2E harness cannot deterministically induce.

Comment thread apps/vs-code-designer/src/app/utils/binaries.ts Outdated
@andrew-eldridge

Copy link
Copy Markdown
Contributor

One general note: I noticed in multiple places throughout vscode extension we have closely related/coupled functions, some of which could even be combined and simplified, spread across multiple different files which makes it harder to see the full picture. I think this applies to installFuncCoreTools.ts and validateFuncCoreToolsInstalled.ts here for example

Fold the managed Functions Core Tools repair into the binaries validation path, remove bespoke UI suppression plumbing from the installer/download helpers, and keep user-facing output/error prompts owned by explicit install branches.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
@lambrianmsft

Brian Lam (lambrianmsft) commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the simplification pass in f7738b8. I kept install mechanics in installFuncCoreTools.ts and validation policy in validateFuncCoreToolsInstalled.ts, but tightened the coupling points: repair now lives in the managed-binaries validation flow, install/download helpers no longer carry UI-suppression options, and user-facing prompts are branch-local where the selected action is known. That keeps the file ownership split while making the F5 path readable as one validation decision tree.

Copilot AI and others added 9 commits August 7, 2026 11:58
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Register extension commands before activation-time dependency validation and design-time startup so E2E setup and users cannot invoke a command before it exists. Add an explicit dependency-validation command gate in the fixture setup path so command-registration races fail with direct diagnostics instead of later webview visibility timeouts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Explicit runtime dependency validation now bypasses the per-session bundle health cache so a tampered extension bundle is re-read from disk and synchronously repaired. This keeps the startup/design-time cache fast while making the user-invoked validation command deterministic for bundle repair.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Invoke the product dependency validation command before the func self-heal test waits for managed FuncCoreTools, so the scenario does not depend on activation-time provisioning. Shorten the Mocha suite/test names to keep generated failure screenshot paths extractable on Windows.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Skip the explicit dependency validation command when the managed Func Core Tools binary is already runnable. CI preflight had already provisioned FuncCoreTools, and re-running validation could remove that folder during a failed revalidation before the test reached the self-heal scenario.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Resolved the run-e2e.ts conflict by keeping both the func self-heal standalone mode from this PR and the new nuget debug standalone mode from main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Main now generates azureFunctions.preDeployTask for converted NuGet projects. Update the new NuGet debug lifecycle E2E expectation to match the project-consistency generator while keeping this PR's func self-heal mode intact.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Comment thread apps/vs-code-designer/src/app/utils/funcCoreTools/cpUtils.ts Outdated
Copilot AI and others added 5 commits August 10, 2026 10:17
Address Andrew's latest PR comments by moving the Func Core Tools probe timeout and sanitized logging paths behind executeCommand options, preserving process-tree timeout safety while removing specialized helper variants.

Update the manual install fallback copy to recommend retrying before manual installation and adjust focused unit coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Add unit coverage for connectToSMB across Windows, macOS, and Linux mount commands, including credential-safe logging and upload flow coverage.

Strengthen cpUtils regression coverage for string-only callers, timeout options, option stripping, and the sanitized logging wrapper after the command helper simplification.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Keep converted NuGet Logic App settings aligned with the deployed extension contract by writing azureLogicAppsStandard.preDeployTask, while leaving codeful Function App projects on azureFunctions.* settings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
Ensure codeless Logic App settings generation matches the deployed azureLogicAppsStandard settings payload and order while preserving codeful azureFunctions settings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f6808401-494f-4b39-99a2-aa213d886ae0
@lambrianmsft
Brian Lam (lambrianmsft) merged commit 3095fb8 into Azure:main Aug 10, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-validated risk:medium Medium risk change with potential impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants