Skip to content

Commit

Permalink
ProvenanceDatabaseConfig
Browse files Browse the repository at this point in the history
  • Loading branch information
devhawk committed Feb 28, 2024
1 parent 6ad1b91 commit 770bb61
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 56 deletions.
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("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");
}
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
23 changes: 16 additions & 7 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,26 +28,35 @@ 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,
});
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 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', [wfid]);
if (results.rows.length > 1) { throw new Error(`Multiple workflow status records found for workflow ID ${wfid}`); }
Expand Down
22 changes: 12 additions & 10 deletions src/cloudCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,42 @@ async function dbos_cloud_cli<T>(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<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.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<string>();

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 {
const ctsPromise = new Promise<void>(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.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));
Expand All @@ -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}`);
}
});
Expand All @@ -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 });
}
}
35 changes: 23 additions & 12 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,48 @@ 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";
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`);
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 @@ -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 => <vscode.QuickPickItem>{
label: new Date(parseInt(s.created_at)).toLocaleString(),
description: `${s.authenticated_user.length === 0 ? "<anonymous>" : s.authenticated_user} (${s.status})`,
Expand Down
37 changes: 18 additions & 19 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DatabaseConfig | undefined> {
async function getProvDBConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise<ProvenanceDatabaseConfig | undefined> {
try {
const app = await dbos_cloud_app_status(folder);
const db = await dbos_cloud_db_status(folder, app.PostgresInstanceName);
Expand All @@ -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<string>(PROV_DB_HOST);
Expand Down Expand Up @@ -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<string | undefined>);
port?: number | undefined;
host?: string | undefined;
}

export class Configuration {
constructor(private readonly secrets: vscode.SecretStorage) { }

async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig | undefined> {
async getProvDBConfig(folder: vscode.WorkspaceFolder): Promise<ProvenanceDatabaseConfig | undefined> {
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;
Expand All @@ -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));
Expand All @@ -118,18 +118,17 @@ export class Configuration {
return `${TTDBG_CONFIG_SECTION}.prov_db_password.${folder.uri.fsPath}`;
}

async #getPassword(folder: vscode.WorkspaceFolder): Promise<string> {
async #getPassword(folder: vscode.WorkspaceFolder): Promise<string | undefined> {
const passwordKey = this.#getPasswordKey(folder);
let password = await this.secrets.get(passwordKey);
if (!password) {
password = await vscode.window.showInputBox({
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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -42,7 +43,7 @@ export async function exists(uri: vscode.Uri): Promise<boolean> {

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}`)
Expand Down

0 comments on commit 770bb61

Please sign in to comment.