Skip to content

Commit 43cc022

Browse files
committed
screensaver: Do more to acquire a modal grab attempt when locking.
cinnamon-screensaver made muultiple attempts to acquire a modal 'grab' when activating, including using xdo to cancel ui toolkit popups. - Add an asynchronous pushScreensaverModal to do the same. - Screensaver service 'response' is delayed until this ultimately succeeds or fails. - Suspend inhibitor state is held during this time also.
1 parent c59b689 commit 43cc022

6 files changed

Lines changed: 402 additions & 72 deletions

File tree

js/ui/main.js

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,40 @@ function _findModal(actor) {
13181318
return -1;
13191319
}
13201320

1321+
function _completeModalSetup(actor, mode) {
1322+
if (modalCount == 0)
1323+
Meta.disable_unredirect_for_display(global.display);
1324+
1325+
global.set_stage_input_mode(Cinnamon.StageInputMode.FULLSCREEN);
1326+
1327+
actionMode = mode;
1328+
1329+
modalCount += 1;
1330+
let actorDestroyId = actor.connect('destroy', function() {
1331+
let index = _findModal(actor);
1332+
if (index >= 0)
1333+
popModal(actor);
1334+
});
1335+
1336+
let record = {
1337+
actor: actor,
1338+
focus: global.stage.get_key_focus(),
1339+
destroyId: actorDestroyId,
1340+
actionMode: mode
1341+
};
1342+
if (record.focus != null) {
1343+
record.focusDestroyId = record.focus.connect('destroy', function() {
1344+
record.focus = null;
1345+
record.focusDestroyId = null;
1346+
});
1347+
}
1348+
modalActorFocusStack.push(record);
1349+
1350+
global.stage.set_key_focus(actor);
1351+
1352+
layoutManager.updateChrome(true);
1353+
}
1354+
13211355
/**
13221356
* pushModal:
13231357
* @actor (Clutter.Actor): actor which will be given keyboard focus
@@ -1356,38 +1390,44 @@ function pushModal(actor, timestamp, options, mode) {
13561390
log('pushModal: invocation of begin_modal failed');
13571391
return false;
13581392
}
1359-
Meta.disable_unredirect_for_display(global.display);
13601393
}
13611394

1362-
global.set_stage_input_mode(Cinnamon.StageInputMode.FULLSCREEN);
1395+
_completeModalSetup(actor, mode);
1396+
return true;
1397+
}
13631398

1364-
actionMode = mode;
1399+
/**
1400+
* pushScreensaverModal:
1401+
* @actor (Clutter.Actor): actor which will be given keyboard focus.
1402+
* @timestamp (number): optional X server timestamp.
1403+
* @mode (Cinnamon.ActionMode): the action mode for the modal grab.
1404+
* @callback (function): called with (success) when the grab completes.
1405+
*
1406+
* Like pushModal(), but uses begin_modal_with_retry() to asynchronously
1407+
* retry the grab on X11, using libxdo to break stuck grabs from popup
1408+
* menus. The callback is called with true on success, false on failure.
1409+
*/
1410+
function pushScreensaverModal(actor, timestamp, mode, callback) {
1411+
if (timestamp == undefined)
1412+
timestamp = global.get_current_time();
13651413

1366-
modalCount += 1;
1367-
let actorDestroyId = actor.connect('destroy', function() {
1368-
let index = _findModal(actor);
1369-
if (index >= 0)
1370-
popModal(actor);
1371-
});
1414+
global.begin_modal_with_retry(timestamp, 0,
1415+
(obj, success) => {
1416+
if (!success) {
1417+
log('pushScreensaverModal: failed to acquire modal grab after retries (or cancelled)');
1418+
callback(false);
1419+
return;
1420+
}
13721421

1373-
let record = {
1374-
actor: actor,
1375-
focus: global.stage.get_key_focus(),
1376-
destroyId: actorDestroyId,
1377-
actionMode: mode
1378-
};
1379-
if (record.focus != null) {
1380-
record.focusDestroyId = record.focus.connect('destroy', function() {
1381-
record.focus = null;
1382-
record.focusDestroyId = null;
1422+
try {
1423+
_completeModalSetup(actor, mode);
1424+
callback(true);
1425+
} catch (e) {
1426+
global.logError(`pushScreensaverModal: error during modal setup: ${e.message}`);
1427+
global.end_modal(global.get_current_time());
1428+
callback(false);
1429+
}
13831430
});
1384-
}
1385-
modalActorFocusStack.push(record);
1386-
1387-
global.stage.set_key_focus(actor);
1388-
1389-
layoutManager.updateChrome(true);
1390-
return true;
13911431
}
13921432

13931433
/**

js/ui/screensaver/screenShield.js

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ var ScreenShield = GObject.registerClass({
129129
this._widgetLoadIdleId = 0;
130130
this._infoPanel = null;
131131
this._inhibitor = null;
132+
this._activationPending = false;
132133

133134
this._nameBlocker = new NameBlocker.NameBlocker();
134135

@@ -356,19 +357,26 @@ var ScreenShield = GObject.registerClass({
356357
}
357358

358359
lock(immediate = false, awayMessage = null) {
359-
if (this.isLocked())
360+
if (this.isLocked() || this._activationPending) {
361+
_log('ScreenShield: Already locked or activation pending, ignoring lock request');
360362
return;
363+
}
361364

362365
this._awayMessage = awayMessage;
363366

364367
_log(`ScreenShield: Locking screen (immediate=${immediate})`);
365368

366369
if (this._state === State.HIDDEN) {
367-
this.activate(immediate);
370+
this.activate(immediate, (success) => {
371+
if (success) {
372+
this._stopLockDelay();
373+
this._setLocked(true);
374+
}
375+
});
376+
} else {
377+
this._stopLockDelay();
378+
this._setLocked(true);
368379
}
369-
370-
this._stopLockDelay();
371-
this._setLocked(true);
372380
}
373381

374382
_setLocked(locked) {
@@ -397,9 +405,9 @@ var ScreenShield = GObject.registerClass({
397405
this._hideShield(true);
398406
}
399407

400-
activate(immediate = false) {
401-
if (this._state !== State.HIDDEN) {
402-
_log('ScreenShield: Already active');
408+
activate(immediate = false, lock_callback = null) {
409+
if (this._state !== State.HIDDEN || this._activationPending) {
410+
_log('ScreenShield: Already active or activation pending');
403411
return;
404412
}
405413

@@ -409,44 +417,87 @@ var ScreenShield = GObject.registerClass({
409417
this._lastMotionY = -1;
410418
this._activationTime = GLib.get_monotonic_time();
411419

412-
if (!Main.pushModal(this, global.get_current_time(), 0, Cinnamon.ActionMode.LOCK_SCREEN)) {
413-
global.logError('ScreenShield: Failed to acquire modal grab');
414-
return;
415-
}
420+
this._activationPending = true;
421+
_log('ScreenShield: requesting screensaver modal grab');
422+
Main.pushScreensaverModal(this, global.get_current_time(), Cinnamon.ActionMode.LOCK_SCREEN,
423+
(success) => {
424+
this._activationPending = false;
416425

417-
this._setState(State.SHOWN);
426+
if (success) {
427+
_log('ScreenShield: modal grab acquired, finishing activation');
418428

419-
this._createBackgrounds();
429+
if (this._finishActivation(immediate)) {
430+
if (lock_callback)
431+
lock_callback(true);
420432

421-
this._capturedEventId = global.stage.connect('captured-event',
422-
this._onCapturedEvent.bind(this));
433+
return;
434+
} else {
435+
Main.popModal(this);
436+
}
437+
}
423438

424-
global.stage.hide_cursor();
439+
global.logWarning('ScreenShield: Failed to acquire modal grab (or cancelled)');
440+
this._activationTime = 0;
441+
this._syncInhibitor();
442+
if (lock_callback)
443+
lock_callback(false);
444+
});
445+
}
425446

426-
if (Main.deskletContainer)
427-
Main.deskletContainer.actor.hide();
447+
_finishActivation(immediate) {
448+
try {
449+
this._createBackgrounds();
428450

429-
Main.layoutManager.screenShieldGroup.show();
430-
this.show();
451+
this._capturedEventId = global.stage.connect('captured-event',
452+
this._onCapturedEvent.bind(this));
431453

432-
if (immediate) {
433-
this.opacity = 255;
434-
this._activateBackupLocker();
435-
this._scheduleWidgetLoading();
436-
} else {
437-
this.opacity = 0;
438-
this.ease({
439-
opacity: 255,
440-
duration: FADE_TIME,
441-
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
442-
onComplete: () => {
443-
this._activateBackupLocker();
444-
this._scheduleWidgetLoading();
445-
}
446-
});
447-
}
454+
global.stage.hide_cursor();
455+
456+
if (Main.deskletContainer)
457+
Main.deskletContainer.actor.hide();
458+
459+
Main.layoutManager.screenShieldGroup.show();
460+
this.show();
461+
462+
if (immediate) {
463+
this.opacity = 255;
464+
this._activateBackupLocker();
465+
this._scheduleWidgetLoading();
466+
} else {
467+
this.opacity = 0;
468+
this.ease({
469+
opacity: 255,
470+
duration: FADE_TIME,
471+
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
472+
onComplete: () => {
473+
this._activateBackupLocker();
474+
this._scheduleWidgetLoading();
475+
}
476+
});
477+
}
478+
479+
this._setState(State.SHOWN);
480+
this._startLockDelay();
481+
return true;
482+
} catch (e) {
483+
global.logError(`ScreenShield: error during activation: ${e.message}`);
484+
485+
if (this._capturedEventId) {
486+
global.stage.disconnect(this._capturedEventId);
487+
this._capturedEventId = 0;
488+
}
489+
490+
global.stage.show_cursor();
491+
492+
if (Main.deskletContainer)
493+
Main.deskletContainer.actor.show();
448494

449-
this._startLockDelay();
495+
Main.layoutManager.screenShieldGroup.hide();
496+
this.hide();
497+
this._destroyBackgrounds();
498+
499+
return false;
500+
}
450501
}
451502

452503
_startLockDelay() {
@@ -495,6 +546,12 @@ var ScreenShield = GObject.registerClass({
495546
return;
496547
}
497548

549+
if (this._activationPending) {
550+
_log('ScreenShield: Cancelling pending activation');
551+
global.end_modal(global.get_current_time());
552+
return;
553+
}
554+
498555
this._stopLockDelay();
499556
this._hideShield(false);
500557
}
@@ -552,7 +609,8 @@ var ScreenShield = GObject.registerClass({
552609
_syncInhibitor() {
553610
let lockEnabled = this._settings.get_boolean('lock-enabled');
554611
let lockDisabled = Main.lockdownSettings.get_boolean('disable-lock-screen');
555-
let shouldInhibit = this._state === State.HIDDEN && lockEnabled && !lockDisabled;
612+
let shouldInhibit = (this._state === State.HIDDEN || this._activationPending) &&
613+
lockEnabled && !lockDisabled;
556614

557615
if (shouldInhibit && !this._inhibitor) {
558616
_log('ScreenShield: Acquiring sleep inhibitor');
@@ -563,7 +621,7 @@ var ScreenShield = GObject.registerClass({
563621
}
564622

565623
// Re-check after async - conditions may have changed
566-
let stillNeeded = this._state === State.HIDDEN &&
624+
let stillNeeded = (this._state === State.HIDDEN || this._activationPending) &&
567625
this._settings.get_boolean('lock-enabled') &&
568626
!Main.lockdownSettings.get_boolean('disable-lock-screen');
569627
if (stillNeeded) {

src/cinnamon-global-private.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#endif
2121

2222
#include <meta/meta-plugin.h>
23+
#include <xdo.h>
2324

2425

2526
#include "cinnamon-enum-types.h"
@@ -31,6 +32,16 @@
3132

3233
#include <cjs/gjs.h>
3334

35+
typedef struct {
36+
CinnamonGlobal *global;
37+
guint32 timestamp;
38+
MetaModalOptions options;
39+
gint attempt;
40+
gboolean tried_xdo;
41+
CinnamonModalCallback callback;
42+
gpointer user_data;
43+
} ModalRetryData;
44+
3445
struct _CinnamonGlobal {
3546
GObject parent;
3647

@@ -64,6 +75,10 @@ struct _CinnamonGlobal {
6475
gboolean has_modal;
6576

6677
guint notif_service_id;
78+
79+
xdo_t *xdo;
80+
guint modal_retry_source_id;
81+
ModalRetryData *modal_retry_data;
6782
};
6883

6984
void _cinnamon_global_init (const char *first_property_name,

0 commit comments

Comments
 (0)