From 770bb61bef52e132a79d85a94cc834ed796e941e Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 28 Feb 2024 13:04:36 -0800 Subject: [PATCH] ProvenanceDatabaseConfig --- src/DebugProxy.ts | 19 ++++++++++++------- src/ProvenanceDatabase.ts | 23 ++++++++++++++++------- src/cloudCli.ts | 22 ++++++++++++---------- src/commands.ts | 35 +++++++++++++++++++++++------------ src/configuration.ts | 37 ++++++++++++++++++------------------- src/utils.ts | 3 ++- 6 files changed, 83 insertions(+), 56 deletions(-) diff --git a/src/DebugProxy.ts b/src/DebugProxy.ts index 6f34bc6..e16b815 100644 --- a/src/DebugProxy.ts +++ b/src/DebugProxy.ts @@ -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" : ""}`; @@ -46,11 +46,11 @@ export class DebugProxy { } } - async launch(clientConfig: ClientConfig) { + async launch(clientConfig: ProvenanceDatabaseConfig): Promise { 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); @@ -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("Application 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"); } @@ -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() { diff --git a/src/ProvenanceDatabase.ts b/src/ProvenanceDatabase.ts index 24b2a17..5eecce7 100644 --- a/src/ProvenanceDatabase.ts +++ b/src/ProvenanceDatabase.ts @@ -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; @@ -28,26 +28,35 @@ export class ProvenanceDatabase { } } - private async connect(clientConfig: ClientConfig): Promise { - const configHash = hashClientConfig(clientConfig); + private async connect(dbConfig: ProvenanceDatabaseConfig): Promise { + 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, + }); await db.connect(); this._databases.set(configHash, db); return db; } - async getWorkflowStatuses(clientConfig: ClientConfig, name: string, $type: DbosMethodType): Promise { + async getWorkflowStatuses(clientConfig: ProvenanceDatabaseConfig, name: string, $type: DbosMethodType): Promise { const wfName = getDbosWorkflowName(name, $type); const db = await this.connect(clientConfig); const results = await db.query('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 { + async getWorkflowStatus(clientConfig: ProvenanceDatabaseConfig, wfid: string): Promise { const db = await this.connect(clientConfig); const results = await db.query('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}`); } diff --git a/src/cloudCli.ts b/src/cloudCli.ts index 3f64c99..879e127 100644 --- a/src/cloudCli.ts +++ b/src/cloudCli.ts @@ -26,21 +26,23 @@ async function dbos_cloud_cli(folder: vscode.WorkspaceFolder, ...args: string }); return JSON.parse(stdout) as T; } + export async function dbos_cloud_app_status(folder: vscode.WorkspaceFolder) { return dbos_cloud_cli(folder, "application", "status"); } + export async function dbos_cloud_db_status(folder: vscode.WorkspaceFolder, databaseName: string) { return dbos_cloud_cli(folder, "database", "status", databaseName); } export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) { - logger.info("dbos_cloud_login", { folder: folder.uri.fsPath }); + 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(); - const regexLoginUrl = /Login URL: (http.*\/activate\?user_code=([A-Z][A-Z][A-Z][A-Z]-[A-Z][A-Z][A-Z][A-Z]))/; + 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 { @@ -48,18 +50,18 @@ export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) { cts.token.onCancellationRequested(() => resolve()); }); - loginProc.on('exit', () => { logger.info("dbos-cloud login on exit"); cts.cancel(); }); - loginProc.on('close', () => { logger.info("dbos-cloud login on close"); cts.cancel(); }); - loginProc.on('error', err => { logger.error("dbos-cloud login on error", err); cts.cancel(); }); + 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.info("dbos-cloud login stdout on data", { data }); + logger.debug("dbos_cloud_login on stdout data", { data }); - const loginUrlMatch = regexLoginUrl.exec(data); + const loginUrlMatch = regexLoginInfo.exec(data); if (loginUrlMatch && loginUrlMatch.length === 3) { const [, loginUrl, userCode] = loginUrlMatch; - logger.info("dbos-cloud login url", { loginUri: loginUrl, userCode }); + logger.info("dbos_cloud_login login info", { loginUri: loginUrl, userCode }); userCodeEmitter.fire(userCode); const openResult = await vscode.env.openExternal(vscode.Uri.parse(loginUrl)); @@ -72,7 +74,7 @@ export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) { const successfulLoginMatch = regexSuccessfulLogin.exec(data); if (successfulLoginMatch && successfulLoginMatch.length === 2) { const [, user] = successfulLoginMatch; - logger.info("dbos-cloud login successful", { user }); + logger.info("dbos-dbos_cloud_login successful login", { user }); vscode.window.showInformationMessage(`Successfully logged in to DBOS Cloud as ${user}`); } }); @@ -99,6 +101,6 @@ export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) { const killed = loginProc.killed; const killResult = loginProc.kill(); - logger.info("dbos_cloud_login exit", { killed, killResult }); + logger.debug("dbos_cloud_login exit", { killed, killResult }); } } diff --git a/src/commands.ts b/src/commands.ts index 8bb4332..c86ba65 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -3,7 +3,7 @@ import { logger, config, provDB, debugProxy } from './extension'; import { DbosMethodType } from "./sourceParser"; import { getWorkspaceFolder, isQuickPickItem, showQuickPick } from './utils'; import { dbos_cloud_login } from './cloudCli'; -import { ClientConfig } from 'pg'; +import { ProvenanceDatabaseConfig } from './configuration'; export const cloudLoginCommandName = "dbos-ttdbg.cloud-login"; export const startDebuggingCodeLensCommandName = "dbos-ttdbg.start-debugging-code-lens"; @@ -11,29 +11,40 @@ 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) { +async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (clientConfig: ProvenanceDatabaseConfig) => Promise) { 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`); 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, { @@ -57,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 => { label: new Date(parseInt(s.created_at)).toLocaleString(), description: `${s.authenticated_user.length === 0 ? "" : s.authenticated_user} (${s.status})`, diff --git a/src/configuration.ts b/src/configuration.ts index a4adfce..caf95e4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -11,14 +11,7 @@ const PROV_DB_DATABASE = "prov_db_database"; const PROV_DB_USER = "prov_db_user"; const DEBUG_PROXY_PORT = "debug_proxy_port"; -interface DatabaseConfig { - host: string | undefined; - port: number | undefined; - database: string | undefined; - user: string | undefined; -} - -async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise { +async function getProvDBConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise { try { const app = await dbos_cloud_app_status(folder); const db = await dbos_cloud_db_status(folder, app.PostgresInstanceName); @@ -38,7 +31,7 @@ async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise } } -function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): DatabaseConfig { +function getProvDBConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): ProvenanceDatabaseConfig { const cfg = vscode.workspace.getConfiguration(TTDBG_CONFIG_SECTION, folder); const host = cfg.get(PROV_DB_HOST); @@ -70,21 +63,29 @@ async function startInvalidCredentialsFlow(folder: vscode.WorkspaceFolder): Prom const result = await vscode.window.showWarningMessage(message, ...items); switch (result) { // case "Register": break; - case "Login": - await dbos_cloud_login(folder); + case "Login": + await dbos_cloud_login(folder); break; } } +export interface ProvenanceDatabaseConfig { + user?: string | undefined; + database?: string | undefined; + password?: string | undefined | (() => Promise); + port?: number | undefined; + host?: string | undefined; +} + export class Configuration { constructor(private readonly secrets: vscode.SecretStorage) { } - async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise { + async getProvDBConfig(folder: vscode.WorkspaceFolder): Promise { const dbConfig = await vscode.window.withProgress( { location: vscode.ProgressLocation.Window }, async () => { - const cloudConfig = await getDbConfigFromDbosCloud(folder); - const localConfig = getDbConfigFromVSCodeConfig(folder); + const cloudConfig = await getProvDBConfigFromDbosCloud(folder); + const localConfig = getProvDBConfigFromVSCodeConfig(folder); const host = localConfig?.host ?? cloudConfig?.host; const port = localConfig?.port ?? cloudConfig?.port ?? 5432; @@ -101,7 +102,6 @@ export class Configuration { database: dbConfig.database, user: dbConfig.user, password: () => this.#getPassword(folder), - ssl: { rejectUnauthorized: false } }; } else { startInvalidCredentialsFlow(folder).catch(e => logger.error("startInvalidCredentialsFlow", e)); @@ -118,7 +118,7 @@ export class Configuration { return `${TTDBG_CONFIG_SECTION}.prov_db_password.${folder.uri.fsPath}`; } - async #getPassword(folder: vscode.WorkspaceFolder): Promise { + async #getPassword(folder: vscode.WorkspaceFolder): Promise { const passwordKey = this.#getPasswordKey(folder); let password = await this.secrets.get(passwordKey); if (!password) { @@ -126,10 +126,9 @@ export class Configuration { prompt: "Enter application database password", password: true, }); - if (!password) { - throw new Error('Application database password is required'); + if (password) { + await this.secrets.store(passwordKey, password); } - await this.secrets.store(passwordKey, password); } return password; } diff --git a/src/utils.ts b/src/utils.ts index 64612ae..b1bf483 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import { execFile as cpExecFile } from "child_process"; import util from 'util'; import { fast1a32 } from 'fnv-plus'; import { ClientConfig } from 'pg'; +import { ProvenanceDatabaseConfig } from './configuration'; export const PLATFORM = function () { switch (process.platform) { @@ -42,7 +43,7 @@ export async function exists(uri: vscode.Uri): Promise { export const execFile = util.promisify(cpExecFile); -export function hashClientConfig(clientConfig: ClientConfig) { +export function hashClientConfig(clientConfig: ClientConfig | ProvenanceDatabaseConfig) { const { host, port, database, user } = clientConfig; return host && port && database && user ? fast1a32(`${host}:${port}:${database}:${user}`)