Skip to content

Commit 9764185

Browse files
author
Nathan Turinski
committed
Display stopped hosts as well
1 parent 5c09c3f commit 9764185

File tree

6 files changed

+176
-36
lines changed

6 files changed

+176
-36
lines changed

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,12 @@
508508
"title": "%azureFunctions.funcHostDebug.askCopilot%",
509509
"category": "Azure Functions",
510510
"icon": "$(sparkle)"
511+
},
512+
{
513+
"command": "azureFunctions.funcHostDebug.clearStoppedSessions",
514+
"title": "%azureFunctions.funcHostDebug.clearStoppedSessions%",
515+
"category": "Azure Functions",
516+
"icon": "$(trash)"
511517
}
512518
],
513519
"submenus": [
@@ -554,6 +560,11 @@
554560
"command": "azureFunctions.funcHostDebug.clearErrors",
555561
"when": "view == azureFunctions.funcHostDebugView",
556562
"group": "navigation@1"
563+
},
564+
{
565+
"command": "azureFunctions.funcHostDebug.clearStoppedSessions",
566+
"when": "view == azureFunctions.funcHostDebugView",
567+
"group": "navigation@2"
557568
}
558569
],
559570
"view/item/context": [
@@ -567,6 +578,16 @@
567578
"when": "view == azureFunctions.funcHostDebugView && viewItem == azFunc.funcHostDebug.hostTask",
568579
"group": "inline"
569580
},
581+
{
582+
"command": "azureFunctions.funcHostDebug.showRecentLogs",
583+
"when": "view == azureFunctions.funcHostDebugView && viewItem == azFunc.funcHostDebug.stoppedHostTask",
584+
"group": "inline"
585+
},
586+
{
587+
"command": "azureFunctions.funcHostDebug.copyRecentLogs",
588+
"when": "view == azureFunctions.funcHostDebugView && viewItem == azFunc.funcHostDebug.stoppedHostTask",
589+
"group": "inline"
590+
},
570591
{
571592
"command": "azureFunctions.funcHostDebug.askCopilot",
572593
"when": "view == azureFunctions.funcHostDebugView && viewItem == azFunc.funcHostDebug.hostError",

package.nls.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,11 @@
145145
"azureFunctions.mcpProjectType.NoMcpServer": "Runs the standard Azure Functions runtime with no MCP integration.",
146146
"azureFunctions.mcpProjectType.McpExtensionServer": "Runs the Functions host with an embedded MCP server provided by the Azure Functions MCP extension.",
147147
"azureFunctions.mcpProjectType.SelfHostedMcpServer": "Runs the Functions host in custom-handler mode, forwarding requests to a self-hosted MCP server process.",
148-
"azureFunctions.funcHostDebugView.title": "Function Host Debug",
148+
"azureFunctions.funcHostDebugView.title": "Functions Host Exceptions Debugger",
149149
"azureFunctions.funcHostDebug.refresh": "Refresh",
150150
"azureFunctions.funcHostDebug.clearErrors": "Clear Function Host Errors",
151151
"azureFunctions.funcHostDebug.showRecentLogs": "Show Recent Host Logs",
152152
"azureFunctions.funcHostDebug.copyRecentLogs": "Copy Recent Host Logs",
153-
"azureFunctions.funcHostDebug.askCopilot": "Ask Copilot"
153+
"azureFunctions.funcHostDebug.askCopilot": "Ask Copilot",
154+
"azureFunctions.funcHostDebug.clearStoppedSessions": "Clear Stopped Sessions"
154155
}

src/debug/FunctionHostDebugView.ts

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { runningFuncTaskMap } from '../funcCoreTools/funcHostTask';
7+
import { runningFuncTaskMap, stoppedFuncTasks, type IStoppedFuncTask } from '../funcCoreTools/funcHostTask';
88
import { localize } from '../localize';
99

1010
enum FuncHostDebugContextValue {
1111
HostTask = 'azFunc.funcHostDebug.hostTask',
12+
StoppedHostTask = 'azFunc.funcHostDebug.stoppedHostTask',
1213
HostError = 'azFunc.funcHostDebug.hostError',
1314
}
1415

15-
type FuncHostDebugNode = INoHostNode | IHostTaskNode | IHostErrorNode;
16+
type FuncHostDebugNode = INoHostNode | IHostTaskNode | IStoppedHostNode | IHostErrorNode;
1617

