Skip to content

Commit

Permalink
Assorted Cleanup (#9)
Browse files Browse the repository at this point in the history
* remove term "provenance database"

* detect missing and expired cloud cred errors

* order wf ids by created_at DESC

* minor cleanup

* move cloud cli logic to separate file

* fix file name convention

* ProvenanceDatabaseConfig

* fix ssl setting in ProvenanceDatabase.connect

* readme

* marked extension as preview in package.json

* don't include .github files in packed extension

* add current dbos logo to extension

* update changelog and version for early adopter release

* update CHANGELOG
  • Loading branch information
devhawk authored Feb 28, 2024
1 parent d463bc0 commit f23f79f
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 191 deletions.
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.vscode/**
.vscode-test/**
src/**
.github/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
Expand All @@ -9,3 +10,4 @@ vsc-extension-quickstart.md
**/*.map
**/*.ts
**/.vscode-test.*

11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

All notable changes to the "dbos-ttdbg" extension will be documented in this file.

Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
This project adheres to [Semantic Versioning](https://semver.org) and uses
[NerdBank.GitVersioning](https://github.com/AArnott/Nerdbank.GitVersioning) to manage version numbers.

## [Unreleased]
As per [VSCode recommendation](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions),
this project uses *EVEN* minor version numbers for release versions and *ODD* minor version numbers for pre-release versions,

- Initial release
## [0.9]

- Initial preview release
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,45 @@
# dbos-ttdbg README
# DBOS Time Travel Debugger

This extension enables developers to debug their DBOS applications deployed to the DBOS Cloud using VS Code.
DBOS Cloud allows time travel debugging of any DBOS application execution that has occurred in the past three days.

## Time Travel Debug CodeLens

The DBOS Time Travel Debugger extension installed automatically attaches a "⏳ Time Travel Debug"
[CodeLens](https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup)
to every DBOS [workflow](https://docs.dbos.dev/tutorials/workflow-tutorial),
[transaction](https://docs.dbos.dev/tutorials/transaction-tutorial)
and [communicator](https://docs.dbos.dev/tutorials/communicator-tutorial) method in your DBOS application.

![DBOS Time Travel CodeLens Screenshot](images/ttdbg-code-lens.png)

When you click on the Time Travel Debug CodeLens, you are provided a list of recent executions of that method to debug.

![DBOS Time Travel Workflow ID picker](images/ttdbg-wfid-quick-pick.png)

After selecting a recent execution of your function, the DBOS Time Travel Debugger will launch the DBOS debug runtime
and VS Code typescript debugger. This allows you to debug your DBOS application against the database as it existed
at the time the selected execution originally occurred.

For more information, please see the [official DBOS documentation](https://docs.dbos.dev/).

## Installation

The latest released version of the DBOS Time Travel Debugger for VS Code can be installed via the
[VS Code Marketplace](https://marketplace.visualstudio.com/publishers/dbos-inc).

DBOS depends on [Node.js](https://nodejs.org/) version 20 or later.
The DBOS Time Travel Debugger for VS Code has no additional dependencies beyond what DBOS depends on.

### Preview Releases

The DBOS Time Travel Debugger builds every commit in our [GitHub repo](https://github.com/dbos-inc/ttdbg-extension).
You can install a preview build of the Time Travel Debugger extension navigating to a recent
[GitHub action run](https://github.com/dbos-inc/ttdbg-extension/actions/workflows/on_push.yml)
and downloading the associated "Extension" build artifact.
The "Extension" build artifact is a zip file containing the Time Travel Debugger's VSIX file, which can be installed manually.
For more information on installing VSIX extensions in Visual Studio Code, please see the
[official Visual Studio Code docs](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix).

## Versioning Strategy

Expand Down
Binary file added dbos-logo-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/ttdbg-code-lens.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/ttdbg-wfid-quick-pick.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"displayName": "DBOS Time Travel Debugger",
"publisher": "dbos-inc",
"version": "0.0.0-placeholder",
"icon": "dbos-logo-128.png",
"engines": {
"vscode": "^1.86.0"
},
"preview": true,
"categories": [
"Other"
],
Expand All @@ -30,7 +32,7 @@
},
{
"command": "dbos-ttdbg.delete-prov-db-passwords",
"title": "Delete Stored Provenance DB Passwords",
"title": "Delete Stored Application Database Passwords",
"category": "DBOS"
},
{
Expand Down
19 changes: 12 additions & 7 deletions src/DebugProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as semver from 'semver';
import { CloudStorage } from './CloudStorage';
import { config, logger } from './extension';
import { execFile, exists, hashClientConfig } from './utils';
import { ClientConfig } from 'pg';
import { ProvenanceDatabaseConfig } from './configuration';

const IS_WINDOWS = process.platform === "win32";
const EXE_FILE_NAME = `debug-proxy${IS_WINDOWS ? ".exe" : ""}`;
Expand Down Expand Up @@ -46,11 +46,11 @@ export class DebugProxy {
}
}

async launch(clientConfig: ClientConfig) {
async launch(clientConfig: ProvenanceDatabaseConfig): Promise<boolean> {
const configHash = hashClientConfig(clientConfig);

if (!configHash) { throw new Error("Invalid configuration"); }
if (this._proxyProcesses.has(configHash)) { return; }
if (this._proxyProcesses.has(configHash)) { return true; }

const exeUri = exeFileName(this.storageUri);
const exeExists = await exists(exeUri);
Expand All @@ -60,12 +60,15 @@ export class DebugProxy {

const proxy_port = config.proxyPort;
let { host, port, database, user, password } = clientConfig;
if (typeof password === "function") {
password = await password();
if (!password) {
throw new Error("Provenance database password is required");
if (typeof password === "function") {
const $password = await password();
if ($password) {
password = $password;
} else {
return false;
}
}

if (!host || !database || !user || !password) {
throw new Error("Invalid configuration");
}
Expand Down Expand Up @@ -130,6 +133,8 @@ export class DebugProxy {
this._proxyProcesses.delete(configHash);
this._outChannel.info(`Debug Proxy exited with exit code ${code}`, { database });
});

return true;
}

async getVersion() {
Expand Down
28 changes: 19 additions & 9 deletions src/ProvenanceDatabase.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import { Client, ClientConfig } from 'pg';
import { config, logger } from './extension';
import { logger } from './extension';
import { DbosMethodType, getDbosWorkflowName } from './sourceParser';
import { hashClientConfig } from './utils';
import { ProvenanceDatabaseConfig } from './configuration';

export interface workflow_status {
workflow_uuid: string;
Expand All @@ -28,28 +28,38 @@ export class ProvenanceDatabase {
}
}

private async connect(clientConfig: ClientConfig): Promise<Client> {
const configHash = hashClientConfig(clientConfig);
private async connect(dbConfig: ProvenanceDatabaseConfig): Promise<Client> {
const configHash = hashClientConfig(dbConfig);
if (!configHash) { throw new Error("Invalid configuration"); }
const existingDB = this._databases.get(configHash);
if (existingDB) { return existingDB; }

const db = new Client(clientConfig);
const password = typeof dbConfig.password === "function" ? await dbConfig.password() : dbConfig.password;
if (!password) { throw new Error("Invalid password"); }

const db = new Client({
host: dbConfig.host,
port: dbConfig.port,
database: dbConfig.database,
user: dbConfig.user,
password,
ssl: { rejectUnauthorized: false }
});
await db.connect();
this._databases.set(configHash, db);
return db;
}

async getWorkflowStatuses(clientConfig: ClientConfig, name: string, $type: DbosMethodType): Promise<workflow_status[]> {
async getWorkflowStatuses(clientConfig: ProvenanceDatabaseConfig, name: string, $type: DbosMethodType): Promise<workflow_status[]> {
const wfName = getDbosWorkflowName(name, $type);
const db = await this.connect(clientConfig);
const results = await db.query<workflow_status>('SELECT * FROM dbos.workflow_status WHERE name = $1 LIMIT 10', [wfName]);
const results = await db.query<workflow_status>('SELECT * FROM dbos.workflow_status WHERE name = $1 ORDER BY created_at DESC LIMIT 10', [wfName]);
return results.rows;
}

async getWorkflowStatus(clientConfig: ClientConfig, wfid: string): Promise<workflow_status | undefined> {
async getWorkflowStatus(clientConfig: ProvenanceDatabaseConfig, wfid: string): Promise<workflow_status | undefined> {
const db = await this.connect(clientConfig);
const results = await db.query<workflow_status>('SELECT * FROM dbos.workflow_status WHERE workflow_uuid = $1 LIMIT 10', [wfid]);
const results = await db.query<workflow_status>('SELECT * FROM dbos.workflow_status WHERE workflow_uuid = $1', [wfid]);
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;
}
Expand Down
106 changes: 106 additions & 0 deletions src/cloudCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as vscode from 'vscode';
import { spawn as cpSpawn } from "child_process";
import { execFile } from './utils';
import { logger } from './extension';

export interface DbosCloudApp {
Name: string;
ID: string;
PostgresInstanceName: string;
ApplicationDatabaseName: string;
Status: string;
Version: string;
}

export interface DbosCloudDatabase {
PostgresInstanceName: string;
HostName: string;
Status: string;
Port: number;
AdminUsername: string;
}

async function dbos_cloud_cli<T>(folder: vscode.WorkspaceFolder, ...args: string[]): Promise<T> {
const { stdout } = await execFile("npx", ["dbos-cloud", ...args, "--json"], {
cwd: folder.uri.fsPath,
});
return JSON.parse(stdout) as T;
}

export async function dbos_cloud_app_status(folder: vscode.WorkspaceFolder) {
return dbos_cloud_cli<DbosCloudApp>(folder, "application", "status");
}

export async function dbos_cloud_db_status(folder: vscode.WorkspaceFolder, databaseName: string) {
return dbos_cloud_cli<DbosCloudDatabase>(folder, "database", "status", databaseName);
}

export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) {
logger.debug("dbos_cloud_login", { folder: folder.uri.fsPath });

const cts = new vscode.CancellationTokenSource();
const loginProc = cpSpawn("npx", ["dbos-cloud", "login"], { cwd: folder.uri.fsPath });
const userCodeEmitter = new vscode.EventEmitter<string>();

const regexLoginInfo = /Login URL: (http.*\/activate\?user_code=([A-Z][A-Z][A-Z][A-Z]-[A-Z][A-Z][A-Z][A-Z]))/;
const regexSuccessfulLogin = /Successfully logged in as (.*)!/;

try {
const ctsPromise = new Promise<void>(resolve => {
cts.token.onCancellationRequested(() => resolve());
});

loginProc.on('exit', (code) => { logger.debug("dbos_cloud_login on proc exit", { code }); cts.cancel(); });
loginProc.on('close', (code) => { logger.debug("dbos_cloud_login on proc close", { code }); cts.cancel(); });
loginProc.on('error', err => { logger.error("dbos_cloud_login on proc error", err); cts.cancel(); });

loginProc.stdout.on("data", async (buffer: Buffer) => {
const data = buffer.toString().trim();
logger.debug("dbos_cloud_login on stdout data", { data });

const loginUrlMatch = regexLoginInfo.exec(data);
if (loginUrlMatch && loginUrlMatch.length === 3) {
const [, loginUrl, userCode] = loginUrlMatch;
logger.info("dbos_cloud_login login info", { loginUri: loginUrl, userCode });
userCodeEmitter.fire(userCode);

const openResult = await vscode.env.openExternal(vscode.Uri.parse(loginUrl));
if (!openResult) {
logger.error("dbos_cloud_login openExternal failed", { loginUri: loginUrl, userCode });
cts.cancel();
}
}

const successfulLoginMatch = regexSuccessfulLogin.exec(data);
if (successfulLoginMatch && successfulLoginMatch.length === 2) {
const [, user] = successfulLoginMatch;
logger.info("dbos-dbos_cloud_login successful login", { user });
vscode.window.showInformationMessage(`Successfully logged in to DBOS Cloud as ${user}`);
}
});

await vscode.window.withProgress({
cancellable: true,
location: vscode.ProgressLocation.Notification,
title: "Launching browser to log into DBOS Cloud"
}, async (progress, token) => {
userCodeEmitter.event(userCode => {
progress.report({ message: `\nUser code: ${userCode}` });
});

token.onCancellationRequested(() => cts.cancel());
await ctsPromise;
});
} finally {
loginProc.stdout.removeAllListeners();
loginProc.stderr.removeAllListeners();
loginProc.removeAllListeners();

cts.dispose();
userCodeEmitter.dispose();

const killed = loginProc.killed;
const killResult = loginProc.kill();
logger.debug("dbos_cloud_login exit", { killed, killResult });
}
}
41 changes: 25 additions & 16 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,49 @@ import * as vscode from 'vscode';
import { logger, config, provDB, debugProxy } from './extension';
import { DbosMethodType } from "./sourceParser";
import { getWorkspaceFolder, isQuickPickItem, showQuickPick } from './utils';
import { dbos_cloud_login } from './configuration';
import { ClientConfig } from 'pg';
import { dbos_cloud_login } from './cloudCli';
import { ProvenanceDatabaseConfig } from './configuration';

export const cloudLoginCommandName = "dbos-ttdbg.cloud-login";
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";

async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (clientConfig: ClientConfig) => Promise<string | undefined>) {
async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (clientConfig: ProvenanceDatabaseConfig) => Promise<string | undefined>) {
try {
const clientConfig = await config.getProvDbConfig(folder);
if (!clientConfig) { return; }

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 dbConfig = await config.getProvDBConfig(folder);
if (!dbConfig) {
logger.warn("startDebugging: config.getProvDBConfig returned undefined");
return;
}

const proxyLaunched = await debugProxy.launch(dbConfig);
if (!proxyLaunched) {
logger.warn("startDebugging: debugProxy.launch returned false");
return;
}

const workflowID = await getWorkflowID(dbConfig);
if (!workflowID) {
logger.warn("startDebugging: getWorkflowID returned undefined");
return;
}

const workflowStatus = await provDB.getWorkflowStatus(clientConfig, workflowID);
const workflowStatus = await provDB.getWorkflowStatus(dbConfig, workflowID);
if (!workflowStatus) {
vscode.window.showErrorMessage(`Workflow ID ${workflowID} not found in provenance database`);
vscode.window.showErrorMessage(`Workflow ID ${workflowID} not found`);
return;
}

const proxyURL = `http://localhost:${config.proxyPort ?? 2345}`;
logger.info(`startDebugging`, { folder: folder.uri.fsPath, database: clientConfig.database, workflowID });
logger.info(`startDebugging`, { folder: folder.uri.fsPath, database: dbConfig.database, workflowID });
const debuggerStarted = await vscode.debug.startDebugging(
folder,
{
Expand All @@ -49,8 +60,6 @@ async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (cl
}
}
);


} catch (e) {
logger.error("startDebugging", e);
vscode.window.showErrorMessage(`Failed to start debugging`);
Expand All @@ -59,8 +68,8 @@ async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (cl

export async function startDebuggingFromCodeLens(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) {
logger.info(`startDebuggingFromCodeLens`, { folder: folder.uri.fsPath, name, type: $type });
await startDebugging(folder, async (clientConfig) => {
const statuses = await provDB.getWorkflowStatuses(clientConfig, name, $type);
await startDebugging(folder, async (dbConfig) => {
const statuses = await provDB.getWorkflowStatuses(dbConfig, name, $type);
const items = statuses.map(s => <vscode.QuickPickItem>{
label: new Date(parseInt(s.created_at)).toLocaleString(),
description: `${s.authenticated_user.length === 0 ? "<anonymous>" : s.authenticated_user} (${s.status})`,
Expand Down
Loading

0 comments on commit f23f79f

Please sign in to comment.