Skip to content
Open
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
3 changes: 3 additions & 0 deletions src-main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ app.enableSandbox();
// https://github.com/LLK/scratch-desktop/blob/4b462212a8e406b15bcf549f8523645602b46064/src/main/index.js#L45
app.commandLine.appendSwitch('host-resolver-rules', 'MAP device-manager.scratch.mit.edu 127.0.0.1');

// Needed for collectJavaScriptCallStack() in Electron 34.
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports');

app.on('session-created', (session) => {
// Permission requests are delegated to AbstractWindow

Expand Down
8 changes: 8 additions & 0 deletions src-main/l10n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -486,5 +486,13 @@
"extension-documentation.title": {
"string": "{APP_NAME} Extension Documentation",
"developer_comment": "Title of in-app window for viewing extension documentation."
},
"unresponsive.message": {
"string": "This window is unresponsive. It will continue to run in the background.",
"developer_comment": "Alert that appears when a window stops responding (stuck in infinite loop)"
},
"unresponsive.checkbox": {
"string": "Ignore future warnings",
"developer_comment": "Option in unresponsive window alert to ignore additional alerts."
}
}
5 changes: 4 additions & 1 deletion src-main/protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ const errorPageHeaders = {
const getBaseProtocolHeaders = metadata => {
const result = {
// Make sure Chromium always trusts our content-type and doesn't try anything clever
'x-content-type-options': 'nosniff'
'x-content-type-options': 'nosniff',

// Needed for collectJavaScriptCallStack() in Electron 34.
'document-policy': 'include-js-call-stacks-in-crash-reports=?1'
};

// Optional Content-Security-Policy
Expand Down
85 changes: 84 additions & 1 deletion src-main/windows/abstract.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const { BrowserWindow, screen, session } = require('electron');
const {BrowserWindow, screen, session, dialog} = require('electron');
const path = require('path');
const openExternal = require('../open-external');
const settings = require('../settings');
const {APP_NAME} = require('../brand');
const {translate} = require('../l10n');

/** @type {Map<unknown, AbstractWindow[]>} */
const windowsByClass = new Map();
Expand All @@ -22,6 +24,13 @@ class AbstractWindow {
this.window.webContents.on('before-input-event', this.handleInput.bind(this));
this.applySettings();

this.window.webContents.on('unresponsive', this.handleUnresponsive.bind(this));
this.window.webContents.on('responsive', this.handleResponsive.bind(this));
/** @type {AbortController|null} */
this._unresponsiveController = null;
/** @type {boolean} */
this._unresponsiveWarningsDisabled = false;

if (!options.existingWindow) {
// getCursorScreenPoint() segfaults on Linux in Wayland if called before a BrowserWindow is created, so
// we can't compute this in getWindowOptions().
Expand Down Expand Up @@ -290,6 +299,80 @@ class AbstractWindow {
}
}

async handleUnresponsive () {
if (this._unresponsiveWarningsDisabled) {
return;
}

if (this._unresponsiveController) {
this._unresponsiveController.abort();
}

const controller = new AbortController();
this._unresponsiveController = controller;

let detail = `URL: ${this.window.webContents.mainFrame.url}\n`;

// collectJavaScriptCallStack() was added in Electron 34, so not support in legacy
// builds for Windows 7, 8, 8.1 or macOS 10.13, 10.14, 10.15
if (typeof this.window.webContents.mainFrame.collectJavaScriptCallStack === 'function') {
let stack;
try {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

// collectJavaScriptCallStack will never resolve if somehow no JavaScript is running, so
// add a timer so we never wait too long.
stack = await Promise.race([
this.window.webContents.mainFrame.collectJavaScriptCallStack(),
sleep(1000).then(() => null)
]).catch(() => null);
} catch (e) {
console.error(e);
stack = '' + e;
}

// collectJavaScriptCallStack returns non-string if it couldn't get details for any reason.
if (typeof stack === 'string') {
detail += `Stack: ${stack}\n`;
} else {
detail += 'Stack: internal error\n';
}
} else {
detail += 'Stack: API not available\n';
}

// Operations above are async; window may have become responsive during that time.
if (!controller.signal.aborted) {
dialog.showMessageBox(this.window, {
type: 'error',
title: APP_NAME,
signal: controller.signal,
message: translate('unresponsive.message'),
checkboxChecked: false,
checkboxLabel: translate('unresponsive.checkbox'),
noLink: true,
detail
}).then((result) => {
// TODO: only consider checkboxes when user clicks explicitly and not when its checked
// then the window becomes responsive?
if (result.checkboxChecked) {
this._unresponsiveWarningsDisabled = true;
}

if (this._unresponsiveController === controller) {
this._unresponsiveController = null
}
});
}
}

handleResponsive () {
if (this._unresponsiveController) {
this._unresponsiveController.abort();
this._unresponsiveController = null;
}
}

/**
* @param {Electron.WillNavigateEvent} event
* @param {string} url
Expand Down