1718
interface INoHostNode {
1819
kind: 'noHost';
@@ -23,6 +24,12 @@ export interface IHostTaskNode {
2324
workspaceFolder: vscode.WorkspaceFolder | vscode.TaskScope;
2425
cwd?: string;
2526
portNumber: string;
27+
startTime: Date;
28+
}
29+
30+
export interface IStoppedHostNode {
31+
kind: 'stoppedHost';
32+
stoppedTask: IStoppedFuncTask;
2633
}
2734

2835
export interface IHostErrorNode {
@@ -33,6 +40,28 @@ export interface IHostErrorNode {
3340
message: string;
3441
}
3542

43+
function formatTimestamp(date: Date): string {
44+
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
45+
}
46+
47+
function buildHostTooltip(opts: { label: string; scopeLabel: string; portNumber: string; startTime: Date; stopTime?: Date; cwd?: string; pid?: number }): vscode.MarkdownString {
48+
const tooltip = new vscode.MarkdownString(undefined, true);
49+
tooltip.appendMarkdown(`**${opts.label}**\n\n`);
50+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.workspace', 'Workspace')}: ${opts.scopeLabel}\n`);
51+
if (opts.pid !== undefined) {
52+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.pid', 'PID')}: ${opts.pid}\n`);
53+
}
54+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.port', 'Port')}: ${opts.portNumber}\n`);
55+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.started', 'Started')}: ${opts.startTime.toLocaleString()}\n`);
56+
if (opts.stopTime) {
57+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.stopped', 'Stopped')}: ${opts.stopTime.toLocaleString()}\n`);
58+
}
59+
if (opts.cwd) {
60+
tooltip.appendMarkdown(`- ${localize('funcHostDebug.cwd', 'CWD')}: ${opts.cwd}\n`);
61+
}
62+
return tooltip;
63+
}
64+
3665
function getNoHostTreeItem(): vscode.TreeItem {
3766
const item = new vscode.TreeItem(localize('funcHostDebug.noneRunning', 'No Function Host task is currently running.'), vscode.TreeItemCollapsibleState.None);
3867
item.description = localize('funcHostDebug.startDebuggingHint', 'Start debugging (F5) to launch the host.');
@@ -59,23 +88,35 @@ function getHostTaskTreeItem(element: IHostTaskNode): vscode.TreeItem {
5988

6089
const label = localize('funcHostDebug.hostLabel', 'Function Host ({0})', element.portNumber);
6190

62-
const tooltip = new vscode.MarkdownString(undefined, true);
63-
tooltip.appendMarkdown(`**${label}**\n\n`);
64-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.workspace', 'Workspace')}: ${scopeLabel}\n`);
65-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.pid', 'PID')}: ${task?.processId ?? localize('funcHostDebug.unknown', 'Unknown')}\n`);
66-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.port', 'Port')}: ${element.portNumber}\n`);
67-
if (element.cwd) {
68-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.cwd', 'CWD')}: ${element.cwd}\n`);
69-
}
91+
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: element.portNumber, startTime: element.startTime, cwd: element.cwd, pid: task?.processId });
7092

7193
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.Expanded);
72-
item.description = scopeLabel;
94+
item.description = `${scopeLabel} - ${formatTimestamp(element.startTime)}`;
7395
item.tooltip = tooltip;
7496
item.contextValue = FuncHostDebugContextValue.HostTask;
7597
item.iconPath = new vscode.ThemeIcon('server-process');
7698
return item;
7799
}
78100

