Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve prov DB info from cloud via dbos-cloud CLI #4

Merged
merged 11 commits into from
Feb 21, 2024
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 5 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,14 @@
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "dbos-ttdbg.launch-debug-proxy",
qianl15 marked this conversation as resolved.
Show resolved Hide resolved
"title": "Launch Debug Proxy",
"category": "DBOS"
},
{
"command": "dbos-ttdbg.shutdown-debug-proxy",
"title": "Shutdown Debug Proxy",
"category": "DBOS"
},

{
"command": "dbos-ttdbg.delete-prov-db-password",
"title": "Delete Stored Provenance DB Password",
"command": "dbos-ttdbg.delete-prov-db-passwords",
"title": "Delete Stored Provenance DB Passwords",
"category": "DBOS"
}
],
Expand All @@ -51,8 +45,7 @@
"type": "string"
},
"dbos-ttdbg.prov_db_port": {
"type": "number",
"default": 5432
"type": "number"
},
"dbos-ttdbg.prov_db_database": {
"type": "string"
Expand All @@ -75,6 +68,7 @@
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.504.0",
"@types/fnv-plus": "^1.3.2",
"@types/mocha": "^10.0.6",
"@types/node": "18.x",
"@types/pg": "^8.11.0",
Expand All @@ -88,6 +82,7 @@
"@vscode/vsce": "^2.23.0",
"esbuild": "^0.20.0",
"eslint": "^8.56.0",
"fnv-plus": "^1.3.1",
"jszip": "^3.10.1",
"pg": "^8.11.3",
"rimraf": "^5.0.5",
Expand Down
4 changes: 2 additions & 2 deletions src/CloudStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class S3CloudStorage implements CloudStorage {
signer: { sign: (request) => Promise.resolve(request) }
});
}

dispose() {
this.s3.destroy();
}
Expand Down Expand Up @@ -84,7 +84,7 @@ export class S3CloudStorage implements CloudStorage {
if (!IsTruncated) { break; }
if (!NextContinuationToken) { break; } // (should not happen, but just in case...)
cmd.input.ContinuationToken = NextContinuationToken;
}
}
}
}

68 changes: 33 additions & 35 deletions src/DebugProxy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as vscode from 'vscode';
import { ChildProcessWithoutNullStreams as ChildProcess, spawn, execFile } from "child_process";
import { ChildProcessWithoutNullStreams as ChildProcess, spawn } from "child_process";
import jszip from 'jszip';
import * as fs from 'node:fs/promises';
import * as semver from 'semver';
import { CloudStorage } from './CloudStorage';
import { config, logger } from './extension';
import { exists } from './utils';
import { execFile, exists, hashClientConfig } from './utils';
import { ClientConfig } from 'pg';

