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

Assorted Cleanup #9

Merged
merged 14 commits into from
Feb 28, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
{
"command": "dbos-ttdbg.delete-prov-db-passwords",
"title": "Delete Stored Provenance DB Passwords",
"title": "Delete Stored Application Database Passwords",
"category": "DBOS"
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/DebugProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class DebugProxy {
if (typeof password === "function") {
password = await password();
if (!password) {
throw new Error("Provenance database password is required");
throw new Error("Application database password is required");
}
}
if (!host || !database || !user || !password) {
Expand Down
4 changes: 2 additions & 2 deletions src/ProvenanceDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ export class ProvenanceDatabase {
async getWorkflowStatuses(clientConfig: ClientConfig, 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> {
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
104 changes: 104 additions & 0 deletions src/cloudCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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.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<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 regexSuccessfulLogin = /Successfully logged in as (.*)!/;
devhawk marked this conversation as resolved.
Show resolved Hide resolved

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.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 });
}
}
6 changes: 2 additions & 4 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { dbos_cloud_login } from './cloudCli';
import { ClientConfig } from 'pg';

export const cloudLoginCommandName = "dbos-ttdbg.cloud-login";
Expand All @@ -28,7 +28,7 @@ async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (cl

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

Expand All @@ -49,8 +49,6 @@ async function startDebugging(folder: vscode.WorkspaceFolder, getWorkflowID: (cl
}
}
);


} catch (e) {
logger.error("startDebugging", e);
vscode.window.showErrorMessage(`Failed to start debugging`);
Expand Down
157 changes: 24 additions & 133 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,125 +11,6 @@ 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<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;
}

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

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 });

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 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.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;
Expand All @@ -148,11 +29,12 @@ 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;
}
}

Expand All @@ -176,13 +58,21 @@ 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.";

const items = ["Login", "Cancel"];

if (result === "Login") {
await dbos_cloud_login(folder);
// 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;
}
}

Expand Down Expand Up @@ -233,11 +123,11 @@ export class Configuration {
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');
throw new Error('Application database password is required');
}
await this.secrets.store(passwordKey, password);
}
Expand All @@ -251,3 +141,4 @@ export class Configuration {
}
}
}

Loading