101+
function getStoppedHostTreeItem(element: IStoppedHostNode): vscode.TreeItem {
102+
const stopped = element.stoppedTask;
103+
const scopeLabel = typeof stopped.workspaceFolder === 'object'
104+
? stopped.workspaceFolder.name
105+
: localize('funcHostDebug.globalScope', 'Global');
106+
107+
const label = localize('funcHostDebug.stoppedHostLabel', 'Function Host ({0}) — Stopped', stopped.portNumber);
108+
109+
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: stopped.portNumber, startTime: stopped.startTime, stopTime: stopped.stopTime, cwd: stopped.cwd });
110+
111+
const errorCount = stopped.errorLogs.length;
112+
const item = new vscode.TreeItem(label, errorCount > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None);
113+
item.description = `${scopeLabel} - ${formatTimestamp(stopped.startTime)}${formatTimestamp(stopped.stopTime)}`;
114+
item.tooltip = tooltip;
115+
item.contextValue = FuncHostDebugContextValue.StoppedHostTask;
116+
item.iconPath = new vscode.ThemeIcon('debug-stop', new vscode.ThemeColor('disabledForeground'));
117+
return item;
118+
}
119+
79120
export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHostDebugNode> {
80121
private readonly _onDidChangeTreeDataEmitter = new vscode.EventEmitter<FuncHostDebugNode | undefined>();
81122
public readonly onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
@@ -92,6 +133,8 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
92133
return getHostErrorTreeItem(element);
93134
case 'hostTask':
94135
return getHostTaskTreeItem(element);
136+
case 'stoppedHost':
137+
return getStoppedHostTreeItem(element);
95138
default: {
96139
// Exhaustive check: if we reach here, the FuncHostDebugNode union is out of sync with this switch.
97140
throw new Error(`Unexpected FuncHostDebugNode kind: ${(element as { kind?: unknown }).kind}`);
@@ -103,7 +146,6 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
103146
if (element?.kind === 'hostTask') {
104147
const task = runningFuncTaskMap.get(element.workspaceFolder, element.cwd);
105148
const errors = task?.errorLogs ?? [];
106-
// Show most recent errors first.
107149
return errors
108150
.slice()
109151
.reverse()
@@ -114,26 +156,47 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
114156
portNumber: element.portNumber,
115157
message,
116158
}));
159+
} else if (element?.kind === 'stoppedHost') {
160+
const stopped = element.stoppedTask;
161+
return stopped.errorLogs
162+
.slice()
163+
.reverse()
164+
.map((message): IHostErrorNode => ({
165+
kind: 'hostError',
166+
workspaceFolder: stopped.workspaceFolder,
167+
cwd: stopped.cwd,
168+
portNumber: stopped.portNumber,
169+
message,
170+
}));
117171
} else if (element) {
118172
return [];
119173
}
120174

121-
const hostTasks: IHostTaskNode[] = [];
175+
const nodes: FuncHostDebugNode[] = [];
176+
let hasRunning = false;
122177

178+
// Running sessions first (newest on top by insertion order).
123179
for (const folder of vscode.workspace.workspaceFolders ?? []) {
124180
for (const t of runningFuncTaskMap.getAll(folder)) {
125181
if (!t) {
126182
continue;
127183
}
128184
const cwd = (t.taskExecution.task.execution as vscode.ShellExecution | undefined)?.options?.cwd;
129-
hostTasks.push({ kind: 'hostTask', workspaceFolder: folder, cwd, portNumber: t.portNumber });
185+
nodes.push({ kind: 'hostTask', workspaceFolder: folder, cwd, portNumber: t.portNumber, startTime: t.startTime });
186+
hasRunning = true;
130187
}
131188
}
132189

133-
if (hostTasks.length === 0) {
134-
return [{ kind: 'noHost' }];
190+
// Always show the hint node when no host is actively running.
191+
if (!hasRunning) {
192+
nodes.push({ kind: 'noHost' });
193+
}
194+
195+
// Stopped sessions (already newest-first in the array).
196+
for (const stopped of stoppedFuncTasks) {
197+
nodes.push({ kind: 'stoppedHost', stoppedTask: stopped });
135198
}
136199

137-
return hostTasks;
200+
return nodes;
138201
}
139202
}

src/debug/registerFunctionHostDebugView.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
import { registerCommand, type IActionContext } from '@microsoft/vscode-azext-utils';
77
import * as vscode from 'vscode';
88
import { getRecentLogsPlainText } from '../funcCoreTools/funcHostErrorUtils';
9-
import { onRunningFuncTasksChanged, runningFuncTaskMap, type IRunningFuncTask } from '../funcCoreTools/funcHostTask';
9+
import { clearStoppedSessions, onRunningFuncTasksChanged, runningFuncTaskMap, stoppedFuncTasks, type IRunningFuncTask } from '../funcCoreTools/funcHostTask';
1010
import { localize } from '../localize';
1111
import { stripAnsiControlCharacters } from '../utils/ansiUtils';
12-
import { FuncHostDebugViewProvider, type IHostErrorNode, type IHostTaskNode } from './FunctionHostDebugView';
12+
import { FuncHostDebugViewProvider, type IHostErrorNode, type IHostTaskNode, type IStoppedHostNode } from './FunctionHostDebugView';
1313

