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

Bugsnag.lastRunInfo for Electron #1486

Merged
merged 4 commits into from
Aug 9, 2021
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
34 changes: 32 additions & 2 deletions packages/electron-filestore/filestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ class FileStore {
sessions: join(base, 'sessions'),
runinfo: join(base, 'runinfo'),
device: join(base, 'device.json'),
lastRunInfo: join(base, 'last-run-info.json'),
minidumps: crashDir
}

this._appRunMetadata = { [identifierKey]: createIdentifier() }
}

// Create directory layout
Expand Down Expand Up @@ -134,8 +137,35 @@ class FileStore {
return device
}

createAppRunMetadata () {
return { [identifierKey]: createIdentifier() }
getLastRunInfo () {
try {
// similar to getDeviceInfo - the lastRunInfo must be available during tha app-launch phase
// as such we use readFileSync to ensure that the data is loaded immediately
const contents = readFileSync(this._paths.lastRunInfo)
lemnik marked this conversation as resolved.
Show resolved Hide resolved
const lastRunInfo = JSON.parse(contents)

if (typeof lastRunInfo.crashed === 'boolean' &&
typeof lastRunInfo.crashedDuringLaunch === 'boolean' &&
typeof lastRunInfo.consecutiveLaunchCrashes === 'number') {
return lastRunInfo
}
} catch (e) {
}

return null
}

setLastRunInfo (lastRunInfo = { crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 }) {
try {
mkdirSync(dirname(this._paths.lastRunInfo), { recursive: true })
writeFileSync(this._paths.lastRunInfo, JSON.stringify(lastRunInfo))
} catch (e) {
}
return lastRunInfo
}

getAppRunMetadata () {
return this._appRunMetadata
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/electron-filestore/test/filestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ describe('FileStore', () => {
})
})

