screencastService: Propagate machine-parseable error types to gnome-shell

We'll be using hardware encoding for screencasts soon, so we'll likely see
more things go wrong in the future, including crashes of the whole
screencastService. To deal with this, we'll introduce logic to blocklist
certain recording pipelines in case of failure and also add some logic
to retry the recording automatically.

To allow for better messaging to the user in those failure cases, we want
to be aware in gnome-shell, what exactly the error in the recorder was.

So propagate the most common types of errors that can happen in the
ScreencastService to gnome-shell using the new DBusError module.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2976>
This commit is contained in:
Jonas Dreßler 2023-10-01 20:10:33 +02:00
parent a7e93f3af2
commit 75f86a6f60
4 changed files with 91 additions and 37 deletions

View File

@ -93,6 +93,7 @@
</method>
<signal name="Error">
<arg type="s" name="name"/>
<arg type="s" name="message"/>
</signal>

View File

@ -6,6 +6,7 @@ import Gtk from 'gi://Gtk?version=4.0';
import {ServiceImplementation} from './dbusService.js';
import {ScreencastErrors, ScreencastError} from './misc/dbusErrors.js';
import {loadInterfaceXML, loadSubInterfaceXML} from './misc/dbusUtils.js';
import * as Signals from './misc/signals.js';
@ -150,13 +151,13 @@ class Recorder extends Signals.EventEmitter {
}
}
_bailOutOnError(error) {
_bailOutOnError(message, errorDomain = ScreencastErrors, errorCode = ScreencastError.RECORDER_ERROR) {
const error = new GLib.Error(errorDomain, errorCode, message);
this._teardownPipeline();
this._unwatchSender();
this._stopSession();
log(`Recorder error: ${error.message}`);
if (this._startRequest) {
this._startRequest.reject(error);
delete this._startRequest;
@ -170,13 +171,13 @@ class Recorder extends Signals.EventEmitter {
this.emit('error', error);
}
_handleFatalPipelineError(message) {
_handleFatalPipelineError(message, errorDomain, errorCode) {
this._pipelineState = PipelineState.ERROR;
this._bailOutOnError(new Error(`Fatal pipeline error: ${message}`));
this._bailOutOnError(message, errorDomain, errorCode);
}
_senderVanished() {
this._bailOutOnError(new Error('Sender has vanished'));
this._bailOutOnError('Sender has vanished');
}
_onSessionClosed() {
@ -184,7 +185,7 @@ class Recorder extends Signals.EventEmitter {
return; // We closed the session ourselves
this._sessionState = SessionState.STOPPED;
this._bailOutOnError(new Error('Session closed unexpectedly'));
this._bailOutOnError('Session closed unexpectedly');
}
_initSession(sessionPath) {
@ -197,7 +198,8 @@ class Recorder extends Signals.EventEmitter {
_tryNextPipeline() {
const {done, value: pipelineConfig} = this._pipelineConfigs.next();
if (done) {
this._handleFatalPipelineError('All pipelines failed to start');
this._handleFatalPipelineError('All pipelines failed to start',
ScreencastErrors, ScreencastError.ALL_PIPELINES_FAILED);
return;
}
@ -325,7 +327,8 @@ class Recorder extends Signals.EventEmitter {
case PipelineState.PLAYING:
this._addRecentItem();
this._handleFatalPipelineError('Unexpected EOS message');
this._handleFatalPipelineError('Unexpected EOS message',
ScreencastErrors, ScreencastError.PIPELINE_ERROR);
break;
case PipelineState.FLUSHING:
@ -358,11 +361,14 @@ class Recorder extends Signals.EventEmitter {
break;
case PipelineState.PLAYING:
case PipelineState.FLUSHING:
// Everything else we can't handle, so error out
case PipelineState.FLUSHING: {
const [error] = message.parse_error();
this._handleFatalPipelineError(
`GStreamer error while in state ${this._pipelineState}: ${message.parse_error()[0].message}`);
`GStreamer error while in state ${this._pipelineState}: ${error.message}`,
ScreencastErrors, ScreencastError.PIPELINE_ERROR);
break;
}
default:
break;
@ -526,17 +532,19 @@ export const ScreencastService = class extends ServiceImplementation {
}
async ScreencastAsync(params, invocation) {
let returnValue = [false, ''];
if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.SAVE_TO_DISK_DISABLED,
'Saving to disk is disabled');
return;
}
const sender = invocation.get_sender();
if (this._recorders.get(sender)) {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.ALREADY_RECORDING,
'Service is already recording');
return;
}
@ -558,7 +566,10 @@ export const ScreencastService = class extends ServiceImplementation {
invocation);
} catch (error) {
log(`Failed to create recorder: ${error.message}`);
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.RECORDER_ERROR,
error.message);
return;
}
@ -566,33 +577,46 @@ export const ScreencastService = class extends ServiceImplementation {
try {
await recorder.startRecording();
returnValue = [true, filePath];
invocation.return_value(GLib.Variant.new('(bs)', [true, filePath]));
} catch (error) {
log(`Failed to start recorder: ${error.message}`);
this._removeRecorder(sender);
} finally {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
if (error instanceof GLib.Error) {
invocation.return_gerror(error);
} else {
invocation.return_error_literal(ScreencastErrors,
ScreencastError.RECORDER_ERROR,
error.message);
}
return;
}
recorder.connect('error', (r, error) => {
log(`Fatal error while recording: ${error.message}`);
this._removeRecorder(sender);
this._dbusImpl.emit_signal('Error',
new GLib.Variant('(s)', [error.message]));
new GLib.Variant('(ss)', [
Gio.DBusError.encode_gerror(error),
error.message,
]));
});
}
async ScreencastAreaAsync(params, invocation) {
let returnValue = [false, ''];
if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.SAVE_TO_DISK_DISABLED,
'Saving to disk is disabled');
return;
}
const sender = invocation.get_sender();
if (this._recorders.get(sender)) {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.ALREADY_RECORDING,
'Service is already recording');
return;
}
@ -613,7 +637,10 @@ export const ScreencastService = class extends ServiceImplementation {
invocation);
} catch (error) {
log(`Failed to create recorder: ${error.message}`);
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
invocation.return_error_literal(ScreencastErrors,
ScreencastError.RECORDER_ERROR,
error.message);
return;
}
@ -621,18 +648,29 @@ export const ScreencastService = class extends ServiceImplementation {
try {
await recorder.startRecording();
returnValue = [true, filePath];
invocation.return_value(GLib.Variant.new('(bs)', [true, filePath]));
} catch (error) {
log(`Failed to start recorder: ${error.message}`);
this._removeRecorder(sender);
} finally {
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
if (error instanceof GLib.Error) {
invocation.return_gerror(error);
} else {
invocation.return_error_literal(ScreencastErrors,
ScreencastError.RECORDER_ERROR,
error.message);
}
return;
}
recorder.connect('error', (r, error) => {
log(`Fatal error while recording: ${error.message}`);
this._removeRecorder(sender);
this._dbusImpl.emit_signal('Error',
new GLib.Variant('(s)', [error.message]));
new GLib.Variant('(ss)', [
Gio.DBusError.encode_gerror(error),
error.message,
]));
});
}

