Skip to content

Commit

Permalink
Launch login flow if retrieving db config fails
Browse files Browse the repository at this point in the history
  • Loading branch information
devhawk committed Feb 24, 2024
1 parent 66f7879 commit af3f756
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 72 deletions.
7 changes: 6 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ export const deleteProvDbPasswordsCommandName = "dbos-ttdbg.delete-prov-db-passw
export async function startDebugging(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) {
try {
const clientConfig = await config.getProvDbConfig(folder);
const statuses = await provDB.getWorkflowStatuses(clientConfig, name, $type);
if (!clientConfig) { return; }

const statuses = await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Window },
() => { return 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"
Expand Down
169 changes: 98 additions & 71 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { spawn } from "child_process";
import { SpawnOptions, spawn as cpSpawn } from "child_process";
import { ClientConfig } from 'pg';
import { execFile } from './utils';
import { execFile, exists } from './utils';
import { logger } from './extension';

const TTDBG_CONFIG_SECTION = "dbos-ttdbg";
Expand Down Expand Up @@ -43,78 +43,73 @@ async function dbos_cloud_db_status(folder: vscode.WorkspaceFolder, databaseName
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 proc = spawn(
"npx",
["dbos-cloud", "login" ],
{ cwd: 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 killEvent = new vscode.EventEmitter<void>();
const killEventPromise = new Promise<void>(resolve => {
killEvent.event(() => { resolve(); });
const ctsPromise = new Promise<void>(resolve => {
cts.token.onCancellationRequested(() => resolve());
});

proc.on("exit", (code, signal) => {
logger.debug("dbos_cloud on exit", { code, signal, killed: proc.killed });
killEvent.fire();
});
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(); });

proc.stdout.on("data", async (data: Buffer) => {
const $data = data.toString().trim();
logger.debug("dbos_cloud stdout on data", { data: $data });
loginProc.stdout.on("data", async (buffer: Buffer) => {
const data = buffer.toString().trim();
logger.info("dbos-cloud login stdout on data", { data });

const loginUrlMatch = /Login URL: (http.*\/activate\?user_code=([A-Z][A-Z][A-Z][A-Z]-[A-Z][A-Z][A-Z][A-Z]))/.exec($data);
const loginUrlMatch = regexLoginUrl.exec(data);
if (loginUrlMatch && loginUrlMatch.length === 3) {
const [, loginUri, userCode] = loginUrlMatch;
logger.info("dbos_cloud Login URL", { loginUri, userCode });

const result = await vscode.window.showInformationMessage(`Login to DBOS Cloud using user code: ${userCode}?`, "Login via Browser", "Cancel");
if (result === "Login via Browser") {
logger.info("dbos_cloud Login via Browser", { loginUri, userCode });

const openResult = await vscode.env.openExternal(vscode.Uri.parse(loginUri));
if (openResult) {
await vscode.window.withProgress({
cancellable: true,
location: vscode.ProgressLocation.Notification,
title: `Logging into DBOS Cloud with code ${userCode}...`
}, async (_, token) => {
logger.info("dbos_cloud login cancelled", { loginUri, result: openResult });
token.onCancellationRequested(() => killEvent.fire());
await killEventPromise;
});
} else {
logger.error("dbos_cloud failed to open external browser", { loginUri });
killEvent.fire();
vscode.window.showErrorMessage(`Failed to open external browser for ${loginUri}`);
}
} else {
logger.info("dbos_cloud login cancelled", { loginUri, result });
killEvent.fire();
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 = /Successfully logged in as (.*)!/.exec($data);
const successfulLoginMatch = regexSuccessfulLogin.exec(data);
if (successfulLoginMatch && successfulLoginMatch.length === 2) {
const [, user] = successfulLoginMatch;
logger.info("dbos_cloud Login Successfully", { user });
vscode.window.showInformationMessage(`Successfully logged in as ${user}`);
killEvent.fire();
logger.info("dbos-cloud login successful", { user });
vscode.window.showInformationMessage(`Successfully logged in to DBOS Cloud as ${user}`);
}
});

await killEventPromise;
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 {
proc.stdout.removeAllListeners();
proc.stderr.removeAllListeners();
proc.removeAllListeners();
proc.kill();
logger.info("dbos_cloud_login exit", { killed: proc.killed });
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 });
}
}

Expand All @@ -135,7 +130,14 @@ function isExecFileError(e: unknown): e is ExecFileError {
return false;
}

async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
interface DatabaseConfig {
host: string | undefined;
port: number | undefined;
database: string | undefined;
user: string | undefined;
}

async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise<DatabaseConfig | undefined> {
try {
const app = await dbos_cloud_app_status(folder);
const db = await dbos_cloud_db_status(folder, app.PostgresInstanceName);
Expand All @@ -146,19 +148,15 @@ async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise
user: db.AdminUsername
};
} catch (e) {
if (isExecFileError(e)) {
if (e.stdout.trim().endsWith("Error: not logged in")) {
// TODO: initiate login
vscode.window.showErrorMessage("Not logged in to DBOS Cloud");
}
if (isExecFileError(e) && e.stdout.includes("Error: not logged in")) {
return undefined;
} else {
throw e;
}

logger.error("getDbosCloudInfo", e);
return {};
}
}

async function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): DatabaseConfig {
const cfg = vscode.workspace.getConfiguration(TTDBG_CONFIG_SECTION, folder);

const host = cfg.get<string>(PROV_DB_HOST);
Expand All @@ -174,19 +172,48 @@ async function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): Prom
};
}

async function startInvalidCredentialsFlow(folder: vscode.WorkspaceFolder): Promise<void> {
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");

if (result === "Login") {
await dbos_cloud_login(folder);
}
}

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

async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig | undefined> {
const dbConfig = await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Window },
async () => {
const cloudConfig = await getDbConfigFromDbosCloud(folder);
const localConfig = getDbConfigFromVSCodeConfig(folder);

const host = localConfig?.host ?? cloudConfig?.host;
const port = localConfig?.port ?? cloudConfig?.port ?? 5432;
const database = localConfig?.database ?? cloudConfig?.database;
const user = localConfig?.user ?? cloudConfig?.user;

const cloudConfig = await getDbConfigFromDbosCloud(folder);
const localConfig = await getDbConfigFromVSCodeConfig(folder);
return { host, port, database, user };
});

if (!dbConfig.host || !dbConfig.database || !dbConfig.user) {
startInvalidCredentialsFlow(folder).catch(e => logger.error("startInvalidCredentialsFlow", e));
return undefined;
}

return {
host: localConfig.host ?? cloudConfig.host,
port: localConfig.port ?? cloudConfig.port,
database: localConfig.database ?? cloudConfig.database,
user: localConfig.user ?? cloudConfig.user,
host: dbConfig.host,
port: dbConfig.port,
database: dbConfig.database,
user: dbConfig.user,
password: () => this.#getPassword(folder),
ssl: {
rejectUnauthorized: false,
Expand Down

0 comments on commit af3f756

Please sign in to comment.