Skip to content

Commit b9b24f9

Browse files
committed
Prevent GtkBuilder crashes
Up until now - we've been implementing workarounds to detect XML tree that would make GtkBuilder crash. This was very fragile and did not support the many ways a widget can crash at init time. Remove the workarorunds and build the XML in a separate process to assert that it won't crash the main process. Fixes #170
1 parent 029732d commit b9b24f9

File tree

12 files changed

+73
-73
lines changed

12 files changed

+73
-73
lines changed

data/app.metainfo.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@
4949
<release version="43.2" date="2022-12-xx">
5050
<description>
5151
<ul>
52-
<li>Add support for JavaScript diagnostics</li>
52+
<li>Switching between XML and Blueprint will now convert between the two</li>
53+
<li>Add support for JavaScript diagnostics and linting</li>
54+
<li>Optimize Blueprint preview</li>
5355
<li>Blueprint 0.6.0</li>
56+
<li>Fix preview of non GtkBuildable</li>
57+
<li>Prevent all GtkBuilder related crash</li>
5458
</ul>
5559
</description>
5660
</release>

src/PanelCode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function PanelCode({
5454
panel.language = dropdown_code_lang.selected_item.string;
5555
stack_code.visible_child_name = panel.language;
5656
previewer.useInternal();
57-
previewer.update();
57+
previewer.update().catch(logError);
5858
}
5959
switchLanguage();
6060

src/PanelUI.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,8 @@ export default function PanelUI({
131131
function start() {
132132
stop();
133133
lang = getLanguage(dropdown_ui_lang.selected_item.string);
134-
handler_id_xml = code_view_xml.connect(
135-
"changed",
136-
() => code_view_xml.buffer.text,
134+
handler_id_xml = code_view_xml.connect("changed", () =>
135+
onXML(code_view_xml.buffer.text),
137136
);
138137
handler_id_blueprint = code_view_blueprint.connect("changed", onBlueprint);
139138
blueprint.lspc.connect(

src/Previewer/Previewer.js

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import Gio from "gi://Gio";
66
import * as xml from "../langs/xml/xml.js";
77
import * as postcss from "../lib/postcss.js";
88

9-
import { settings } from "../util.js";
9+
import { settings, unstack } from "../util.js";
1010

1111
import Internal from "./Internal.js";
1212
import External from "./External.js";
1313
import { getClassNameType } from "../overrides.js";
1414

15-
import { assertBuildable, isPreviewable } from "./utils.js";
15+
import { isBuilderable, isPreviewable } from "./utils.js";
1616

1717
// Workbench always defaults to in-process preview now if Vala is selected.
1818
// Workbench will switch to out-of-process preview when Vala is run
@@ -101,10 +101,10 @@ export default function Previewer({
101101
function start() {
102102
stop();
103103
if (handler_id_ui === null) {
104-
handler_id_ui = panel_ui.connect("updated", update);
104+
handler_id_ui = panel_ui.connect("updated", schedule_update);
105105
}
106106
if (handler_id_css === null) {
107-
handler_id_css = code_view_css.connect("changed", update);
107+
handler_id_css = code_view_css.connect("changed", schedule_update);
108108
}
109109
}
110110

@@ -145,11 +145,7 @@ export default function Previewer({
145145
);
146146

147147
let symbols = null;
148-
function update() {
149-
const builder = new Gtk.Builder();
150-
const scope = new BuilderScope();
151-
builder.set_scope(scope);
152-
148+
async function update() {
153149
let text = panel_ui.xml.trim();
154150
let target_id;
155151
let tree;
@@ -168,32 +164,29 @@ export default function Previewer({
168164

169165
if (!target_id) return;
170166

171-
try {
172-
assertBuildable(tree);
173-
} catch (err) {
174-
console.error(err);
175-
return;
176-
}
167+
console.time("builderable");
168+
if (!(await isBuilderable(text))) return;
169+
console.timeEnd("builderable");
170+
171+
const builder = new Gtk.Builder();
172+
const scope = new BuilderScope();
173+
builder.set_scope(scope);
177174

178175
registerSignals({ tree, scope, symbols, template });
179176

177+
term_console.clear();
178+
180179
try {
181-
// For some reason this log warnings twice
182180
builder.add_from_string(text, -1);
183181
} catch (err) {
184-
// The following while being obviously invalid
185-
// does no produce an error - so we will need to strictly validate the XML
186-
// before constructing the builder
187-
// prettier-xml throws but doesn't give a stack trace
188-
// <style>
189-
// <class name="title-1"
190-
// </style>
182+
if (err instanceof GLib.MarkupError || err instanceof Gtk.BuilderError) {
183+
console.warn(err.message);
184+
return;
185+
}
191186
logError(err);
192187
return;
193188
}
194189

195-
term_console.clear();
196-
197190
const object_preview = builder.get_object(target_id);
198191
if (!object_preview) return;
199192

@@ -215,6 +208,8 @@ export default function Previewer({
215208
symbols = null;
216209
}
217210

211+
const schedule_update = unstack(update, logError);
212+
218213
function useExternal() {
219214
if (current === external) return;
220215
stack.set_visible_child_name("open_window");

src/Previewer/crasher.vala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public void main (string[] args) {
2+
Adw.init ();
3+
4+
var builder = new Gtk.Builder();
5+
6+
try {
7+
builder.add_from_string(args[1], -1);
8+
// We are only interested in crashes here
9+
// Previewer.js will handle errors
10+
} catch {}
11+
}

src/Previewer/meson.build

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
executable('workbench-vala-previewer',
2+
'previewer.vala',
3+
dependencies: [ dependency('gio-2.0'), dependency('gmodule-2.0'), dependency('libadwaita-1') ],
4+
# vala_args: [ '--gresourcesdir=' + meson.current_build_dir() ],
5+
install: true,
6+
)
7+
8+
executable('workbench-crasher',
9+
'crasher.vala',
10+
dependencies: [ dependency('gio-2.0'), dependency('gmodule-2.0'), dependency('libadwaita-1') ],
11+
# vala_args: [ '--gresourcesdir=' + meson.current_build_dir() ],
12+
install: true,
13+
)
File renamed without changes.

src/Previewer/utils.js

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import GObject from "gi://GObject";
22
import Gtk from "gi://Gtk";
3+
import Gio from "gi://Gio";
4+
import { promiseTask } from "../../troll/src/util.js";
35

46
export function getObjectClass(class_name) {
57
const split = class_name.split(/(?=[A-Z])/);
@@ -13,33 +15,17 @@ export function isPreviewable(class_name) {
1315
const klass = getObjectClass(class_name);
1416
if (!klass) return false;
1517

16-
return GObject.type_is_a(klass, Gtk.Widget);
17-
}
18+
// GLib-GObject-ERROR: cannot create instance of abstract (non-instantiatable) type 'GtkWidget'
19+
if (GObject.type_test_flags(klass, GObject.TypeFlags.ABSTRACT)) return false;
1820

19-
// TODO: GTK Builder shouldn't crash when encountering a non buildable parent
20-
// https://github.com/sonnyp/Workbench/issues/49
21-
export function assertBuildable(el) {
22-
for (const el_object of el.getChildren("object")) {
23-
assertObjectBuildable(el_object);
24-
}
21+
return GObject.type_is_a(klass, Gtk.Widget);
2522
}
2623

27-
function assertObjectBuildable(el_object) {
28-
const class_name = el_object.attrs.class;
29-
if (class_name) {
30-
const klass = getObjectClass(class_name);
31-
// GLib-GObject-ERROR: cannot create instance of abstract (non-instantiatable) type 'GtkWidget'
32-
if (klass && GObject.type_test_flags(klass, GObject.TypeFlags.ABSTRACT)) {
33-
throw new Error(`${class_name} is an abstract type`);
34-
}
35-
if (klass && !GObject.type_is_a(klass, Gtk.Buildable)) {
36-
throw new Error(`${class_name} is not a GtkBuildable`);
37-
}
38-
}
39-
40-
for (const el_child of el_object.getChildren("child")) {
41-
for (const el of el_child.getChildren("object")) {
42-
assertObjectBuildable(el);
43-
}
44-
}
24+
export async function isBuilderable(str) {
25+
const flags =
26+
Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE;
27+
const proc = Gio.Subprocess.new(["workbench-crasher", str], flags);
28+
await promiseTask(proc, "wait_async", "wait_finish", null);
29+
if (!proc.get_if_exited()) return false;
30+
return proc.get_exit_status() === 0;
4531
}

src/Previewer/vala-previewer/meson.build

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/lsp/LSPClient.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,10 @@ export default class LSPClient {
129129
_start_process() {
130130
let flags =
131131
Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE;
132-
// vala-language-server emit lots of criticals so we disable this on release
132+
// vala-language-server and blueprint are very verbose
133133
// https://github.com/vala-lang/vala-language-server/issues/274
134-
// blueprint logs everything on stderr and is also quite verbose
135-
if (!__DEV__) {
136-
flags = flags | Gio.SubprocessFlags.STDERR_SILENCE;
137-
}
134+
// comment this to debug LSP
135+
flags = flags | Gio.SubprocessFlags.STDERR_SILENCE;
138136

139137
this.proc = Gio.Subprocess.new(this.argv, flags);
140138
this.proc.wait_async(null, (_self, res) => {

0 commit comments

Comments
 (0)