const IS_WINDOWS = process.platform === "win32";
const EXE_FILE_NAME = `debug-proxy${IS_WINDOWS ? ".exe" : ""}`;
Expand All @@ -24,7 +25,7 @@ function throwOnCancelled(token?: vscode.CancellationToken) {

export class DebugProxy {
private _outChannel: vscode.LogOutputChannel;
private _proxyProcess: ChildProcess | undefined;
private _proxyProcesses: Map<number, ChildProcess> = new Map();

constructor(private readonly cloudStorage: CloudStorage, private readonly storageUri: vscode.Uri) {
this._outChannel = vscode.window.createOutputChannel("DBOS Debug Proxy", { log: true });
Expand All @@ -35,19 +36,21 @@ export class DebugProxy {
}

shutdown() {
if (this._proxyProcess) {
const process = this._proxyProcess;
this._proxyProcess = undefined;
logger.info(`Debug Proxy shutting down`, { pid: process.pid });
for (const [key, process] of this._proxyProcesses.entries()) {
this._proxyProcesses.delete(key);
logger.info(`Debug Proxy shutting down`, { folder: key, pid: process.pid });
process.stdout.removeAllListeners();
process.stderr.removeAllListeners();
process.removeAllListeners();
process.kill();
}
}

async launch() {
if (this._proxyProcess) { return; }
async launch(clientConfig: ClientConfig) {
const configHash = hashClientConfig(clientConfig);

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

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

const proxy_port = config.proxyPort;
let { host, port, database, user, password } = config.provDbConfig;
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 (!host || !database || !user || !password) {
throw new Error("Invalid configuration");
Expand All @@ -77,7 +83,7 @@ export class DebugProxy {
args.push("-listen", `${proxy_port}`);
}

this._proxyProcess = spawn(
const proxyProcess = spawn(
exeUri.fsPath,
args,
{
Expand All @@ -86,35 +92,38 @@ export class DebugProxy {
}
}
);
logger.info(`Debug Proxy launched`, { port: proxy_port, pid: this._proxyProcess.pid });
logger.info(`Debug Proxy launched`, { port: proxy_port, pid: proxyProcess.pid, database });
this._proxyProcesses.set(configHash, proxyProcess);

this._proxyProcess.stdout.on("data", (data: Buffer) => {
proxyProcess.stdout.on("data", (data: Buffer) => {
const { time, level, msg, ...properties } = JSON.parse(data.toString()) as { time: string, level: string, msg: string, [key: string]: unknown };
const $properties = { ...properties, database };
switch (level.toLowerCase()) {
case "debug":
this._outChannel.debug(msg, properties);
this._outChannel.debug(msg, $properties);
break;
case "info":
this._outChannel.info(msg, properties);
this._outChannel.info(msg, $properties);
break;
case "warn":
this._outChannel.warn(msg, properties);
this._outChannel.warn(msg, $properties);
break;
case "error":
this._outChannel.error(msg, properties);
this._outChannel.error(msg, $properties);
break;
default:
this._outChannel.appendLine(`${time} [${level}] ${msg} ${JSON.stringify(properties)}`);
this._outChannel.appendLine(`${time} [${level}] ${msg} ${JSON.stringify($properties)}`);
break;
}
});

this._proxyProcess.on("error", e => {
this._outChannel.error(e);
proxyProcess.on("error", e => {
this._outChannel.error(e, { database });
});

this._proxyProcess.on("exit", (code, _signal) => {
this._outChannel.info(`Debug Proxy exited with exit code ${code}`);
proxyProcess.on("exit", (code, _signal) => {
this._proxyProcesses.delete(configHash);
this._outChannel.info(`Debug Proxy exited with exit code ${code}`, { database });
});
}

Expand Down Expand Up @@ -163,19 +172,8 @@ export class DebugProxy {
}

try {
return await new Promise<string | undefined>((resolve, reject) => {
execFile(exeUri.fsPath, ["-version"], (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
if (stderr) {
reject(stderr);
} else {
resolve(stdout.trim());
}
}
});
});
const { stdout } = await execFile(exeUri.fsPath, ["-version"]);
return stdout.trim();
} catch (e) {
logger.error("Failed to get local debug proxy version", e);
return undefined;
Expand Down
25 changes: 16 additions & 9 deletions src/ProvenanceDatabase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Client } from 'pg';
import * as vscode from 'vscode';
import { Client, ClientConfig } from 'pg';
import { config, logger } from './extension';
import { DbosMethodType, getDbosWorkflowName } from './sourceParser';
import { hashClientConfig } from './utils';

export interface workflow_status {
workflow_uuid: string;
Expand All @@ -16,24 +18,29 @@ export interface workflow_status {
}

export class ProvenanceDatabase {
private _db: Client | undefined;
private _databases: Map<number, Client> = new Map();

dispose() {
this._db?.end(e => logger.error(e));
for (const db of this._databases.values()) {
db.end(e => logger.error(e));
}
}

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

const db = new Client(config.provDbConfig);
const db = new Client(clientConfig);
await db.connect();
this._db = db;
this._databases.set(configHash, db);
return db;
}

async getWorkflowStatuses(name: string, $type: DbosMethodType): Promise<workflow_status[]> {
async getWorkflowStatuses(clientConfig: ClientConfig, name: string, $type: DbosMethodType): Promise<workflow_status[]> {
const wfName = getDbosWorkflowName(name, $type);
const db = await this.connect();
const db = await this.connect(clientConfig);
const results = await db.query<workflow_status>('SELECT * FROM dbos.workflow_status WHERE name = $1 LIMIT 10', [wfName]);
return results.rows;
}
Expand Down
5 changes: 4 additions & 1 deletion src/codeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { getDbosMethodType, parse } from './sourceParser';
export class TTDbgCodeLensProvider implements vscode.CodeLensProvider {
provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
try {
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!folder) { return; }

const text = document.getText();
const file = ts.createSourceFile(
document.fileName,
Expand All @@ -30,7 +33,7 @@ export class TTDbgCodeLensProvider implements vscode.CodeLensProvider {
title: '⏳ Time Travel Debug',
tooltip: `Debug ${methodInfo.name} with the DBOS Time Travel Debugger`,
command: startDebuggingCommandName,
arguments: [methodInfo.name, methodType]
arguments: [folder, methodInfo.name, methodType]
});
})
.filter(<T>(x?: T): x is T => !!x);
Expand Down
30 changes: 11 additions & 19 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import * as vscode from 'vscode';
import { logger, config, provDB, debugProxy } from './extension';
import { DbosMethodType } from "./sourceParser";
import { stringify } from './utils';

export const startDebuggingCommandName = "dbos-ttdbg.startDebugging";
export const launchDebugProxyCommandName = "dbos-ttdbg.launch-debug-proxy";
export const shutdownDebugProxyCommandName = "dbos-ttdbg.shutdown-debug-proxy";
export const deleteProvenanceDatabasePasswordCommandName = "dbos-ttdbg.delete-prov-db-password";
export const deleteProvenanceDatabasePasswordCommandName = "dbos-ttdbg.delete-prov-db-passwords";

export async function startDebugging(name: string, $type: DbosMethodType) {
export async function startDebugging(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) {
try {
await debugProxy.launch();
const statuses = await provDB.getWorkflowStatuses(name, $type);
const clientConfig = await config.getProvDbConfig(folder);
const statuses = await provDB.getWorkflowStatuses(clientConfig, name, $type);
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), {
Expand All @@ -33,18 +34,9 @@ export async function startDebugging(name: string, $type: DbosMethodType) {
}
);
} catch (e) {
const reason = stringify(e);
logger.error("startDebugging", e);
vscode.window.showErrorMessage("Failed to start debugging");
}
}

export async function launchDebugProxy() {
try {
await debugProxy.launch();
vscode.window.showInformationMessage("Debug proxy launched");
} catch (e) {
logger.error("launchDebugProxy", e);
vscode.window.showErrorMessage("Failed to launch debug proxy");
vscode.window.showErrorMessage(`Failed to start debugging\n${reason}`);
}
}

Expand All @@ -56,10 +48,10 @@ export async function shutdownDebugProxy() {
}
}

export async function deleteProvenanceDatabasePassword() {
export async function deleteProvenanceDatabasePasswords() {
try {
await config.deletePassword();
await config.deletePasswords();
} catch (e) {
logger.error("deleteProvenanceDatabasePassword", e);
logger.error("deleteProvenanceDatabasePasswords", e);
}
}
Loading