1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 01:23:24 +00:00

Flight recorder

This commit is contained in:
Bernd Schoolmann
2025-11-12 13:59:53 +01:00
parent 275c6a93b4
commit 09da9f5d13
25 changed files with 176 additions and 45 deletions

View File

@@ -76,6 +76,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { FlightRecorderService } from "@bitwarden/logging";
import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
import { DeleteAccountComponent } from "../auth/delete-account.component";
@@ -197,6 +198,7 @@ export class AppComponent implements OnInit, OnDestroy {
private readonly tokenService: TokenService,
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
private readonly lockService: LockService,
private readonly flightRecorderService: FlightRecorderService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
@@ -271,7 +273,7 @@ export class AppComponent implements OnInit, OnDestroy {
if (
message.userId == null ||
message.userId ===
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))))
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))))
) {
await this.router.navigate(["lock"]);
}
@@ -314,8 +316,8 @@ export class AppComponent implements OnInit, OnDestroy {
if (publicKey == null) {
this.logService.error(
"[AppComponent] No public key available for the user: " +
activeUserId +
" fingerprint can't be displayed.",
activeUserId +
" fingerprint can't be displayed.",
);
} else {
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
@@ -729,7 +731,7 @@ export class AppComponent implements OnInit, OnDestroy {
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
this.authService.logOut(async () => {}, userBeingLoggedOut);
this.authService.logOut(async () => { }, userBeingLoggedOut);
}
private async recordActivity() {

View File

@@ -56,4 +56,4 @@ import { SharedModule } from "./shared/shared.module";
providers: [SshAgentService],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule { }

View File

@@ -110,6 +110,7 @@ import {
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { FlightRecorderService } from "@bitwarden/logging";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
@@ -136,6 +137,7 @@ import { ElectronRendererMessageSender } from "../../platform/services/electron-
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
import { RendererFlightRecorderService } from "../../platform/services/renderer-flight-recorder.service";
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
import { BiometricMessageHandlerService } from "../../services/biometric-message-handler.service";
@@ -168,6 +170,11 @@ const safeProviders: SafeProvider[] = [
useClass: RendererBiometricsService,
deps: [],
}),
safeProvider({
provide: FlightRecorderService,
useClass: RendererFlightRecorderService,
deps: [],
}),
safeProvider(NativeMessagingService),
safeProvider(BiometricMessageHandlerService),
safeProvider(SearchBarService),
@@ -185,7 +192,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: LogServiceAbstraction,
useClass: ElectronLogRendererService,
deps: [],
deps: [FlightRecorderService],
}),
safeProvider({
provide: PlatformUtilsServiceAbstraction,
@@ -488,4 +495,4 @@ const safeProviders: SafeProvider[] = [
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders,
})
export class ServicesModule {}
export class ServicesModule { }

View File

@@ -22,6 +22,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { DefaultBiometricStateService } from "@bitwarden/key-management";
import { FlightRecorderService, MemoryFlightRecorderService } from "@bitwarden/logging";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
import {
DefaultActiveUserStateProvider,
@@ -56,12 +57,14 @@ import { ElectronLogMainService } from "./platform/services/electron-log.main.se
import { ElectronStorageService } from "./platform/services/electron-storage.service";
import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service";
import { I18nMainService } from "./platform/services/i18n.main.service";
import { MainFlightRecorderService } from "./platform/services/main-flight-recorder.service";
import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service";
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
import { MainSdkLoadService } from "./services/main-sdk-load-service";
import { isMacAppStore } from "./utils";
export class Main {
flightRecorder: FlightRecorderService;
logService: ElectronLogMainService;
i18nService: I18nMainService;
storageService: ElectronStorageService;
@@ -129,7 +132,8 @@ export class Main {
});
}
this.logService = new ElectronLogMainService(null, app.getPath("userData"));
this.flightRecorder = new MainFlightRecorderService();
this.logService = new ElectronLogMainService(null, app.getPath("userData"), this.flightRecorder);
const storageDefaults: any = {};
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
@@ -146,7 +150,7 @@ export class Main {
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
this.sdkLoadService = new MainSdkLoadService();
this.sdkLoadService = new MainSdkLoadService(this.flightRecorder);
this.mainCryptoFunctionService = new NodeCryptoFunctionService();

View File

@@ -4,6 +4,7 @@ import { MenuItemConstructorOptions } from "electron";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { FlightRecorderService } from "@bitwarden/logging";
import { isDev } from "../../utils";
@@ -30,6 +31,7 @@ export class ViewMenu implements IMenubarMenu {
this.toggleFullscreen,
this.separator,
this.reload,
this.flightRecorder,
];
if (isDev()) {
@@ -134,6 +136,14 @@ export class ViewMenu implements IMenubarMenu {
};
}
private get flightRecorder(): MenuItemConstructorOptions {
return {
id: "flightRecorder",
label: "Flight recorder",
click: () => this.sendMessage("openFlightRecorder"),
};
}
private localize(s: string) {
return this._i18nService.t(s);
}

View File

@@ -3,7 +3,7 @@
import * as fs from "fs";
import * as path from "path";
import { app, ipcMain } from "electron";
import { app, dialog, ipcMain } from "electron";
import { firstValueFrom } from "rxjs";
import { autostart } from "@bitwarden/desktop-napi";
@@ -22,7 +22,7 @@ export class MessagingMain {
constructor(
private main: Main,
private desktopSettingsService: DesktopSettingsService,
) {}
) { }
async init() {
this.scheduleNextSync();
@@ -89,6 +89,18 @@ export class MessagingMain {
case "getWindowIsFocused":
this.windowIsFocused();
break;
case "openFlightRecorder": {
const records = (await this.main.flightRecorder.getRecords()).join("\n");
const result = await dialog.showSaveDialog(this.main.windowMain.win, {
title: "Save Flight Recorder Log",
defaultPath: `bitwarden_flight_recorder_${new Date().toISOString().replace(/[:.]/g, "-")}.log`,
filters: [{ name: "Log Files", extensions: ["log", "txt"] }],
});
if (!result.canceled && result.filePath) {
fs.writeFileSync(result.filePath, records, { encoding: "utf-8" });
}
break;
}
default:
break;
}
@@ -129,7 +141,7 @@ export class MessagingMain {
private addOpenAtLogin() {
if (process.platform === "linux") {
if (isFlatpak()) {
autostart.setAutostart(true, []).catch((e) => {});
autostart.setAutostart(true, []).catch((e) => { });
} else {
const data = `[Desktop Entry]
Type=Application
@@ -154,7 +166,7 @@ export class MessagingMain {
private removeOpenAtLogin() {
if (process.platform === "linux") {
if (isFlatpak()) {
autostart.setAutostart(false, []).catch((e) => {});
autostart.setAutostart(false, []).catch((e) => { });
} else {
if (fs.existsSync(this.linuxStartupFile())) {
fs.unlinkSync(this.linuxStartupFile());

View File

@@ -49,7 +49,7 @@ export class WindowMain {
private desktopSettingsService: DesktopSettingsService,
private argvCallback: (argv: string[]) => void = null,
private createWindowCallback: (win: BrowserWindow) => void,
) {}
) { }
init(): Promise<any> {
// Perform a hard reload of the render process by crashing it. This is suboptimal but ensures that all memory gets
@@ -267,6 +267,7 @@ export class WindowMain {
this.enableAlwaysOnTop = await firstValueFrom(this.desktopSettingsService.alwaysOnTop$);
this.session = session.fromPartition("persist:bitwarden", { cache: false });
console.log("IS DEV", isDev());
// Create the browser window.
this.win = new BrowserWindow({
@@ -350,6 +351,7 @@ export class WindowMain {
}
// Open the DevTools.
console.log("IS DEV1", isDev());
if (isDev()) {
this.win.webContents.openDevTools();
}

View File

@@ -68,6 +68,11 @@ const sshAgent = {
},
};
const flightRecorder = {
write: (message: string) => ipcRenderer.invoke("flightrecorder.write", message),
getRecords: (): Promise<string[]> => ipcRenderer.invoke("flightrecorder.getRecords"),
};
const powermonitor = {
isLockMonitorAvailable: (): Promise<boolean> =>
ipcRenderer.invoke("powermonitor.isLockMonitorAvailable"),
@@ -185,6 +190,7 @@ export default {
crypto,
ephemeralStore,
localhostCallbackService,
flightRecorder,
};
function deviceType(): DeviceType {

View File

@@ -8,6 +8,7 @@ import log from "electron-log/main";
import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum";
import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service";
import { logging } from "@bitwarden/desktop-napi";
import { FlightRecorderService } from "@bitwarden/logging";
import { isDev } from "../../utils";
@@ -15,6 +16,7 @@ export class ElectronLogMainService extends BaseLogService {
constructor(
protected filter: (level: LogLevelType) => boolean = null,
private logDir: string = null,
private flightRecorder: FlightRecorderService = null
) {
super(isDev(), filter);
@@ -61,6 +63,11 @@ export class ElectronLogMainService extends BaseLogService {
return;
}
console.log("ElectronLogMainService.write:", this.flightRecorder, message, ...optionalParams);
if (this.flightRecorder != null) {
this.flightRecorder.write(`[TS] [${LogLevelType[level]}] ${message}`).then(() => { }).catch(() => { });
}
switch (level) {
case LogLevelType.Debug:
log.debug(message, ...optionalParams);

View File

@@ -4,8 +4,8 @@ import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.en
import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service";
export class ElectronLogRendererService extends BaseLogService {
constructor(protected filter: (level: LogLevelType) => boolean = null) {
super(ipc.platform.isDev, filter);
constructor() {
super(ipc.platform.isDev, null, null);
}
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {

View File

@@ -0,0 +1,18 @@
import { ipcMain } from "electron";
import { MemoryFlightRecorderService } from "@bitwarden/logging";
export class MainFlightRecorderService extends MemoryFlightRecorderService {
constructor() {
super();
ipcMain.handle("flightrecorder.write", async (_event, message: string) => {
console.log("MainFlightRecorderService.write:", message);
await this.write(message);
});
ipcMain.handle("flightrecorder.getRecords", async () => {
console.log("MainFlightRecorderService.getRecords");
return await this.getRecords();
});
}
}

View File

@@ -0,0 +1,13 @@
import { FlightRecorderService } from "@bitwarden/logging";
export class RendererFlightRecorderService extends FlightRecorderService {
async write(message: string): Promise<void> {
console.log("RendererFlightRecorderService.write:", message);
await ipc.platform.flightRecorder.write(message);
}
async getRecords(): Promise<string[]> {
return await ipc.platform.flightRecorder.getRecords();
}
}

View File

@@ -1,9 +1,19 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { FlightRecorderService } from "@bitwarden/logging";
import * as sdk from "@bitwarden/sdk-internal";
export class MainSdkLoadService extends SdkLoadService {
constructor(private flightRecorder: FlightRecorderService) {
super();
}
async load(): Promise<void> {
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
(sdk as any).init(module);
}
protected log(message: string): void {
this.flightRecorder.write(message).then(() => { }).catch(() => { });
}
}

View File

@@ -118,6 +118,7 @@ import {
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { FlightRecorderService } from "@bitwarden/logging";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
@@ -371,7 +372,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: SdkLoadService,
useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService,
deps: [],
deps: [FlightRecorderService],
}),
safeProvider({
provide: SdkClientFactory,

View File

@@ -25,17 +25,6 @@ export class WebIpcService extends IpcService {
this.communicationBackend = new IpcCommunicationBackend({
async send(message: OutgoingMessage): Promise<void> {
if (message.destination === "BrowserBackground") {
window.postMessage(
{
type: "bitwarden-ipc-message",
message: {
destination: message.destination,
payload: [...message.payload],
topic: message.topic,
},
} satisfies IpcMessage,
window.location.origin,
);
return;
}
@@ -70,7 +59,7 @@ export class WebIpcService extends IpcService {
);
});
await super.initWithClient(new IpcClient(this.communicationBackend));
//await super.initWithClient(new IpcClient(this.communicationBackend));
if (this.platformUtilsService.isDev()) {
await ipcRegisterDiscoverHandler(this.client, {

View File

@@ -1,4 +1,5 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { FlightRecorderService } from "@bitwarden/logging";
import * as sdk from "@bitwarden/sdk-internal";
// https://stackoverflow.com/a/47880734
@@ -19,6 +20,10 @@ const supported = (() => {
})();
export class WebSdkLoadService extends SdkLoadService {
constructor(private flightRecorderService: FlightRecorderService) {
super();
}
async load(): Promise<void> {
let module: any;
if (supported) {
@@ -28,4 +33,8 @@ export class WebSdkLoadService extends SdkLoadService {
}
(sdk as any).init(module);
}
protected log(message: string): void {
this.flightRecorderService.write("[SDK] " + message);
}
}

View File

@@ -345,6 +345,7 @@ import {
UserAsymmetricKeysRegenerationApiService,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";
import { FlightRecorderService, MemoryFlightRecorderService } from "@bitwarden/logging";
import {
ActiveUserStateProvider,
DerivedStateProvider,
@@ -455,11 +456,11 @@ const safeProviders: SafeProvider[] = [
provide: LOGOUT_CALLBACK,
useFactory:
(messagingService: MessagingServiceAbstraction) =>
async (logoutReason: LogoutReason, userId?: string) => {
return Promise.resolve(
messagingService.send("logout", { logoutReason: logoutReason, userId: userId }),
);
},
async (logoutReason: LogoutReason, userId?: string) => {
return Promise.resolve(
messagingService.send("logout", { logoutReason: logoutReason, userId: userId }),
);
},
deps: [MessagingServiceAbstraction],
}),
safeProvider({
@@ -658,8 +659,8 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: LogService,
useFactory: () => new ConsoleLogService(process.env.NODE_ENV === "development"),
deps: [],
useFactory: (flightRecorderService: FlightRecorderService) => new ConsoleLogService(process.env.NODE_ENV === "development", null, flightRecorderService),
deps: [FlightRecorderService],
}),
safeProvider({
provide: CollectionService,
@@ -1755,4 +1756,4 @@ const safeProviders: SafeProvider[] = [
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
providers: safeProviders,
})
export class JslibServicesModule {}
export class JslibServicesModule { }

View File

@@ -102,10 +102,10 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM22887_RiskInsightsActivityTab]: FALSE,
/* Vault */
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
[FeatureFlag.CipherKeyEncryption]: true,
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: true,
[FeatureFlag.PM22134SdkCipherListView]: true,
[FeatureFlag.PM22136_SdkCipherEncryption]: true,
[FeatureFlag.AutofillConfirmation]: FALSE,
[FeatureFlag.RiskInsightsForPremium]: FALSE,

View File

@@ -1,4 +1,4 @@
import { init_sdk } from "@bitwarden/sdk-internal";
import { init_sdk, LogLevel } from "@bitwarden/sdk-internal";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
import type { SdkService } from "./sdk.service";
@@ -41,12 +41,14 @@ export abstract class SdkLoadService {
async loadAndInit(): Promise<void> {
try {
await this.load();
init_sdk();
init_sdk(LogLevel.Info, (message: string) => this.log(message));
SdkLoadService.markAsReady();
} catch (error) {
SdkLoadService.markAsFailed(error);
}
}
protected abstract log(message: string): void;
protected abstract load(): Promise<void>;
}

View File

@@ -1,3 +1,4 @@
import { FlightRecorderService } from "@bitwarden/logging";
import * as sdk from "@bitwarden/sdk-internal";
import * as bitwardenModule from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm";
@@ -9,7 +10,15 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
* **Warning**: This requires WASM support and will fail if the environment does not support it.
*/
export class DefaultSdkLoadService extends SdkLoadService {
constructor(private flightRecorderService: FlightRecorderService) {
super();
}
async load(): Promise<void> {
(sdk as any).init(bitwardenModule);
}
protected log(message: string): void {
this.flightRecorderService.write("[SDK] " + message).then(() => { }).catch(() => { });
}
}

View File

@@ -4,4 +4,6 @@ export class NoopSdkLoadService extends SdkLoadService {
async load() {
throw new Error("SDK not available in this environment");
}
}
protected log(message: string): void {
}
}

View File

@@ -1,3 +1,4 @@
import { FlightRecorderService } from "./flight-recorder.service";
import { LogLevel } from "./log-level";
import { LogService } from "./log.service";
@@ -7,7 +8,8 @@ export class ConsoleLogService implements LogService {
constructor(
protected isDev: boolean,
protected filter: ((level: LogLevel) => boolean) | null = null,
) {}
protected flightRecorderService: FlightRecorderService | null = null,
) { }
debug(message?: any, ...optionalParams: any[]) {
if (!this.isDev) {
@@ -33,6 +35,12 @@ export class ConsoleLogService implements LogService {
return;
}
if (this.flightRecorderService) {
const fullMessage = [message, ...optionalParams].join(" ");
console.log("ConsoleLogService.write:", fullMessage);
this.flightRecorderService.write(`[TS] [${LogLevel[level]}] ${fullMessage}`).then(() => { }).catch(() => { });
}
switch (level) {
case LogLevel.Debug:
// eslint-disable-next-line

View File

@@ -0,0 +1,4 @@
export abstract class FlightRecorderService {
abstract write(message: string): Promise<void>;
abstract getRecords(): Promise<string[]>;
}

View File

@@ -1,3 +1,5 @@
export { LogService } from "./log.service";
export { LogLevel } from "./log-level";
export { ConsoleLogService } from "./console-log.service";
export { FlightRecorderService } from "./flight-recorder.service";
export { MemoryFlightRecorderService } from "./memory-flight-recorder.service";

View File

@@ -0,0 +1,13 @@
import { FlightRecorderService } from "./flight-recorder.service";
export class MemoryFlightRecorderService extends FlightRecorderService {
private readonly records: string[] = [];
async write(message: string): Promise<void> {
this.records.push(message);
}
async getRecords(): Promise<string[]> {
return this.records;
}
}