1414
const viewId = 'azureFunctions.funcHostDebugView';
1515

1616
function isHostTaskNode(node: unknown): node is IHostTaskNode {
1717
return !!node && typeof node === 'object' && (node as IHostTaskNode).kind === 'hostTask';
1818
}
1919

20+
function isStoppedHostNode(node: unknown): node is IStoppedHostNode {
21+
return !!node && typeof node === 'object' && (node as IStoppedHostNode).kind === 'stoppedHost';
22+
}
23+
2024
function isHostErrorNode(node: unknown): node is IHostErrorNode {
2125
return !!node && typeof node === 'object' && (node as IHostErrorNode).kind === 'hostError';
2226
}
@@ -77,29 +81,43 @@ export function registerFunctionHostDebugView(context: vscode.ExtensionContext):
7781
}
7882
}
7983

84+
// Also clear errors from stopped sessions.
85+
for (const s of stoppedFuncTasks) {
86+
s.errorLogs = [];
87+
}
88+
8089
provider.refresh();
8190
});
8291

92+
registerCommand('azureFunctions.funcHostDebug.clearStoppedSessions', async (actionContext: IActionContext) => {
93+
actionContext.telemetry.properties.source = 'funcHostDebugView';
94+
clearStoppedSessions();
95+
});
96+
8397
registerCommand('azureFunctions.funcHostDebug.copyRecentLogs', async (actionContext: IActionContext, args: unknown) => {
8498
actionContext.telemetry.properties.source = 'funcHostDebugView';
85-
if (!isHostTaskNode(args)) {
86-
return;
99+
if (isHostTaskNode(args)) {
100+
const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
101+
const text = getRecentLogsPlainText(task);
102+
await vscode.env.clipboard.writeText(text);
103+
} else if (isStoppedHostNode(args)) {
104+
const text = getRecentLogsPlainText(args.stoppedTask);
105+
await vscode.env.clipboard.writeText(text);
87106
}
88-
89-
const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
90-
const text = getRecentLogsPlainText(task);
91-
await vscode.env.clipboard.writeText(text);
92107
});
93108

94109
registerCommand('azureFunctions.funcHostDebug.showRecentLogs', async (actionContext: IActionContext, args: unknown) => {
95110
actionContext.telemetry.properties.source = 'funcHostDebugView';
96-
if (!isHostTaskNode(args)) {
111+
let text: string | undefined;
112+
if (isHostTaskNode(args)) {
113+
const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
114+
text = getRecentLogsPlainText(task);
115+
} else if (isStoppedHostNode(args)) {
116+
text = getRecentLogsPlainText(args.stoppedTask);
117+
} else {
97118
return;
98119
}
99120

100-
const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
101-
const text = getRecentLogsPlainText(task);
102-
103121
const doc = await vscode.workspace.openTextDocument({
104122
content: text || localize('funcHostDebug.noLogs', 'No logs captured yet.'),
105123
language: 'log',

src/funcCoreTools/funcHostErrorUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { stripAnsiControlCharacters } from '../utils/ansiUtils';
7-
import { type IRunningFuncTask } from './funcHostTask';
7+
import { type IRunningFuncTask, type IStoppedFuncTask } from './funcHostTask';
88

9-
export function getRecentLogs(task: IRunningFuncTask | undefined, limit: number = 250): string {
9+
export function getRecentLogs(task: IRunningFuncTask | IStoppedFuncTask | undefined, limit: number = 250): string {
1010
const logs = task?.logs ?? [];
1111
const recent = logs.slice(Math.max(0, logs.length - limit));
1212
return recent.join('');
1313
}
1414

15-
export function getRecentLogsPlainText(task: IRunningFuncTask | undefined, limit: number = 250): string {
15+
export function getRecentLogsPlainText(task: IRunningFuncTask | IStoppedFuncTask | undefined, limit: number = 250): string {
1616
return stripAnsiControlCharacters(getRecentLogs(task, limit));
1717
}
1818

0 commit comments

Comments
 (0)