From b1f5288641311754006f27704e0db7fc482cba85 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Mon, 26 Feb 2024 11:13:51 -0800 Subject: [PATCH 1/3] URI Handler --- src/ProvenanceDatabase.ts | 8 +++++ src/codeLensProvider.ts | 4 +-- src/commands.ts | 74 ++++++++++++++++++++++++++------------- src/configuration.ts | 22 ++++++------ src/extension.ts | 10 ++++-- src/uriHandler.ts | 22 ++++++++++++ src/utils.ts | 12 +++++++ 7 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 src/uriHandler.ts diff --git a/src/ProvenanceDatabase.ts b/src/ProvenanceDatabase.ts index d106180..836dbaa 100644 --- a/src/ProvenanceDatabase.ts +++ b/src/ProvenanceDatabase.ts @@ -15,6 +15,8 @@ export interface workflow_status { authenticated_roles: string; // Serialized list of roles. request: string; // Serialized HTTPRequest executor_id: string; // Set to "local" for local deployment, set to microVM ID for cloud deployment. + created_at: string; + updated_at: string; } export class ProvenanceDatabase { @@ -44,4 +46,10 @@ export class ProvenanceDatabase { const results = await db.query('SELECT * FROM dbos.workflow_status WHERE name = $1 LIMIT 10', [wfName]); return results.rows; } + + async getWorkflowStatus(clientConfig: ClientConfig, wfid: string): Promise { + const db = await this.connect(clientConfig); + const results = await db.query('SELECT * FROM dbos.workflow_status WHERE workflow_uuid = $1 LIMIT 10', [wfid]); + return results.rows; + } } diff --git a/src/codeLensProvider.ts b/src/codeLensProvider.ts index c3d964a..0b772b3 100644 --- a/src/codeLensProvider.ts +++ b/src/codeLensProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import ts from 'typescript'; -import { startDebuggingCommandName } from './commands'; +import { startDebuggingCodeLensCommandName } from './commands'; import { logger } from './extension'; import { getDbosMethodType, parse } from './sourceParser'; @@ -32,7 +32,7 @@ export class TTDbgCodeLensProvider implements vscode.CodeLensProvider { return new vscode.CodeLens(range, { title: '⏳ Time Travel Debug', tooltip: `Debug ${methodInfo.name} with the DBOS Time Travel Debugger`, - command: startDebuggingCommandName, + command: startDebuggingCodeLensCommandName, arguments: [folder, methodInfo.name, methodType] }); }) diff --git a/src/commands.ts b/src/commands.ts index c4b44c5..942efb3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,52 +1,78 @@ import * as vscode from 'vscode'; import { logger, config, provDB, debugProxy } from './extension'; import { DbosMethodType } from "./sourceParser"; -import { stringify } from './utils'; +import { getWorkspaceFolder, stringify } from './utils'; import { dbos_cloud_login } from './configuration'; +import { ClientConfig } from 'pg'; export const cloudLoginCommandName = "dbos-ttdbg.cloud-login"; -export const startDebuggingCommandName = "dbos-ttdbg.startDebugging"; +export const startDebuggingCodeLensCommandName = "dbos-ttdbg.start-debugging-code-lens"; +export const startDebuggingUriCommandName = "dbos-ttdbg.start-debugging-uri"; export const shutdownDebugProxyCommandName = "dbos-ttdbg.shutdown-debug-proxy"; export const deleteProvDbPasswordsCommandName = "dbos-ttdbg.delete-prov-db-passwords"; -export async function startDebugging(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) { +async function startDebugging(folder: vscode.WorkspaceFolder, clientConfig: ClientConfig, workflowID: string) { try { - const clientConfig = await config.getProvDbConfig(folder); - if (!clientConfig) { return; } - - const statuses = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Window }, - () => { return provDB.getWorkflowStatuses(clientConfig, name, $type); }); - + logger.info(`startDebugging`, { folder: folder.uri.fsPath, database: clientConfig.database, workflowID }); await debugProxy.launch(clientConfig); - // TODO: eventually, we'll need a better UI than "list all workflow IDs and let the user pick one" - const wfID = await vscode.window.showQuickPick(statuses.map(s => s.workflow_uuid), { - placeHolder: `Select a ${name} workflow ID to debug`, - canPickMany: false, - }); - - if (!wfID) { return; } - - logger.info(`Starting debugging for ${name} workflow ${wfID}`); - const proxyURL = `http://localhost:${config.proxyPort ?? 2345}`; await vscode.debug.startDebugging( vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor!.document.uri), { - name: `Debug ${wfID}`, + name: `Debug ${workflowID}`, type: 'node-terminal', request: 'launch', - command: `npx dbos-sdk debug -x ${proxyURL} -u ${wfID}` + command: `npx dbos-sdk debug -x ${proxyURL} -u ${workflowID}` } ); } catch (e) { - const reason = stringify(e); logger.error("startDebugging", e); - vscode.window.showErrorMessage(`Failed to start debugging\n${reason}`); + vscode.window.showErrorMessage(`Failed to start debugging\n${stringify(e)}`); } } +export async function startDebuggingFromCodeLens(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) { + const clientConfig = await config.getProvDbConfig(folder); + if (!clientConfig) { return; } + + logger.info(`startDebuggingFromCodeLens`, { folder: folder.uri.fsPath, database: clientConfig.database, name, type: $type }); + + const statuses = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Window }, + () => { return provDB.getWorkflowStatuses(clientConfig, name, $type); }); + + // TODO: eventually, we'll need a better UI than "list all workflow IDs and let the user pick one" + const wfid = await vscode.window.showQuickPick(statuses.map(s => s.workflow_uuid), { + placeHolder: `Select a ${name} workflow ID to debug`, + canPickMany: false, + }); + + if (!wfid) { return; } + + await startDebugging(folder, clientConfig, wfid); + +} +export async function startDebuggingFromUri(wfid: string) { + const folder = await getWorkspaceFolder(); + if (!folder) { return; } + const clientConfig = await config.getProvDbConfig(folder); + if (!clientConfig) { return; } + + logger.info(`startDebuggingFromUri`, { folder: folder.uri.fsPath, database: clientConfig.database, wfid }); + + const validStatus = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Window }, + () => { return provDB.getWorkflowStatus(clientConfig, wfid); }); + + if (validStatus.length === 0) { + vscode.window.showErrorMessage(`Workflow ID ${wfid} not found in provenance database`); + return; + } + + await startDebugging(folder, clientConfig, wfid); +} + export function shutdownDebugProxy() { try { debugProxy.shutdown(); diff --git a/src/configuration.ts b/src/configuration.ts index a7fe1a1..1e54ca7 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -204,21 +204,19 @@ export class Configuration { return { host, port, database, user }; }); - if (!dbConfig.host || !dbConfig.database || !dbConfig.user) { + if (dbConfig.host && dbConfig.database && dbConfig.user) { + return { + host: dbConfig.host, + port: dbConfig.port, + database: dbConfig.database, + user: dbConfig.user, + password: () => this.#getPassword(folder), + ssl: { rejectUnauthorized: false } + }; + } else { startInvalidCredentialsFlow(folder).catch(e => logger.error("startInvalidCredentialsFlow", e)); return undefined; } - - return { - host: dbConfig.host, - port: dbConfig.port, - database: dbConfig.database, - user: dbConfig.user, - password: () => this.#getPassword(folder), - ssl: { - rejectUnauthorized: false, - } - }; } get proxyPort(): number { diff --git a/src/extension.ts b/src/extension.ts index b61977f..68c7916 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode'; import { S3CloudStorage } from './CloudStorage'; import { TTDbgCodeLensProvider } from './codeLensProvider'; -import { deleteProvenanceDatabasePasswords, deleteProvDbPasswordsCommandName, startDebuggingCommandName, startDebugging, shutdownDebugProxyCommandName, shutdownDebugProxy, cloudLoginCommandName, cloudLogin } from './commands'; +import { deleteProvenanceDatabasePasswords, deleteProvDbPasswordsCommandName, shutdownDebugProxyCommandName, shutdownDebugProxy, cloudLoginCommandName, cloudLogin, startDebuggingCodeLensCommandName, startDebuggingFromCodeLens, startDebuggingFromUri, startDebuggingUriCommandName } from './commands'; import { Configuration } from './configuration'; import { DebugProxy, } from './DebugProxy'; import { LogOutputChannelTransport, Logger, createLogger } from './logger'; import { ProvenanceDatabase } from './ProvenanceDatabase'; +import { TTDbgUriHandler } from './uriHandler'; export let logger: Logger; export let config: Configuration; @@ -30,7 +31,9 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(debugProxy); context.subscriptions.push( - vscode.commands.registerCommand(startDebuggingCommandName, startDebugging)); + vscode.commands.registerCommand(startDebuggingCodeLensCommandName, startDebuggingFromCodeLens)); + context.subscriptions.push( + vscode.commands.registerCommand(startDebuggingUriCommandName, startDebuggingFromUri)); context.subscriptions.push( vscode.commands.registerCommand(shutdownDebugProxyCommandName, shutdownDebugProxy)); context.subscriptions.push( @@ -43,6 +46,9 @@ export async function activate(context: vscode.ExtensionContext) { { scheme: 'file', language: 'typescript' }, new TTDbgCodeLensProvider())); + context.subscriptions.push( + vscode.window.registerUriHandler(new TTDbgUriHandler())); + await debugProxy.update().catch(e => { logger.error("Debug Proxy Update Failed", e); vscode.window.showErrorMessage(`Debug Proxy Update Failed`); diff --git a/src/uriHandler.ts b/src/uriHandler.ts new file mode 100644 index 0000000..3275c1e --- /dev/null +++ b/src/uriHandler.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; +import { logger } from './extension'; +import { startDebuggingUriCommandName } from './commands'; + +export class TTDbgUriHandler implements vscode.UriHandler { + async handleUri(uri: vscode.Uri): Promise { + logger.debug(`TTDbgUriHandler.handleUri`, { uri: uri.toString() }); + switch (uri.path) { + case '/start-debugging': + const searchParams = new URLSearchParams(uri.query); + const wfID = searchParams.get('wfid'); + if (wfID) { + vscode.commands.executeCommand(startDebuggingUriCommandName, wfID); + } else { + vscode.window.showErrorMessage(`Invalid start-debugging uri: ${uri}`); + } + break; + default: + vscode.window.showErrorMessage(`Unsupported uri: ${uri.path}}`); + } + } +} diff --git a/src/utils.ts b/src/utils.ts index fcbc10c..3f2d3e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,3 +49,15 @@ export function hashClientConfig(clientConfig: ClientConfig) { : undefined; } +export async function getWorkspaceFolder() { + const folders = vscode.workspace.workspaceFolders ?? []; + if (folders.length === 1) { return folders[0]; } + + if (vscode.window.activeTextEditor) { + const folder = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri); + if (folder) { + return folder; + } + } + return await vscode.window.showWorkspaceFolderPick(); +} From 5137d43909b111e39706e89443440868abdc236d Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Mon, 26 Feb 2024 14:34:02 -0800 Subject: [PATCH 2/3] use withProgress to notify user things are happening --- src/DebugProxy.ts | 5 +++ src/commands.ts | 99 +++++++++++++++++++++++++---------------------- src/uriHandler.ts | 19 +++++++-- 3 files changed, 72 insertions(+), 51 deletions(-) diff --git a/src/DebugProxy.ts b/src/DebugProxy.ts index c7a84f6..a79ddda 100644 --- a/src/DebugProxy.ts +++ b/src/DebugProxy.ts @@ -121,6 +121,11 @@ export class DebugProxy { this._outChannel.error(e, { database }); }); + proxyProcess.on('close', (code, signal) => { + this._proxyProcesses.delete(configHash); + this._outChannel.info(`Debug Proxy closed with exit code ${code}`, { database }); + }); + proxyProcess.on("exit", (code, _signal) => { this._proxyProcesses.delete(configHash); this._outChannel.info(`Debug Proxy exited with exit code ${code}`, { database }); diff --git a/src/commands.ts b/src/commands.ts index 942efb3..f06f0d8 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,66 +11,71 @@ export const startDebuggingUriCommandName = "dbos-ttdbg.start-debugging-uri"; export const shutdownDebugProxyCommandName = "dbos-ttdbg.shutdown-debug-proxy"; export const deleteProvDbPasswordsCommandName = "dbos-ttdbg.delete-prov-db-passwords"; -async function startDebugging(folder: vscode.WorkspaceFolder, clientConfig: ClientConfig, workflowID: string) { +async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (clientConfig: ClientConfig) => Promise) { try { - logger.info(`startDebugging`, { folder: folder.uri.fsPath, database: clientConfig.database, workflowID }); - await debugProxy.launch(clientConfig); - - const proxyURL = `http://localhost:${config.proxyPort ?? 2345}`; - await vscode.debug.startDebugging( - vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor!.document.uri), - { - name: `Debug ${workflowID}`, - type: 'node-terminal', - request: 'launch', - command: `npx dbos-sdk debug -x ${proxyURL} -u ${workflowID}` + const clientConfig = await config.getProvDbConfig(folder); + if (!clientConfig) { return; } + + const debuggerStarted = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: "Launching DBOS Time Travel Debugger", + }, + async () => { + await debugProxy.launch(clientConfig); + const workflowID = await getWorkflowID(clientConfig); + if (!workflowID) { return; } + + const proxyURL = `http://localhost:${config.proxyPort ?? 2345}`; + logger.info(`startDebugging`, { folder: folder.uri.fsPath, database: clientConfig.database, workflowID }); + return await vscode.debug.startDebugging( + folder, + { + name: `Debug ${workflowID}`, + type: 'node-terminal', + request: 'launch', + command: `npx dbos-sdk debug -x ${proxyURL} -u ${workflowID}` + } + ); } ); + + if (!debuggerStarted) { + throw new Error("vscode.debug.startDebugging returned false"); + } + } catch (e) { logger.error("startDebugging", e); - vscode.window.showErrorMessage(`Failed to start debugging\n${stringify(e)}`); + vscode.window.showErrorMessage(`Failed to start debugging`); } } export async function startDebuggingFromCodeLens(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) { - const clientConfig = await config.getProvDbConfig(folder); - if (!clientConfig) { return; } - - logger.info(`startDebuggingFromCodeLens`, { folder: folder.uri.fsPath, database: clientConfig.database, name, type: $type }); - - const statuses = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Window }, - () => { return provDB.getWorkflowStatuses(clientConfig, name, $type); }); - - // TODO: eventually, we'll need a better UI than "list all workflow IDs and let the user pick one" - const wfid = await vscode.window.showQuickPick(statuses.map(s => s.workflow_uuid), { - placeHolder: `Select a ${name} workflow ID to debug`, - canPickMany: false, + logger.info(`startDebuggingFromCodeLens`, { folder: folder.uri.fsPath, name, type: $type }); + await startDebugging(folder, async (clientConfig) => { + // TODO: eventually, we'll need a better UI than "list all workflow IDs and let the user pick one" + const statuses = await provDB.getWorkflowStatuses(clientConfig, name, $type); + return await vscode.window.showQuickPick(statuses.map(s => s.workflow_uuid), { + placeHolder: `Select a ${name} workflow ID to debug`, + canPickMany: false, + }); }); - - if (!wfid) { return; } - - await startDebugging(folder, clientConfig, wfid); - } + export async function startDebuggingFromUri(wfid: string) { const folder = await getWorkspaceFolder(); - if (!folder) { return; } - const clientConfig = await config.getProvDbConfig(folder); - if (!clientConfig) { return; } - - logger.info(`startDebuggingFromUri`, { folder: folder.uri.fsPath, database: clientConfig.database, wfid }); - - const validStatus = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Window }, - () => { return provDB.getWorkflowStatus(clientConfig, wfid); }); - - if (validStatus.length === 0) { - vscode.window.showErrorMessage(`Workflow ID ${wfid} not found in provenance database`); - return; - } - - await startDebugging(folder, clientConfig, wfid); + if (!folder) { return; } + + logger.info(`startDebuggingFromUri`, { folder: folder.uri.fsPath, wfid }); + await startDebugging(folder, async (clientConfig) => { + const validStatus = await provDB.getWorkflowStatus(clientConfig, wfid); + if (validStatus.length === 0) { + vscode.window.showErrorMessage(`Workflow ID ${wfid} not found in provenance database`); + return undefined; + } else { + return wfid; + } + }); } export function shutdownDebugProxy() { diff --git a/src/uriHandler.ts b/src/uriHandler.ts index 3275c1e..91c711c 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -2,15 +2,26 @@ import * as vscode from 'vscode'; import { logger } from './extension'; import { startDebuggingUriCommandName } from './commands'; +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + export class TTDbgUriHandler implements vscode.UriHandler { async handleUri(uri: vscode.Uri): Promise { logger.debug(`TTDbgUriHandler.handleUri`, { uri: uri.toString() }); switch (uri.path) { - case '/start-debugging': + case '/start-debugging': const searchParams = new URLSearchParams(uri.query); - const wfID = searchParams.get('wfid'); - if (wfID) { - vscode.commands.executeCommand(startDebuggingUriCommandName, wfID); + const wfid = searchParams.get('wfid'); + if (wfid) { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Starting DBOS Time Travel Debugger for workflow ID ${wfid}`, + }, async () => { + await vscode.commands.executeCommand(startDebuggingUriCommandName, wfid); + }); } else { vscode.window.showErrorMessage(`Invalid start-debugging uri: ${uri}`); } From 058f446aee82831c6eec0265e7b9bbadf39036f1 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Mon, 26 Feb 2024 14:54:05 -0800 Subject: [PATCH 3/3] PR feedback --- src/ProvenanceDatabase.ts | 5 +++-- src/commands.ts | 6 +++--- src/uriHandler.ts | 6 ------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/ProvenanceDatabase.ts b/src/ProvenanceDatabase.ts index 836dbaa..1ac1dfc 100644 --- a/src/ProvenanceDatabase.ts +++ b/src/ProvenanceDatabase.ts @@ -47,9 +47,10 @@ export class ProvenanceDatabase { return results.rows; } - async getWorkflowStatus(clientConfig: ClientConfig, wfid: string): Promise { + async getWorkflowStatus(clientConfig: ClientConfig, wfid: string): Promise { const db = await this.connect(clientConfig); const results = await db.query('SELECT * FROM dbos.workflow_status WHERE workflow_uuid = $1 LIMIT 10', [wfid]); - return results.rows; + if (results.rows.length > 1) { throw new Error(`Multiple workflow status records found for workflow ID ${wfid}`); } + return results.rows.length === 1 ? results.rows[0] : undefined; } } diff --git a/src/commands.ts b/src/commands.ts index f06f0d8..3070437 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -31,7 +31,7 @@ async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (cl return await vscode.debug.startDebugging( folder, { - name: `Debug ${workflowID}`, + name: `Time-Travel Debug ${workflowID}`, type: 'node-terminal', request: 'launch', command: `npx dbos-sdk debug -x ${proxyURL} -u ${workflowID}` @@ -68,8 +68,8 @@ export async function startDebuggingFromUri(wfid: string) { logger.info(`startDebuggingFromUri`, { folder: folder.uri.fsPath, wfid }); await startDebugging(folder, async (clientConfig) => { - const validStatus = await provDB.getWorkflowStatus(clientConfig, wfid); - if (validStatus.length === 0) { + const wfStatus = await provDB.getWorkflowStatus(clientConfig, wfid); + if (!wfStatus) { vscode.window.showErrorMessage(`Workflow ID ${wfid} not found in provenance database`); return undefined; } else { diff --git a/src/uriHandler.ts b/src/uriHandler.ts index 91c711c..bb53d89 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -2,12 +2,6 @@ import * as vscode from 'vscode'; import { logger } from './extension'; import { startDebuggingUriCommandName } from './commands'; -function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export class TTDbgUriHandler implements vscode.UriHandler { async handleUri(uri: vscode.Uri): Promise { logger.debug(`TTDbgUriHandler.handleUri`, { uri: uri.toString() });