diff --git a/.vscodeignore b/.vscodeignore index 72aa0fe..b305421 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,6 +1,7 @@ .vscode/** .vscode-test/** src/** +.github/** .gitignore .yarnrc vsc-extension-quickstart.md @@ -9,3 +10,4 @@ vsc-extension-quickstart.md **/*.map **/*.ts **/.vscode-test.* + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9ab6f..f53588c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 \ No newline at end of file +## [0.9] + +- Initial preview release \ No newline at end of file diff --git a/README.md b/README.md index 3837fc8..f757af0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dbos-logo-128.png b/dbos-logo-128.png new file mode 100644 index 0000000..6639158 Binary files /dev/null and b/dbos-logo-128.png differ diff --git a/images/ttdbg-code-lens.png b/images/ttdbg-code-lens.png new file mode 100644 index 0000000..b476a5d Binary files /dev/null and b/images/ttdbg-code-lens.png differ diff --git a/images/ttdbg-wfid-quick-pick.png b/images/ttdbg-wfid-quick-pick.png new file mode 100644 index 0000000..7e6d496 Binary files /dev/null and b/images/ttdbg-wfid-quick-pick.png differ diff --git a/package.json b/package.json index 3755a0f..b131b5f 100644 --- a/package.json +++ b/package.json @@ -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" ], @@ -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" }, { diff --git a/src/DebugProxy.ts b/src/DebugProxy.ts index a79ddda..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("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"); } @@ -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 1ac1dfc..15eacfe 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,28 +28,38 @@ 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, + ssl: { rejectUnauthorized: false } + }); 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 LIMIT 10', [wfName]); + 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 LIMIT 10', [wfid]); + 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}`); } return results.rows.length === 1 ? results.rows[0] : undefined; } diff --git a/src/cloudCli.ts b/src/cloudCli.ts new file mode 100644 index 0000000..879e127 --- /dev/null +++ b/src/cloudCli.ts @@ -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(folder: vscode.WorkspaceFolder, ...args: string[]): Promise { + 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(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.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 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(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 }); + } +} diff --git a/src/commands.ts b/src/commands.ts index 271b314..c86ba65 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,8 +2,8 @@ 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"; @@ -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 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, { @@ -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`); @@ -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 => { 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 1e54ca7..caf95e4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; -import { SpawnOptions, spawn as cpSpawn } from "child_process"; import { ClientConfig } from 'pg'; -import { execFile, exists } from './utils'; +import { exists, isExecFileError } from './utils'; import { logger } from './extension'; +import { dbos_cloud_app_status, dbos_cloud_db_status, dbos_cloud_login } from './cloudCli'; const TTDBG_CONFIG_SECTION = "dbos-ttdbg"; const PROV_DB_HOST = "prov_db_host"; @@ -11,133 +11,7 @@ const PROV_DB_DATABASE = "prov_db_database"; const PROV_DB_USER = "prov_db_user"; const DEBUG_PROXY_PORT = "debug_proxy_port"; -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(folder: vscode.WorkspaceFolder, ...args: string[]): Promise { - const { stdout } = await execFile("npx", ["dbos-cloud", ...args, "--json"], { - cwd: folder.uri.fsPath, - }); - return JSON.parse(stdout) as T; -} - -async function dbos_cloud_app_status(folder: vscode.WorkspaceFolder) { - return dbos_cloud_cli(folder, "application", "status"); -} - -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 }); - - 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 regexSuccessfulLogin = /Successfully logged in as (.*)!/; - - try { - const ctsPromise = new Promise(resolve => { - 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.stdout.on("data", async (buffer: Buffer) => { - const data = buffer.toString().trim(); - logger.info("dbos-cloud login stdout on data", { data }); - - const loginUrlMatch = regexLoginUrl.exec(data); - if (loginUrlMatch && loginUrlMatch.length === 3) { - const [, loginUrl, userCode] = loginUrlMatch; - logger.info("dbos-cloud login url", { 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-cloud login successful", { 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.info("dbos_cloud_login exit", { killed, killResult }); - } -} - -interface ExecFileError { - cmd: string; - code: number; - killed: boolean; - stdout: string; - stderr: string; - message: string; - stack: string; -} - -function isExecFileError(e: unknown): e is ExecFileError { - if (e instanceof Error) { - return "stdout" in e && "stderr" in e && "cmd" in e; - } - return false; -} - -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); @@ -148,15 +22,16 @@ async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise user: db.AdminUsername }; } catch (e) { - if (isExecFileError(e) && e.stdout.includes("Error: not logged in")) { - return undefined; - } else { - throw e; + if (isExecFileError(e)) { + if (e.stdout.includes("Error: not logged in") || e.stdout.includes("Error: Login expired")) { + return undefined; + } } + throw e; } } -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); @@ -176,25 +51,41 @@ async function startInvalidCredentialsFlow(folder: vscode.WorkspaceFolder): Prom const credentialsPath = vscode.Uri.joinPath(folder.uri, ".dbos", "credentials"); const credentialsExists = await exists(credentialsPath); - // TODO: Register support - const result = await vscode.window.showWarningMessage( - "Invalid DBOS Cloud credentials", - "Login", "Cancel"); + const message = credentialsExists + ? "DBOS Cloud credentials have expired. Please login again." + : "You need to login to DBOS Cloud."; - if (result === "Login") { - await dbos_cloud_login(folder); + const items = ["Login", "Cancel"]; + + // TODO: Register support + // if (!credentialsExists) { items.unshift("Register"); } + + const result = await vscode.window.showWarningMessage(message, ...items); + switch (result) { + // case "Register": break; + 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; @@ -211,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)); @@ -228,18 +118,17 @@ 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) { password = await vscode.window.showInputBox({ - prompt: "Enter provenance database password", + prompt: "Enter application database password", password: true, }); - if (!password) { - throw new Error('Provenance database password is required'); + if (password) { + await this.secrets.store(passwordKey, password); } - await this.secrets.store(passwordKey, password); } return password; } @@ -251,3 +140,4 @@ export class Configuration { } } } + diff --git a/src/utils.ts b/src/utils.ts index 6ff5065..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}`) @@ -70,16 +71,16 @@ export interface QuickPickOptions { placeHolder?: string; } -export type QuickPickResult = vscode.QuickPickItem | vscode.QuickInputButton; +export type QuickPickResult = vscode.QuickPickItem | vscode.QuickInputButton | undefined; -export function isQuickPickItem(item?: QuickPickResult): item is vscode.QuickPickItem { +export function isQuickPickItem(item: QuickPickResult): item is vscode.QuickPickItem { return item !== undefined && "label" in item; } export async function showQuickPick(options: QuickPickOptions) { const disposables: { dispose(): any }[] = []; try { - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const input = vscode.window.createQuickPick(); input.title = options.title; input.placeholder = options.placeHolder; @@ -111,3 +112,20 @@ export async function showQuickPick(options: QuickPickOptions) { disposables.forEach(d => d.dispose()); } } + +export interface ExecFileError { + cmd: string; + code: number; + killed: boolean; + stdout: string; + stderr: string; + message: string; + stack: string; +} + +export function isExecFileError(e: unknown): e is ExecFileError { + if (e instanceof Error) { + return "stdout" in e && "stderr" in e && "cmd" in e; + } + return false; +} \ No newline at end of file diff --git a/version.json b/version.json index 15fef15..2464712 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.6-preview", + "version": "0.9-preview", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/dev$",