Skip to content

Commit 8716a05

Browse files
braceyourselfjmmaranan
authored andcommitted
fix: guard tree rendering against destroyed window actors
If a window gets destroyed mid-render (app crash, rapid close, async cleanup), nodes in the tree still reference the finalized GObject. Accessing anything on it segfaults gnome-shell (signal 11). Adds isNodeValid() that probes the actor with get_name() in a try/catch (GJS throws on finalized GObjects rather than segfaulting). Filters dead nodes out before layout, and guards the remaining property accesses that can race with window destruction.
1 parent 4f05319 commit 8716a05

1 file changed

Lines changed: 39 additions & 5 deletions

File tree

‎lib/extension/tree.js‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,9 @@ export class Node extends GObject.Object {
459459
style_class: "window-tabbed-tab",
460460
x_expand: true,
461461
});
462+
// Window tracker may not resolve an app for a dying window
462463
letapp=this.app;
464+
if(!app)return;
463465
letlabelText=this._getTitle();
464466
letmetaWin=this.nodeValue;
465467
lettitleButton=newSt.Button({
@@ -537,6 +539,21 @@ export class Node extends GObject.Object {
537539
returnnull;
538540
}
539541

542+
// Check if the underlying window actor is still alive. GJS throws
543+
// on property access of finalized GObjects rather than segfaulting,
544+
// so a cheap get_name() call is enough to detect dead actors.
545+
isNodeValid(){
546+
if(!this.isWindow())returntrue;
547+
try{
548+
letactor=this._actor;
549+
if(!actor)returnfalse;
550+
actor.get_name();
551+
returntrue;
552+
}catch(e){
553+
returnfalse;
554+
}
555+
}
556+
540557
render(){
541558
// Always update the title for the tab
542559
if(this.tab!==null&&this.tab!==undefined){
@@ -1274,8 +1291,12 @@ export class Tree extends Node {
12741291
tiledChildren.forEach((w)=>{
12751292
if(w.renderRect){
12761293
if(w.renderRect.width>0&&w.renderRect.height>0){
1294+
// Window may have been destroyed since processNode computed renderRect
12771295
letmetaWin=w.nodeValue;
1278-
this.extWm.move(metaWin,w.renderRect);
1296+
try{
1297+
this.extWm.move(metaWin,w.renderRect);
1298+
}catch(e){
1299+
}
12791300
}else{
12801301
Logger.debug(`ignoring apply for ${w.renderRect.width}x${w.renderRect.height}`);
12811302
}
@@ -1385,7 +1406,8 @@ export class Tree extends Node {
13851406
});
13861407
}
13871408

1388-
tiledChildren.forEach((child,index)=>{
1409+
// Skip windows whose actors were destroyed mid-render
1410+
tiledChildren.filter((c)=>c.isNodeValid()).forEach((child,index)=>{
13891411
// A monitor can contain a window or container child
13901412
if(node.layout===LAYOUT_TYPES.HSPLIT||node.layout===LAYOUT_TYPES.VSPLIT){
13911413
this.processSplit(node,child,params,index);
@@ -1527,10 +1549,17 @@ export class Tree extends Node {
15271549
if(node.childNodes.length>1||alwaysShowDecorationTab){
15281550
nodeY=nodeRect.y+params.stackedHeight;
15291551
nodeHeight=nodeRect.height-params.stackedHeight;
1530-
if(node.decoration&&child.isWindow()){
1552+
if(node.decoration&&child.isWindow()&&child.isNodeValid()){
15311553
letgap=this.extWm.calculateGaps(node);
15321554
letrenderRect=this.processGap(node);
1533-
letborderWidth=child.actor.border.get_theme_node().get_border_width(St.Side.TOP);
1555+
// Border actor may be gone if the window was destroyed mid-render
1556+
letborderWidth=0;
1557+
try{
1558+
if(child.actor?.border){
1559+
borderWidth=child.actor.border.get_theme_node().get_border_width(St.Side.TOP);
1560+
}
1561+
}catch(e){
1562+
}
15341563

15351564
// Make adjustments to the gaps
15361565
letadjust=4*Utils.dpi();
@@ -1552,7 +1581,12 @@ export class Tree extends Node {
15521581
}else{
15531582
decoration.hide();
15541583
}
1555-
if(!decoration.contains(child.tab))decoration.add_child(child.tab);
1584+
if(child.tab&&!decoration.contains(child.tab)){
1585+
try{
1586+
decoration.add_child(child.tab);
1587+
}catch(e){
1588+
}
1589+
}
15561590
}
15571591

15581592
child.render();

0 commit comments

Comments
 (0)