describe('createAppRunMetadata()', () => {
describe('getAppRunMetadata()', () => {
it('generates a key in an expected format', () => {
const metadata = store.createAppRunMetadata()
const metadata = store.getAppRunMetadata()
expect(metadata.bugsnag_crash_id).toMatch(/^[0-9a-z]{64}$/)
})
})
Expand Down
11 changes: 6 additions & 5 deletions packages/electron/src/client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const Client = require('@bugsnag/core/client')
const Event = require('@bugsnag/core/event')
const Breadcrumb = require('@bugsnag/core/breadcrumb')
const Session = require('@bugsnag/core/session')
const {
plugin: PluginClientStatePersistence,
NativeClient
} = require('@bugsnag/plugin-electron-client-state-persistence')

const makeDelivery = require('@bugsnag/delivery-electron')
const { FileStore } = require('@bugsnag/electron-filestore')
Expand All @@ -25,8 +29,6 @@ module.exports = (opts) => {
opts.projectRoot = normalizePath(opts.projectRoot)
}

const { plugin: PluginClientStatePersistence, NativeClient } = require('@bugsnag/plugin-electron-client-state-persistence')

// main internal plugins go here
const internalPlugins = [
// Plugins after the "FirstPlugin" will run in the main process for renderer
Expand All @@ -38,7 +40,7 @@ module.exports = (opts) => {
require('@bugsnag/plugin-electron-ipc'),
require('@bugsnag/plugin-node-uncaught-exception'),
require('@bugsnag/plugin-node-unhandled-rejection'),
require('@bugsnag/plugin-electron-app')(NativeClient, process, electron.app, electron.BrowserWindow),
require('@bugsnag/plugin-electron-app')(NativeClient, process, electron.app, electron.BrowserWindow, filestore),
require('@bugsnag/plugin-electron-app-breadcrumbs')(electron.app, electron.BrowserWindow),
require('@bugsnag/plugin-electron-device')(electron.app, electron.screen, process, filestore, NativeClient, electron.powerMonitor),
require('@bugsnag/plugin-electron-session')(electron.app, electron.BrowserWindow, filestore),
Expand All @@ -58,14 +60,13 @@ module.exports = (opts) => {

const bugsnag = new Client(opts, schema, internalPlugins, require('../id'))

bugsnag._setDelivery(makeDelivery(filestore, electron.net, electron.app))

filestore.init().catch(err => bugsnag._logger.warn('Failed to init crash FileStore directories', err))

// expose markLaunchComplete as a method on the Bugsnag client/facade
const electronApp = bugsnag.getPlugin('electronApp')
const { markLaunchComplete } = electronApp
bugsnag.markLaunchComplete = markLaunchComplete
bugsnag._setDelivery(makeDelivery(filestore, electron.net, electron.app))

bugsnag._logger.debug('Loaded! In main process.')
if (bugsnag._isBreadcrumbTypeEnabled('state')) {
Expand Down
10 changes: 10 additions & 0 deletions packages/electron/src/notifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { Client, Event, Breadcrumb, Session } = createClient

const Bugsnag = {
_client: null,
lastRunInfo: null,
start: (opts) => {
if (Bugsnag._client) {
Bugsnag._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.')
Expand All @@ -23,6 +24,15 @@ const Bugsnag = {
// create the relevant client for the detected environment
Bugsnag._client = createClient(opts)

Object.defineProperty(Bugsnag, 'lastRunInfo', {
get: isMain
? () => Bugsnag._client.lastRunInfo
: () => {
Bugsnag._client._logger.warn('Bugsnag.lastRunInfo can only be accessed in the main process')
return null
}
})

return Bugsnag._client
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/electron/types/notifier.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ interface RendererConfig extends AllowedRendererConfig {
codeBundleId?: string
}

interface LastRunInfo {
crashed: boolean
crashedDuringLaunch: boolean
consecutiveLaunchCrashes: number
}

declare class ElectronClient extends Client {
markLaunchComplete: () => void
readonly lastRunInfo: LastRunInfo | null
}

interface ElectronBugsnagStatic extends ElectronClient {
Expand Down
35 changes: 33 additions & 2 deletions packages/plugin-electron-app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const createAppUpdater = (client, NativeClient, app) => newProperties => {
}
}

const createLastRunInfoUpdater = (client, NativeClient) => lastRunInfo => {
try {
NativeClient.setLastRunInfo(JSON.stringify(lastRunInfo))
} catch (err) {
client._logger.error(err)
}
}

const getInstalledFromStore = process => {
if (process.mas) {
return 'mac'
Expand All @@ -30,15 +38,39 @@ const getInstalledFromStore = process => {
return undefined
}

module.exports = (NativeClient, process, electronApp, BrowserWindow, NativeApp = native) => ({
module.exports = (NativeClient, process, electronApp, BrowserWindow, filestore, NativeApp = native) => ({
name: 'electronApp',
load (client) {
const app = {}
const lastRunInfo = filestore.getLastRunInfo()
const updateApp = createAppUpdater(client, NativeClient, app)
const updateNextCrashLastRunInfo = createLastRunInfoUpdater(client, NativeClient)

client.lastRunInfo = lastRunInfo

updateNextCrashLastRunInfo({
crashed: true,
crashedDuringLaunch: true,
consecutiveLaunchCrashes: lastRunInfo && lastRunInfo.consecutiveLaunchCrashes
? lastRunInfo.consecutiveLaunchCrashes + 1
: 1
})

const markLaunchComplete = () => {
if (app.isLaunching) {
filestore.setLastRunInfo({
crashed: false,
crashedDuringLaunch: false,
consecutiveLaunchCrashes: 0
})
imjoehaines marked this conversation as resolved.
Show resolved Hide resolved

updateApp({ isLaunching: false })
// mark lastRunInfo for possible crash in the NativeClient - only applied for a native crash
updateNextCrashLastRunInfo({
crashed: true,
crashedDuringLaunch: false,
consecutiveLaunchCrashes: 0
})
}
}

Expand Down Expand Up @@ -131,7 +163,6 @@ module.exports = (NativeClient, process, electronApp, BrowserWindow, NativeApp =
})

client._app = app

return { markLaunchComplete }
},
configSchema: {
Expand Down
17 changes: 14 additions & 3 deletions packages/plugin-electron-app/test/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ interface MakeClientOptions {
NativeApp?: any
process?: any
config?: { launchDurationMillis: number|undefined }
filestore?: any
}

function makeClient ({
Expand All @@ -765,17 +766,27 @@ function makeClient ({
NativeClient = makeNativeClient(),
process = makeProcess(),
config = { launchDurationMillis: 0 },
NativeApp = makeNativeApp()
NativeApp = makeNativeApp(),
filestore = makeFileStore()
}: MakeClientOptions = {}): ReturnType<typeof makeClientForPlugin> {
return makeClientForPlugin({
config,
plugin: plugin(NativeClient, process, electronApp, BrowserWindow, NativeApp)
plugin: plugin(NativeClient, process, electronApp, BrowserWindow, filestore, NativeApp)
})
}

function makeNativeClient () {
return {
setApp: jest.fn()
install: jest.fn(),
setApp: jest.fn(),
setLastRunInfo: jest.fn()
}
}

function makeFileStore () {
return {
getLastRunInfo: jest.fn().mockReturnValue({ crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 }),
setLastRunInfo: jest.fn()
}
}

Expand Down
72 changes: 57 additions & 15 deletions packages/plugin-electron-client-state-persistence/src/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@ static napi_value Uninstall(napi_env env, napi_callback_info info) {
}

static napi_value Install(napi_env env, napi_callback_info info) {
size_t argc = 3;
napi_value args[3];
size_t argc = 4;
napi_value args[4];
napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
assert(status == napi_ok);

if (argc < 2) {
if (argc < 3) {
napi_throw_type_error(env, NULL,
"Wrong number of arguments, expected 2 or 3");
"Wrong number of arguments, expected 3 or 4");
return NULL;
}

Expand All @@ -152,9 +152,13 @@ static napi_value Install(napi_env env, napi_callback_info info) {
status = napi_typeof(env, args[1], &valuetype1);
assert(status == napi_ok);

if (valuetype0 != napi_string || valuetype1 != napi_number) {
napi_valuetype valuetype2;
status = napi_typeof(env, args[2], &valuetype2);
assert(status == napi_ok);

if (valuetype0 != napi_string || valuetype1 != napi_string || valuetype2 != napi_number) {
napi_throw_type_error(
env, NULL, "Wrong argument types, expected (string, number, object?)");
env, NULL, "Wrong argument types, expected (string, string, number, object?)");
return NULL;
}

Expand All @@ -163,29 +167,36 @@ static napi_value Install(napi_env env, napi_callback_info info) {
return NULL;
}

char *lastRunInfoFilePath = read_string_value(env, args[1], false);
if (!lastRunInfoFilePath) {
free(filepath);
return NULL;
}

double max_crumbs;
status = napi_get_value_double(env, args[1], &max_crumbs);
status = napi_get_value_double(env, args[2], &max_crumbs);
assert(status == napi_ok);

if (argc > 2) {
napi_valuetype valuetype2;
status = napi_typeof(env, args[2], &valuetype2);
if (argc > 3) {
napi_valuetype valuetype3;
status = napi_typeof(env, args[3], &valuetype3);
assert(status == napi_ok);

if (valuetype2 == napi_object) {
char *state = read_string_value(env, json_stringify(env, args[2]), true);
becsp_install(filepath, max_crumbs, state);
if (valuetype3 == napi_object) {
char *state = read_string_value(env, json_stringify(env, args[3]), true);
becsp_install(filepath, lastRunInfoFilePath, max_crumbs, state);
free(state);
} else {
napi_throw_type_error(
env, NULL,
"Wrong argument types, expected (string, number, object?)");
"Wrong argument types, expected (string, string, number, object?)");
}
} else {
becsp_install(filepath, max_crumbs, NULL);
becsp_install(filepath, lastRunInfoFilePath, max_crumbs, NULL);
}

free(filepath);
free(lastRunInfoFilePath);

return NULL;
}
Expand Down Expand Up @@ -379,6 +390,29 @@ static napi_value PersistState(napi_env env, napi_callback_info info) {
return NULL;
}

static napi_value SetLastRunInfo(napi_env env, napi_callback_info info) {
char *lastRunInfo;
size_t argc = 1;
napi_value args[1];
napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
assert(status == napi_ok);

if (argc < 1) {
napi_throw_type_error(env, NULL, "Wrong number of arguments, expected 1");
return NULL;
}

lastRunInfo = read_string_value(env, args[0], false);

becsp_set_last_run_info(lastRunInfo);
return NULL;
}

static napi_value PersistLastRunInfo(napi_env env, napi_callback_info info) {
bescp_persist_last_run_info_if_required();
return NULL;
}

#define DECLARE_NAPI_METHOD(name, func) \
(napi_property_descriptor) { name, 0, func, 0, 0, 0, napi_default, 0 }

Expand Down Expand Up @@ -419,6 +453,14 @@ napi_value Init(napi_env env, napi_value exports) {
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);

desc = DECLARE_NAPI_METHOD("setLastRunInfo", SetLastRunInfo);
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);

desc = DECLARE_NAPI_METHOD("persistLastRunInfo", PersistLastRunInfo);
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);

desc = DECLARE_NAPI_METHOD("uninstall", Uninstall);
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);
Expand Down
Loading