fix(vscode): self-heal a provisioned-but-unrunnable func Core Tools before blocking F5 - #9478
Conversation
…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
🤖 AI PR Validation ReportPR Review ResultsThank you for your submission! Here's detailed feedback on your PR title and body compliance:✅ PR Title
✅ Commit Type
✅ Risk Level
✅ What & Why
✅ Impact of Change
✅ Test Plan
|
| 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
There was a problem hiding this comment.
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. |
…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
Added E2E coverage for the self-healUp to now this PR was proven only by unit tests. There was no end-to-end coverage of the behavior at all —
What it does
Assertions
Two details worth calling out The corruption marker is deliberately non-empty and has no leading Writes are guarded by a containment check against the managed 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 CI runs this on its own |
…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
Windows E2E coverage — and the bug it foundFollowing up on the Linux-only coverage above: The gate was reached ( Root cause: Fixed in
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. 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. |
|
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
|
Addressed the simplification pass in |
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
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
Commit Type
Risk Level
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.exeexists on disk" from "func.exeactually runs".preDebugValidate->validateFuncCoreToolsInstalled->isFuncToolsInstalled()spawnsfunc --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: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
validateFuncCoreToolsInstallednow keeps one branch per install path:azureLogicAppsStandard.validateFuncCoreToolsdisabled -> installed,func-> installed,validateFuncCoreToolsInstalledBinaries(...),PATHinstall ->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-verifiesfunc --version, and only falls back to the interactive modal when the repair still cannot produce a runnablefunc.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:
azureLogicAppsStandard.validateFuncCoreToolsis enabled, andisFuncToolsInstalled()already returned false, anduseBinariesDependencies()is true — the extension owns these binaries.System/
PATHinstalls 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 withcontext.errorHandling.rethrow = trueandcontext.errorHandling.suppressDisplay = true.That removed the bespoke
{ suppressUi: true }path frominstallFuncCoreTools.tsandbinaries.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.downloadAndExtractDependencyalso 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 existingsuppressDisplay=truescopes, while user-initiated func installs still get the manual-install prompt if the install fails.3. The repair must not race a concurrent install
installFuncCoreToolsBinarieshad no in-flight guard, andextractDependencydeletes and recreates the shared target directory. A failingfunc --versionis 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.tsnow tracks the active install (isFuncCoreToolsInstallInFlight()/waitForFuncCoreToolsInstall(), mirroring the existinginFlightBundleWorkprecedent inbundleFeed.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
funcmust not stall F5 either (found by the Windows E2E leg)tryExecuteCommandonly settles on'close'/'error'. A managedfuncthat hangs instead of exiting therefore stalledvalidateFuncCoreToolsInstalledindefinitely: 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 withEBUSY.This is not hypothetical: it is what the new Windows E2E leg hit on its first run. The gate was reached, the deliberately corrupted
funcwas never repaired, and the retry could not overwritefunc.exebecause it was still locked — which only something spawned from it could do, since the debug task never started.New
executeCommandWithTimeoutincpUtilsterminates a command that outlives its budget. Commands are spawned withshell: true, so the tracked pid is the shell; on Windows the whole tree is taken down withtaskkill(matchingstartDesignTimeApi/funcHostTask), because killing only the shell leaves the real command running and still holding its file handles. Thefunc --versionprobe 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, andfuncRepairErroron the nested repair telemetry scope.Impact of Change
funcrepairs 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.installFuncCoreToolsBinariesanddownloadAndExtractDependencyno longer expose bespoke UI-suppression options; callers own UI surfacing through telemetry/error-handling scopes and branch-local prompts.installFuncCoreToolsBinariescall, only on the managed-binaries path and only whenfunc --versionhas already failed. No new dependencies..github/workflows/vscode-e2e.ymlgains the two self-heal shards described below.Test Plan
validateFuncCoreToolsInstalled(repair succeeds -> returnstruewithout prompting; repair runs butfuncstill 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; thevalidateFuncCoreTools-disabled opt-out; nested repair telemetry setsrethrow=trueandsuppressDisplay=true; silent repair does not reveal the Output panel; user-selected install does reveal it and still prompts for manual install on failure).cpUtilscovers Windowstaskkilltree-kill and non-WindowsSIGKILL, clean exits, non-zero exits, and thattryExecuteCommandstays unbounded.installRuntimeDependenciesandbinariescover install coalescing/serialization and the new caller-owned UI behavior.p414-funcrepair(funcRepair.test.ts): corrupts the managed func binary (containment-guarded, only ever writing under the resolvedLA_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) andvscode-e2e-funcselfheal (windows-latest). A CI manifest guard fails the job rather than letting the testthis.skip()into a vacuous pass.pnpm exec biome check --write <changed files>; focused unit run (4 files / 130 tests passed); fullapps/vs-code-designerunit suite (127 files / 1563 tests passed); E2E TypeScript compile (npx tsup --config tsup.e2e.test.config.ts). A repo-localtsc --noEmit -p apps/vs-code-designer/tsconfig.jsonrun is not a clean validation signal in this worktree because it reports pre-existing unrelated extension typing errors outside the touched files.E2E evidence
func-selfheal)vscode-e2e-funcselfheal)repaired=true, pastConnectionKeys=truerepaired=true, pastConnectionKeys=true, attempt 1/2pastConnectionKeys=trueis what proves the test is not passing vacuously: it confirms execution actually reachedpreDebugValidate(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.