Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 92 additions & 234 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -871,11 +871,13 @@
"@deephaven-enterprise/auth-nodejs": "^1.20240723.124-beta",
"@deephaven-enterprise/query-utils": "^1.20240723.124-beta",
"@deephaven/jsapi-nodejs": "^0.104.0",
"archiver": "^7.0.1",
"nanoid": "^5.0.7"
},
"devDependencies": {
"@deephaven-enterprise/jsapi-types": "^1.20240723.124-beta",
"@deephaven/jsapi-types": "^1.0.0-dev0.37.3",
"@types/archiver": "^6.0.3",
"@types/node": "22.5.4",
"@types/vscode": "^1.91.0",
"@types/ws": "^8.5.10",
Expand Down
105 changes: 78 additions & 27 deletions src/controllers/ExtensionController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import * as os from 'os';
import type { dh as DhcType } from '@deephaven/jsapi-types';
import type { EnterpriseDhType as DheType } from '@deephaven-enterprise/jsapi-types';
import {
Expand Down Expand Up @@ -32,10 +33,13 @@ import {
getTempDir,
isInstanceOf,
isSupportedLanguageId,
LogFileHandler,
Logger,
OutputChannelWithHistory,
sanitizeGRPCLogMessageArgs,
saveLogFiles,
Toaster,
uniqueId,
} from '../util';
import {
RunCommandCodeLensProvider,
Expand Down Expand Up @@ -79,6 +83,7 @@ import type {
CoreUnauthenticatedClient,
ConnectionState,
WorkerURL,
UniqueID,
} from '../types';
import { ServerConnectionTreeDragAndDropController } from './ServerConnectionTreeDragAndDropController';
import { ConnectionController } from './ConnectionController';
Expand All @@ -92,34 +97,38 @@ export class ExtensionController implements Disposable {
constructor(context: vscode.ExtensionContext, configService: IConfigService) {
this._context = context;
this._config = configService;
this._instanceId = uniqueId(8);
this._version = this._context.extension.packageJSON.version;

const envInfo = {
/* eslint-disable @typescript-eslint/naming-convention */
'VS Code version': vscode.version,
'Deephaven Extension version': this._version,
'Deephaven Extension instanceId': this._instanceId,
'Electron version': process.versions.electron,
'Chromium version': process.versions.chrome,
'Node version': process.versions.node,
/* eslint-enable @typescript-eslint/naming-convention */
platform: os.platform(),
release: os.release(),
arch: os.arch(),
version: os.version(),
totalmem: os.totalmem(),
freemem: os.freemem(),
cpus: os.cpus(),
uptime: os.uptime(),
};

this.initializeDiagnostics();
this.initializeConfig();
this.initializeSecrets();
this.initializeCodeLenses();
this.initializeHoverProviders();
this.initializeMessaging();
this.initializeServerManager();
this.initializeTempDirectory();
this.initializeConnectionController();
this.initializePanelController();
this.initializePipServerController();
this.initializeUserLoginController();
this.initializeCommands();
this.initializeWebViews();
this.initializeServerUpdates();

this._context.subscriptions.push(NodeHttp2gRPCTransport);

const version = context.extension.packageJSON.version;
const message = `Deephaven extension ${version} activated`;

logger.info(message);
this._outputChannel?.appendLine(message);
this._envInfoText = Object.entries(envInfo)
.map(([key, value]) => `\t${key}: ${JSON.stringify(value)}`)
.join('\n');
}

readonly _context: vscode.ExtensionContext;
readonly _config: IConfigService;
private readonly _context: vscode.ExtensionContext;
private readonly _config: IConfigService;
private readonly _instanceId: UniqueID;
private readonly _version: string;
private readonly _envInfoText: string;

private _connectionController: ConnectionController | null = null;
private _coreClientCache: URLMap<
Expand All @@ -132,6 +141,7 @@ export class ExtensionController implements Disposable {
null;
private _dheClientFactory: IDheClientFactory | null = null;
private _dheServiceCache: IAsyncCacheService<URL, IDheService> | null = null;
private _logFileHandler: LogFileHandler | null = null;
private _panelController: PanelController | null = null;
private _panelService: IPanelService | null = null;
private _pipServerController: PipServerController | null = null;
Expand Down Expand Up @@ -162,6 +172,38 @@ export class ExtensionController implements Disposable {

async dispose(): Promise<void> {}

activate = (): void => {
this.initializeMessaging();

logger.info(`Activating Deephaven extension\n${this._envInfoText}`);

this.initializeDiagnostics();
this.initializeConfig();
this.initializeSecrets();
this.initializeCodeLenses();
this.initializeHoverProviders();
this.initializeServerManager();
this.initializeTempDirectory();
this.initializeConnectionController();
this.initializePanelController();
this.initializePipServerController();
this.initializeUserLoginController();
this.initializeCommands();
this.initializeWebViews();
this.initializeServerUpdates();

this._context.subscriptions.push(NodeHttp2gRPCTransport);

const message = `Activated Deephaven Extension.`;

logger.info(message);
this._outputChannel?.appendLine(message);
};

deactivate = (): void => {
logger.info(`Deactivating Deephaven extension`);
};

/**
* Initialize code lenses for running Deephaven code.
*/
Expand Down Expand Up @@ -321,6 +363,9 @@ export class ExtensionController implements Disposable {
Logger.addConsoleHandler();
Logger.addOutputChannelHandler(this._outputChannelDebug);

this._logFileHandler = new LogFileHandler(this._instanceId, this._context);
Logger.handlers.add(this._logFileHandler);

const gRPCOutputChannelHandler = Logger.createOutputChannelHandler(
this._outputChannelDebug
);
Expand All @@ -332,6 +377,7 @@ export class ExtensionController implements Disposable {
this._toaster = new Toaster();

this._context.subscriptions.push(
this._logFileHandler,
this._outputChannel,
this._outputChannelDebug
);
Expand Down Expand Up @@ -695,14 +741,19 @@ export class ExtensionController implements Disposable {
* Handle download logs command
*/
onDownloadLogs = async (): Promise<void> => {
assertDefined(this._logFileHandler, 'logFileHandler');
assertDefined(this._outputChannelDebug, 'outputChannelDebug');
assertDefined(this._toaster, 'toaster');

const uri = await this._outputChannelDebug.downloadHistoryToFile();
const uri = await saveLogFiles(this._logFileHandler.logDirectory);

if (uri != null) {
this._toaster.info(`Downloaded logs to ${uri.fsPath}`);
vscode.window.showTextDocument(uri);

await vscode.commands.executeCommand(
'revealFileInOS',
vscode.Uri.parse(uri.fsPath)
);
}
};

Expand Down
11 changes: 8 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import * as vscode from 'vscode';
import { ExtensionController } from './controllers';
import { ConfigService } from './services';

export function activate(context: vscode.ExtensionContext): void {
const controller = new ExtensionController(context, ConfigService);
let controller: ExtensionController | undefined;

export function activate(context: vscode.ExtensionContext): void {
controller = new ExtensionController(context, ConfigService);
context.subscriptions.push(controller);

controller.activate();
}

export function deactivate(): void {}
export function deactivate(): void {
controller?.deactivate();
}
63 changes: 63 additions & 0 deletions src/util/Logger.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,74 @@
import * as vscode from 'vscode';
import fs from 'node:fs';
import path from 'node:path';
import type { Disposable, UniqueID } from '../types';
import { getFilePathDateToken } from './dataUtils';

export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'debug2';

export type LogLevelHandler = (label: string, ...args: unknown[]) => void;

export type LogHandler = Record<LogLevel, LogLevelHandler>;

/**
* Log handler that writes logs to a file.
*/
export class LogFileHandler implements LogHandler, Disposable {
constructor(extensionInstanceId: UniqueID, context: vscode.ExtensionContext) {
// VS Code specifies the log directory but doesn't create it
fs.mkdirSync(context.logUri.fsPath, { recursive: true });

this._extensionInstanceId = extensionInstanceId;
this.logDirectory = context.logUri.fsPath;
}

private readonly _extensionInstanceId: UniqueID;
private _lastLogBucket: string | null = null;
private _logWriter: fs.WriteStream | null = null;

readonly logDirectory: string;

private _getLogFilePath = (isoDateStr: string): string => {
const dateToken = getFilePathDateToken(isoDateStr);

const logFileName = `deephaven-vscode_${dateToken}_${this._extensionInstanceId}.log`;
return path.join(this.logDirectory, logFileName);
};

private _write = (
level: LogLevel,
label: string,
...args: unknown[]
): void => {
const now = new Date().toISOString();
const hourBucket = now.substring(0, 13);

// Initialize log writer or update it if the hour bucket has changed
if (this._logWriter == null || this._lastLogBucket !== hourBucket) {
this._logWriter?.end();
this._lastLogBucket = hourBucket;

const logFilePath = this._getLogFilePath(now);
this._logWriter = fs.createWriteStream(logFilePath);
}

this._logWriter.write(
`[${now}] ${label} ${level.toUpperCase()}: ${args.map(Logger.stringifyArg).join(' ')}\n`
);
};

error = this._write.bind(this, 'error');
warn = this._write.bind(this, 'warn');
info = this._write.bind(this, 'info');
debug = this._write.bind(this, 'debug');
debug2 = this._write.bind(this, 'debug2');

dispose = async (): Promise<void> => {
this._logWriter?.end();
this._logWriter = null;
};
}

/**
* Simple logger delegate that can be used to log messages to a set of handlers.
* Messages will include a label for scoping log messages.
Expand Down
16 changes: 16 additions & 0 deletions src/util/dataUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import type { NonEmptyArray } from '../types';

/**
* Returns a date string formatted for use in a file path.
* The format is YYYYMMDDTHHMMSSZ.
* @param dateOrIsoString A Date object or an ISO 8601 date string.
* @returns A string formatted for use in a file path.
*/
export function getFilePathDateToken(
dateOrIsoString: Date | string = new Date()
): string {
if (dateOrIsoString instanceof Date) {
dateOrIsoString = dateOrIsoString.toISOString();
}

return `${dateOrIsoString.substring(0, 19).replace(/[:-]/g, '')}Z`;
}

/**
* Type guard to determine if an object has a property.
* @param obj The object to check.
Expand Down
8 changes: 6 additions & 2 deletions src/util/idUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { nanoid } from 'nanoid';
import { customAlphabet, urlAlphabet } from 'nanoid';
import type { UniqueID } from '../types';

// The default nanoid alphabet includes `_`. Using custom alphabet without it
// to make ids group better in `_` delimited strings.
const nanoidCustom = customAlphabet(urlAlphabet.replace('_', ''), 21);

/**
* Generate a unique id.
* @param size The size of the id to generate. Defaults to 21 since that is what
* nanoid uses as its default.
*/
export function uniqueId(size: number = 21): UniqueID {
return nanoid(size) as UniqueID;
return nanoidCustom(size) as UniqueID;
}
Loading
Loading