Skip to content

Commit 6996579

Browse files
committed
Improve and clean up panic dialog code and wasm wrapper (#368)
Part of #357
1 parent d290aaf commit 6996579

File tree

17 files changed

+119
-89
lines changed

17 files changed

+119
-89
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/src/communication/dispatcher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl Dispatcher {
5353
}
5454

5555
pub fn collect_actions(&self) -> ActionList {
56-
//TODO: reduce the number of heap allocations
56+
// TODO: Reduce the number of heap allocations
5757
let mut list = Vec::new();
5858
list.extend(self.input_preprocessor.actions());
5959
list.extend(self.input_mapper.actions());

editor/src/frontend/frontend_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub enum FrontendMessage {
1313
SetActiveDocument { document_index: usize },
1414
UpdateOpenDocumentsList { open_documents: Vec<String> },
1515
DisplayError { title: String, description: String },
16-
DisplayPanic { title: String, description: String },
16+
DisplayPanic { panic_info: String, title: String, description: String },
1717
DisplayConfirmationToCloseDocument { document_index: usize },
1818
DisplayConfirmationToCloseAllDocuments,
1919
UpdateCanvas { document: String },

editor/src/lib.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,35 @@
1-
// since our policy is tabs, we want to stop clippy from warning about that
1+
// Since our policy is tabs, we want to stop clippy from warning about that
22
#![allow(clippy::tabs_in_doc_comments)]
33

44
extern crate graphite_proc_macros;
55

66
mod communication;
77
#[macro_use]
88
pub mod misc;
9+
pub mod consts;
910
mod document;
1011
mod frontend;
1112
mod global;
1213
pub mod input;
1314
pub mod tool;
1415

15-
pub mod consts;
16-
17-
#[doc(inline)]
18-
pub use misc::EditorError;
19-
2016
#[doc(inline)]
2117
pub use graphene::color::Color;
22-
18+
#[doc(inline)]
19+
pub use graphene::document::Document as SvgDocument;
2320
#[doc(inline)]
2421
pub use graphene::LayerId;
25-
2622
#[doc(inline)]
27-
pub use graphene::document::Document as SvgDocument;
23+
pub use misc::EditorError;
2824

2925
use communication::dispatcher::Dispatcher;
26+
use message_prelude::*;
27+
3028
// TODO: serialize with serde to save the current editor state
3129
pub struct Editor {
3230
dispatcher: Dispatcher,
3331
}
3432

35-
use message_prelude::*;
36-
3733
impl Editor {
3834
pub fn new() -> Self {
3935
Self { dispatcher: Dispatcher::new() }
@@ -49,6 +45,12 @@ impl Editor {
4945
}
5046
}
5147

48+
impl Default for Editor {
49+
fn default() -> Self {
50+
Self::new()
51+
}
52+
}
53+
5254
pub mod message_prelude {
5355
pub use crate::communication::generate_uuid;
5456
pub use crate::communication::message::{AsMessage, Message, MessageDiscriminant};

frontend/src/components/panels/Document.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ import { defineComponent } from "vue";
227227
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, UpdateScrollbars, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
228228
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
229229
import { comingSoon } from "@/utilities/errors";
230-
import { panicProxy } from "@/utilities/panic";
230+
import { panicProxy } from "@/utilities/panic-proxy";
231231
232232
import LayoutRow from "@/components/layout/LayoutRow.vue";
233233
import LayoutCol from "@/components/layout/LayoutCol.vue";

frontend/src/components/panels/LayerTree.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
import { defineComponent } from "vue";
183183
184184
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, CollapseFolder, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
185-
import { panicProxy } from "@/utilities/panic";
185+
import { panicProxy } from "@/utilities/panic-proxy";
186186
import { SeparatorType } from "@/components/widgets/widgets";
187187
188188
import LayoutRow from "@/components/layout/LayoutRow.vue";

frontend/src/components/widgets/inputs/MenuBarInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
import { defineComponent } from "vue";
5555
5656
import { comingSoon } from "@/utilities/errors";
57-
import { panicProxy } from "@/utilities/panic";
57+
import { panicProxy } from "@/utilities/panic-proxy";
5858
5959
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
6060
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";

frontend/src/components/widgets/inputs/SwatchPairInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
import { defineComponent } from "vue";
7070
7171
import { rgbToDecimalRgb, RGB } from "@/utilities/color";
72-
import { panicProxy } from "@/utilities/panic";
72+
import { panicProxy } from "@/utilities/panic-proxy";
7373
import { ResponseType, registerResponseHandler, Response, UpdateWorkingColors } from "@/utilities/response-handler";
7474
7575
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";

frontend/src/components/widgets/options/ToolOptions.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import { defineComponent, PropType } from "vue";
3333
3434
import { comingSoon } from "@/utilities/errors";
35-
import { panicProxy } from "@/utilities/panic";
35+
import { panicProxy } from "@/utilities/panic-proxy";
3636
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
3737
3838
import Separator from "@/components/widgets/separators/Separator.vue";

frontend/src/utilities/documents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
SaveDocument,
1313
} from "@/utilities/response-handler";
1414
import { download, upload } from "@/utilities/files";
15-
import { panicProxy } from "@/utilities/panic";
15+
import { panicProxy } from "@/utilities/panic-proxy";
1616

1717
const wasm = import("@/../wasm/pkg").then(panicProxy);
1818

frontend/src/utilities/errors.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { createDialog, dismissDialog } from "@/utilities/dialog";
22
import { TextButtonWidget } from "@/components/widgets/widgets";
3-
import { getPanicDetails } from "@/utilities/panic";
43
import { ResponseType, registerResponseHandler, Response, DisplayError, DisplayPanic } from "@/utilities/response-handler";
54

5+
// Coming soon dialog
66
export function comingSoon(issueNumber?: number) {
77
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
88
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
@@ -23,6 +23,7 @@ export function comingSoon(issueNumber?: number) {
2323
createDialog("Warning", "Coming soon", details, buttons);
2424
}
2525

26+
// Graphite error dialog
2627
registerResponseHandler(ResponseType.DisplayError, (responseData: Response) => {
2728
const data = responseData as DisplayError;
2829

@@ -36,30 +37,39 @@ registerResponseHandler(ResponseType.DisplayError, (responseData: Response) => {
3637
createDialog("Warning", data.title, data.description, buttons);
3738
});
3839

40+
// Code panic dialog and console error
3941
registerResponseHandler(ResponseType.DisplayPanic, (responseData: Response) => {
4042
const data = responseData as DisplayPanic;
4143

44+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45+
(Error as any).stackTraceLimit = Infinity;
46+
const stackTrace = new Error().stack || "";
47+
const panicDetails = `${data.panic_info}\n\n${stackTrace}`;
48+
49+
// eslint-disable-next-line no-console
50+
console.error(panicDetails);
51+
4252
const reloadButton: TextButtonWidget = {
4353
kind: "TextButton",
4454
callback: async () => window.location.reload(),
4555
props: { label: "Reload", emphasized: true, minWidth: 96 },
4656
};
4757
const copyErrorLogButton: TextButtonWidget = {
4858
kind: "TextButton",
49-
callback: async () => navigator.clipboard.writeText(getPanicDetails()),
59+
callback: async () => navigator.clipboard.writeText(panicDetails),
5060
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
5161
};
5262
const reportOnGithubButton: TextButtonWidget = {
5363
kind: "TextButton",
54-
callback: async () => window.open(githubUrl(), "_blank"),
64+
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
5565
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
5666
};
5767
const buttons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
5868

5969
createDialog("Warning", data.title, data.description, buttons);
6070
});
6171

62-
function githubUrl() {
72+
function githubUrl(panicDetails: string) {
6373
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
6474

6575
const body = `
@@ -74,17 +84,17 @@ Describe precisely how the crash occurred, step by step, starting with a new edi
7484
4.
7585
5.
7686
77-
**Browser and OS*
78-
List of your browser and its version, as well as your operating system.
79-
8087
**Additional Details**
8188
Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue.
8289
90+
**Browser and OS**
91+
${browserVersion()}, ${operatingSystem()}
92+
8393
**Stack Trace**
8494
Copied from the crash dialog in the Graphite Editor:
8595
8696
\`\`\`
87-
${getPanicDetails()}
97+
${panicDetails}
8898
\`\`\`
8999
`.trim();
90100

@@ -104,3 +114,48 @@ ${getPanicDetails()}
104114

105115
return url.toString();
106116
}
117+
118+
function browserVersion(): string {
119+
const agent = window.navigator.userAgent;
120+
let match = agent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
121+
122+
if (/trident/i.test(match[1])) {
123+
const browser = /\brv[ :]+(\d+)/g.exec(agent) || [];
124+
return `IE ${browser[1] || ""}`.trim();
125+
}
126+
127+
if (match[1] === "Chrome") {
128+
let browser = agent.match(/\bEdg\/(\d+)/);
129+
if (browser !== null) return `Edge (Chromium) ${browser[1]}`;
130+
131+
browser = agent.match(/\bOPR\/(\d+)/);
132+
if (browser !== null) return `Opera ${browser[1]}`;
133+
}
134+
135+
match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, "-?"];
136+
137+
const browser = agent.match(/version\/(\d+)/i);
138+
if (browser !== null) match.splice(1, 1, browser[1]);
139+
140+
return `${match[0]} ${match[1]}`;
141+
}
142+
143+
function operatingSystem(): string {
144+
const osTable: Record<string, string> = {
145+
"Windows NT 11": "Windows 11",
146+
"Windows NT 10": "Windows 10",
147+
"Windows NT 6.3": "Windows 8.1",
148+
"Windows NT 6.2": "Windows 8",
149+
"Windows NT 6.1": "Windows 7",
150+
"Windows NT 6.0": "Windows Vista",
151+
"Windows NT 5.1": "Windows XP",
152+
"Windows NT 5.0": "Windows 2000",
153+
Mac: "Mac",
154+
X11: "Unix",
155+
Linux: "Linux",
156+
Unknown: "YOUR OPERATING SYSTEM",
157+
};
158+
159+
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
160+
return osTable[userAgentOS || "Unknown"];
161+
}

frontend/src/utilities/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { toggleFullscreen } from "@/utilities/fullscreen";
22
import { dialogIsVisible, dismissDialog, submitDialog } from "@/utilities/dialog";
3-
import { panicProxy } from "@/utilities/panic";
3+
import { panicProxy } from "@/utilities/panic-proxy";
44

55
const wasm = import("@/../wasm/pkg").then(panicProxy);
66

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any, func-names */
2+
13
// Import this function and chain it on all `wasm` imports like: const wasm = import("@/../wasm/pkg").then(panicProxy);
24
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing `RuntimeError: unreachable` exceptions sent to the console
3-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45
export function panicProxy(module: any) {
56
const proxyHandler = {
6-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77
get(target: any, propKey: any, receiver: any) {
88
const targetValue = Reflect.get(target, propKey, receiver);
99

1010
// Keep the original value being accessed if it isn't a function or it is a class
11-
// TODO: Figure out how to also wrap (class) constructor functions instead of skipping them for now
11+
// TODO: Figure out how to also wrap class constructor functions instead of skipping them for now
1212
const isFunction = typeof targetValue === "function";
1313
const isClass = isFunction && /^\s*class\s+/.test(targetValue.toString());
1414
if (!isFunction || isClass) return targetValue;
1515

1616
// Replace the original function with a wrapper function that runs the original in a try-catch block
17-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, func-names
1817
return function (...args: any) {
1918
let result;
2019
try {
@@ -31,20 +30,3 @@ export function panicProxy(module: any) {
3130

3231
return new Proxy(module, proxyHandler);
3332
}
34-
35-
// Intercept console.error() for panic messages sent by code in the WASM toolchain
36-
let panicDetails = "";
37-
// eslint-disable-next-line no-console
38-
const error = console.error.bind(console);
39-
// eslint-disable-next-line no-console
40-
console.error = (...args) => {
41-
const details = "".concat(...args).trim();
42-
if (details.startsWith("panicked at")) panicDetails = details;
43-
44-
error(...args);
45-
};
46-
47-
// Get the body of the panic's exception that was printed in the console
48-
export function getPanicDetails(): string {
49-
return panicDetails;
50-
}

frontend/src/utilities/response-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,13 @@ function newDisplayError(input: any): DisplayError {
160160
}
161161

162162
export interface DisplayPanic {
163+
panic_info: string;
163164
title: string;
164165
description: string;
165166
}
166167
function newDisplayPanic(input: any): DisplayPanic {
167168
return {
169+
panic_info: input.panic_info,
168170
title: input.title,
169171
description: input.description,
170172
};

frontend/wasm/Cargo.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ publish = false
1212
[lib]
1313
crate-type = ["cdylib", "rlib"]
1414

15-
[features]
16-
default = ["console_error_panic_hook"]
17-
1815
[dependencies]
19-
console_error_panic_hook = { version = "0.1.6", optional = true }
2016
editor = { path = "../../editor", package = "graphite-editor" }
2117
graphene = { path = "../../graphene", package = "graphite-graphene" }
2218
log = "0.4"

0 commit comments

Comments
 (0)