View File

@ -43,3 +43,14 @@ export const ExtensionError = {
};
export const ExtensionErrors =
registerErrorDomain('Extensions', ExtensionError);
export const ScreencastError = {
ALL_PIPELINES_FAILED: 0,
PIPELINE_ERROR: 1,
SAVE_TO_DISK_DISABLED: 2,
ALREADY_RECORDING: 3,
RECORDER_ERROR: 4,
SERVICE_CRASH: 5,
};
export const ScreencastErrors =
registerErrorDomain('Screencast', ScreencastError);

View File

@ -25,6 +25,7 @@ Gio._promisify(Shell.Screenshot.prototype, 'screenshot_area');
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_stage_to_content');
Gio._promisify(Shell.Screenshot, 'composite_to_stream');
import {ScreencastErrors, ScreencastError} from '../misc/dbusErrors.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';
import {DBusSenderChecker} from '../misc/util.js';
@ -1099,8 +1100,8 @@ export const ScreenshotUI = GObject.registerClass({
this._syncCastButton();
});
this._screencastProxy.connectSignal('Error',
() => this._screencastFailed());
this._screencastProxy.connectSignal('Error', (proxy, sender, [errorName, message]) =>
this._screencastFailed(Gio.DBusError.new_for_dbus_error(errorName, message)));
this._screencastProxy.connect('notify::g-name-owner', () => {
if (this._screencastProxy.g_name_owner)
@ -1109,7 +1110,9 @@ export const ScreenshotUI = GObject.registerClass({
if (!this._screencastInProgress)
return;
this._screencastFailed();
this._screencastFailed(
new GLib.Error(ScreencastErrors, ScreencastError.SERVICE_CRASH,
'Service crashed'));
});
this._lockdownSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.lockdown'});
@ -1974,7 +1977,7 @@ export const ScreenshotUI = GObject.registerClass({
this._setScreencastInProgress(true);
try {
const [success, path] = await method(
const [, path] = await method(
GLib.build_filenamev([
/* Translators: this is the folder where recorded
screencasts are stored. */
@ -1986,8 +1989,7 @@ export const ScreenshotUI = GObject.registerClass({
_('Screencast from %d %t.webm'),
]),
{'draw-cursor': new GLib.Variant('b', drawCursor)});
if (!success)
throw new Error();
this._screencastPath = path;
} catch (error) {
this._setScreencastInProgress(false);
@ -2024,7 +2026,9 @@ export const ScreenshotUI = GObject.registerClass({
this._showNotification(_('Screencast recorded'));
}
_screencastFailed() {
_screencastFailed(error) {
console.error(`Screencast failed: ${error}`);
this._setScreencastInProgress(false);
// Translators: notification title.