From 0920a6260ad50362afa9699a089a410b0828d2f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Wed, 8 Oct 2025 09:12:21 +0300 Subject: [PATCH 01/11] extension/service: refactor `.#wait_subprocess()` to `async`/`await` --- ddterm/shell/service.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/ddterm/shell/service.js b/ddterm/shell/service.js index 55935f308..e56f34223 100644 --- a/ddterm/shell/service.js +++ b/ddterm/shell/service.js @@ -187,21 +187,18 @@ export const Service = GObject.registerClass({ return new Subprocess(params); } - #wait_subprocess() { + async #wait_subprocess(cancellable) { this.#subprocess_wait_cancel = new Gio.Cancellable(); - return this.subprocess.wait_check(this.#subprocess_wait_cancel).catch(ex => { - if (this.starting) - return; - - if (ex.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED)) - return; - - this.emit('error', ex); - }).finally(() => { + try { + await this.subprocess.wait_check(cancellable); + } catch (ex) { + if (!this.starting && !ex.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED)) + this.emit('error', ex); + } finally { this.#subprocess_running = false; this.notify('is-running'); - }); + } } #update_bus_name_owner(owner) { From 7caa68da15a1e9a3423574e63f2b431c7d0cc67b Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Thu, 9 Oct 2025 05:29:33 +0300 Subject: [PATCH 02/11] extension/appcontrol: simplify `wait_{timeout,property}()` And always disconnect from the cancellable. --- ddterm/shell/appcontrol.js | 63 +++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/ddterm/shell/appcontrol.js b/ddterm/shell/appcontrol.js index ad95ebf92..e40cdd834 100644 --- a/ddterm/shell/appcontrol.js +++ b/ddterm/shell/appcontrol.js @@ -13,50 +13,57 @@ import { WindowGeometry } from './geometry.js'; import { WindowMatch } from './windowmatch.js'; async function wait_timeout(message, timeout_ms, cancellable = null) { - await new Promise(resolve => { - const source = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout_ms, () => { - cancellable?.disconnect(cancel_handler); - resolve(); - return GLib.SOURCE_REMOVE; - }); + let source, cancel_handler; + + try { + await new Promise(resolve => { + source = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout_ms, () => { + resolve(); + return GLib.SOURCE_CONTINUE; + }); - const cancel_handler = cancellable?.connect(() => { - GLib.Source.remove(source); - resolve(); + cancel_handler = cancellable?.connect(() => { + resolve(); + }); }); - }); + } finally { + GLib.Source.remove(source); + } + cancellable?.disconnect(cancel_handler); cancellable?.set_error_if_cancelled(); + throw GLib.Error.new_literal(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT, message); } async function wait_property(object, property, predicate, cancellable = null) { - const result = await new Promise(resolve => { - let value = object[property]; + let value = object[property]; - if (predicate(value)) { - resolve(value); - return; - } + if (predicate(value)) + return value; - const handler = object.connect(`notify::${property}`, () => { - value = object[property]; + let result, handler, cancel_handler; - if (!predicate(value)) - return; + try { + result = await new Promise(resolve => { + handler = object.connect(`notify::${property}`, () => { + value = object[property]; - cancellable?.disconnect(cancel_handler); - object.disconnect(handler); - resolve(value); - }); + if (predicate(value)) + resolve(value); + }); - const cancel_handler = cancellable?.connect(() => { - object.disconnect(handler); - resolve(); + cancel_handler = cancellable?.connect(() => { + resolve(); + }); }); - }); + } finally { + object.disconnect(handler); + } + cancellable?.disconnect(cancel_handler); cancellable?.set_error_if_cancelled(); + return result; } From d6e5530f1ff5841c76f622c0be91ccce589f03c4 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Thu, 9 Oct 2025 05:33:15 +0300 Subject: [PATCH 03/11] util/promise: extract wait functions from shell/appcontrol --- ddterm/shell/appcontrol.js | 57 +---------------------------------- ddterm/util/meson.build | 2 +- ddterm/util/promise.js | 61 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 ddterm/util/promise.js diff --git a/ddterm/shell/appcontrol.js b/ddterm/shell/appcontrol.js index e40cdd834..fce3a0569 100644 --- a/ddterm/shell/appcontrol.js +++ b/ddterm/shell/appcontrol.js @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; @@ -11,61 +10,7 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { Service } from './service.js'; import { WindowGeometry } from './geometry.js'; import { WindowMatch } from './windowmatch.js'; - -async function wait_timeout(message, timeout_ms, cancellable = null) { - let source, cancel_handler; - - try { - await new Promise(resolve => { - source = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout_ms, () => { - resolve(); - return GLib.SOURCE_CONTINUE; - }); - - cancel_handler = cancellable?.connect(() => { - resolve(); - }); - }); - } finally { - GLib.Source.remove(source); - } - - cancellable?.disconnect(cancel_handler); - cancellable?.set_error_if_cancelled(); - - throw GLib.Error.new_literal(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT, message); -} - -async function wait_property(object, property, predicate, cancellable = null) { - let value = object[property]; - - if (predicate(value)) - return value; - - let result, handler, cancel_handler; - - try { - result = await new Promise(resolve => { - handler = object.connect(`notify::${property}`, () => { - value = object[property]; - - if (predicate(value)) - resolve(value); - }); - - cancel_handler = cancellable?.connect(() => { - resolve(); - }); - }); - } finally { - object.disconnect(handler); - } - - cancellable?.disconnect(cancel_handler); - cancellable?.set_error_if_cancelled(); - - return result; -} +import { wait_timeout, wait_property } from '../util/promise.js'; export const AppControl = GObject.registerClass({ Properties: { diff --git a/ddterm/util/meson.build b/ddterm/util/meson.build index a83110449..18223b662 100644 --- a/ddterm/util/meson.build +++ b/ddterm/util/meson.build @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later util_js_out_files = [] -util_js_src_files = files('displayconfig.js') +util_js_src_files = files('displayconfig.js', 'promise.js') if get_option('typelib_installer') typelib_installer = subproject('gjs-typelib-installer') diff --git a/ddterm/util/promise.js b/ddterm/util/promise.js new file mode 100644 index 000000000..1ddb6d223 --- /dev/null +++ b/ddterm/util/promise.js @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2025 Aleksandr Mezin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; + +export async function wait_timeout(message, timeout_ms, cancellable = null) { + let source, cancel_handler; + + try { + await new Promise(resolve => { + source = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout_ms, () => { + resolve(); + return GLib.SOURCE_CONTINUE; + }); + + cancel_handler = cancellable?.connect(() => { + resolve(); + }); + }); + } finally { + GLib.Source.remove(source); + } + + cancellable?.disconnect(cancel_handler); + cancellable?.set_error_if_cancelled(); + + throw GLib.Error.new_literal(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT, message); +} + +export async function wait_property(object, property, predicate, cancellable = null) { + let value = object[property]; + + if (predicate(value)) + return value; + + let result, handler, cancel_handler; + + try { + result = await new Promise(resolve => { + handler = object.connect(`notify::${property}`, () => { + value = object[property]; + + if (predicate(value)) + resolve(value); + }); + + cancel_handler = cancellable?.connect(() => { + resolve(); + }); + }); + } finally { + object.disconnect(handler); + } + + cancellable?.disconnect(cancel_handler); + cancellable?.set_error_if_cancelled(); + + return result; +} From d20eacc3a43ad3c629403f6662f2cf4300b238ec Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Thu, 9 Oct 2025 05:42:56 +0300 Subject: [PATCH 04/11] util/promise: get rid of pointless `result` in `wait_property()` --- ddterm/util/promise.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ddterm/util/promise.js b/ddterm/util/promise.js index 1ddb6d223..19d6b981b 100644 --- a/ddterm/util/promise.js +++ b/ddterm/util/promise.js @@ -35,15 +35,15 @@ export async function wait_property(object, property, predicate, cancellable = n if (predicate(value)) return value; - let result, handler, cancel_handler; + let handler, cancel_handler; try { - result = await new Promise(resolve => { + await new Promise(resolve => { handler = object.connect(`notify::${property}`, () => { value = object[property]; if (predicate(value)) - resolve(value); + resolve(); }); cancel_handler = cancellable?.connect(() => { @@ -57,5 +57,5 @@ export async function wait_property(object, property, predicate, cancellable = n cancellable?.disconnect(cancel_handler); cancellable?.set_error_if_cancelled(); - return result; + return value; } From d6a87eaf27a08c96f8f51731418258fe14762fc3 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Thu, 9 Oct 2025 05:46:10 +0300 Subject: [PATCH 05/11] util/promise: propagate property read errors in `wait_property()` --- ddterm/util/promise.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ddterm/util/promise.js b/ddterm/util/promise.js index 19d6b981b..4405c0716 100644 --- a/ddterm/util/promise.js +++ b/ddterm/util/promise.js @@ -38,12 +38,16 @@ export async function wait_property(object, property, predicate, cancellable = n let handler, cancel_handler; try { - await new Promise(resolve => { + await new Promise((resolve, reject) => { handler = object.connect(`notify::${property}`, () => { - value = object[property]; + try { + value = object[property]; - if (predicate(value)) - resolve(); + if (predicate(value)) + resolve(); + } catch (error) { + reject(error); + } }); cancel_handler = cancellable?.connect(() => { @@ -52,9 +56,9 @@ export async function wait_property(object, property, predicate, cancellable = n }); } finally { object.disconnect(handler); + cancellable?.disconnect(cancel_handler); } - cancellable?.disconnect(cancel_handler); cancellable?.set_error_if_cancelled(); return value; From 83487b51c098ab662a1001c768e04712a12b8bcc Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Wed, 8 Oct 2025 09:33:15 +0300 Subject: [PATCH 06/11] extension/service: handle concurrent `.start()` calls and cancellation --- ddterm/shell/service.js | 65 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/ddterm/shell/service.js b/ddterm/shell/service.js index e56f34223..e9f84c547 100644 --- a/ddterm/shell/service.js +++ b/ddterm/shell/service.js @@ -6,6 +6,7 @@ import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; import { Subprocess, WaylandSubprocess } from './subprocess.js'; +import { wait_property } from '../util/promise.js'; export const Service = GObject.registerClass({ Properties: { @@ -217,15 +218,22 @@ export const Service = GObject.registerClass({ } async start(cancellable = null) { - if (this.is_registered) - return; - - this.#starting = true; - this.notify('starting'); + const inner_cancellable = Gio.Cancellable.new(); + const cancellable_chain = cancellable?.connect(() => inner_cancellable.cancel()); try { - const inner_cancellable = Gio.Cancellable.new(); - const cancellable_chain = cancellable?.connect(() => inner_cancellable.cancel()); + inner_cancellable.set_error_if_cancelled(); + + while (this.starting) { + // eslint-disable-next-line no-await-in-loop + await wait_property(this, 'starting', starting => !starting, inner_cancellable); + } + + if (this.is_registered) + return; + + this.#starting = true; + this.notify('starting'); try { if (!this.is_running) { @@ -236,34 +244,31 @@ export const Service = GObject.registerClass({ this.#subprocess_wait = this.#wait_subprocess(); } - const registered = new Promise(resolve => { - const handler = this.connect('notify::is-registered', () => { - if (this.is_registered) - resolve(); - }); + await Promise.race([ + wait_property(this, 'is-registered', Boolean, inner_cancellable), + this.#subprocess_wait, + ]); - inner_cancellable.connect(() => { - this.disconnect(handler); - }); - }); + inner_cancellable.set_error_if_cancelled(); - await Promise.race([registered, this.#subprocess_wait]); - } finally { - cancellable?.disconnect(cancellable_chain); - inner_cancellable.cancel(); - } + if (!this.is_running) { + throw new Error( + `${this.bus_name}: subprocess terminated without registering on D-Bus` + ); + } - if (!this.is_registered) { - throw new Error( - `${this.bus_name}: subprocess terminated without registering on D-Bus` - ); + if (!this.is_registered) + throw new Error(`${this.bus_name}: subprocess failed to register on D-Bus`); + } catch (ex) { + this.emit('error', ex); + throw ex; + } finally { + this.#starting = false; + this.notify('starting'); } - } catch (ex) { - this.emit('error', ex); - throw ex; } finally { - this.#starting = false; - this.notify('starting'); + cancellable?.disconnect(cancellable_chain); + inner_cancellable.cancel(); } } }); From eebc6bb89e588278276d4a8327e7035167dc9d5b Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Thu, 9 Oct 2025 06:10:33 +0300 Subject: [PATCH 07/11] util/promise: add null/undefined/0 checks Gio.Cancellable.connect() will return 0 if already cancelled. Exception can be thrown at any point, leaving one or both handlers undefined. --- ddterm/util/promise.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/ddterm/util/promise.js b/ddterm/util/promise.js index 4405c0716..98b6bb8f2 100644 --- a/ddterm/util/promise.js +++ b/ddterm/util/promise.js @@ -12,7 +12,8 @@ export async function wait_timeout(message, timeout_ms, cancellable = null) { await new Promise(resolve => { source = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout_ms, () => { resolve(); - return GLib.SOURCE_CONTINUE; + source = null; + return GLib.SOURCE_REMOVE; }); cancel_handler = cancellable?.connect(() => { @@ -20,10 +21,13 @@ export async function wait_timeout(message, timeout_ms, cancellable = null) { }); }); } finally { - GLib.Source.remove(source); + if (source) + GLib.Source.remove(source); + + if (cancel_handler) + cancellable.disconnect(cancel_handler); } - cancellable?.disconnect(cancel_handler); cancellable?.set_error_if_cancelled(); throw GLib.Error.new_literal(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT, message); @@ -55,8 +59,11 @@ export async function wait_property(object, property, predicate, cancellable = n }); }); } finally { - object.disconnect(handler); - cancellable?.disconnect(cancel_handler); + if (handler) + object.disconnect(handler); + + if (cancel_handler) + cancellable.disconnect(cancel_handler); } cancellable?.set_error_if_cancelled(); From ce99844712e3554b9dc1b62c901c690e64c06778 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Sun, 12 Oct 2025 04:22:51 +0300 Subject: [PATCH 08/11] util/promise: add `promisify()` helper --- ddterm/util/promise.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ddterm/util/promise.js b/ddterm/util/promise.js index 98b6bb8f2..8c09ce9a7 100644 --- a/ddterm/util/promise.js +++ b/ddterm/util/promise.js @@ -5,6 +5,21 @@ import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; +export function promisify(start, finish) { + return function (...args) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-invalid-this + start.call(this, ...args, (source, result) => { + try { + resolve(finish.call(source, result)); + } catch (error) { + reject(error); + } + }); + }); + }; +} + export async function wait_timeout(message, timeout_ms, cancellable = null) { let source, cancel_handler; From c27f0a4bd5ba96718e0dd2dc57fe6ac1076e37b2 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Sun, 12 Oct 2025 07:20:31 +0300 Subject: [PATCH 09/11] subprocess: use `promisify()` and generators --- ddterm/shell/extension.js | 21 ++-- ddterm/shell/subprocess.js | 206 +++++++++++++++++-------------------- 2 files changed, 103 insertions(+), 124 deletions(-) diff --git a/ddterm/shell/extension.js b/ddterm/shell/extension.js index 29d062671..776ba300a 100644 --- a/ddterm/shell/extension.js +++ b/ddterm/shell/extension.js @@ -283,19 +283,14 @@ class EnabledExtension { }); this.service.connect('error', (service, ex) => { - const log_collector = service.subprocess?.log_collector; - - if (!log_collector) { - this.notifications.show_error(ex); - return; - } - - log_collector.collect().then(output => { - this.notifications.show_error(ex, output); - }).catch(ex2 => { - logError(ex2, 'Failed to collect logs'); - this.notifications.show_error(ex); - }); + (service.subprocess?.get_logs() ?? Promise.resolve()).then( + output => { + this.notifications.show_error(ex, output); + }, ex2 => { + logError(ex2, 'Failed to collect logs'); + this.notifications.show_error(ex); + } + ); }); this.window_geometry = new WindowGeometry(); diff --git a/ddterm/shell/subprocess.js b/ddterm/shell/subprocess.js index f03eea329..6004f6183 100644 --- a/ddterm/shell/subprocess.js +++ b/ddterm/shell/subprocess.js @@ -11,6 +11,7 @@ import Meta from 'gi://Meta'; import Gi from 'gi'; import { sd_journal_stream_fd } from './sd_journal.js'; +import { promisify } from '../util/promise.js'; function try_require(namespace, version = undefined) { try { @@ -60,115 +61,102 @@ function shell_join(argv) { return argv.map(arg => GLib.shell_quote(arg)).join(' '); } -class JournalctlLogCollector { - constructor(journalctl, since, pid) { - this._argv = [ - journalctl, - '--user', - '-b', - `--since=${since.format('%C%y-%m-%d %H:%M:%S UTC')}`, - '-ocat', - `-n${KEEP_LOG_LINES}`, - `_PID=${pid}`, - ]; - } +async function collect_journald_logs(journalctl, since, pid) { + const argv = [ + journalctl, + '--user', + '-b', + `--since=${since.format('%C%y-%m-%d %H:%M:%S UTC')}`, + '-ocat', + `-n${KEEP_LOG_LINES}`, + ]; + + if (pid) + argv.push(`_PID=${pid}`); + + const proc = Gio.Subprocess.new( + argv, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE + ); - _begin(resolve, reject) { - const proc = Gio.Subprocess.new( - this._argv, - Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE - ); + const communicate = promisify(proc.communicate_async, proc.communicate_finish); + const [, stdout_buf] = await communicate.call(proc, null, null); - proc.communicate_utf8_async(null, null, this._finish.bind(this, resolve, reject)); - } + return new TextDecoder().decode(stdout_buf); +} - _finish(resolve, reject, source, result) { - try { - const [, stdout_buf] = source.communicate_utf8_finish(result); - resolve(stdout_buf); - } catch (ex) { - reject(ex); - } - } +async function *read_chunks(input_stream) { + const read_bytes = + promisify(input_stream.read_bytes_async, input_stream.read_bytes_finish); - collect() { - return new Promise(this._begin.bind(this)); - } -} + try { + for (;;) { + // eslint-disable-next-line no-await-in-loop + const chunk = await read_bytes.call(input_stream, 4096, GLib.PRIORITY_DEFAULT, null); -class TeeLogCollector { - constructor(stream) { - this._input = stream; - this._output = new UnixOutputStream({ fd: STDERR_FD, close_fd: false }); - this._collected = []; - this._collected_lines = 0; - this._promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - - this._read_more(); - } + if (chunk.get_size() === 0) + return; - _read_more() { - this._input.read_bytes_async(4096, GLib.PRIORITY_DEFAULT, null, this._read_done.bind(this)); + yield chunk.toArray(); + } + } finally { + input_stream.close(null); } +} - _read_done(source, result) { - try { - const chunk = source.read_bytes_finish(result).toArray(); +function *split_array_keep_delimiter(bytes, delimiter) { + let start = 0; - if (chunk.length === 0) { - this._input.close(null); - this._output.close(null); - this._resolve(); - return; - } + for (;;) { + let end = bytes.indexOf(delimiter, start); + + if (end === -1) + break; - const delimiter = '\n'.charCodeAt(0); - let start = 0; + yield bytes.subarray(start, end + 1); - for (;;) { - let end = chunk.indexOf(delimiter, start); + start = end + 1; + } - if (end === -1) { - if (start < chunk.length) - this._collected.push(chunk.subarray(start)); + yield bytes.subarray(start); +} - break; - } +async function collect_stdio_logs(input_stream) { + const delimiter = '\n'.charCodeAt(0); + const collected = []; + let lines = 0; + const stderr = new UnixOutputStream({ fd: STDERR_FD, close_fd: false }); - this._collected.push(chunk.subarray(start, end + 1)); - this._collected_lines += 1; + for await (const chunk of read_chunks(input_stream)) { + // I hope sync/blocking writes to stderr are fine. + // After all, this is the same thing that printerr() does. + stderr.write_all(chunk, null); - start = end + 1; - } + for (const sub_chunk of split_array_keep_delimiter(chunk, delimiter)) { + collected.push(sub_chunk); - let remove = 0; + if (sub_chunk.at(-1) === delimiter) + lines += 1; + } - while (this._collected_lines > KEEP_LOG_LINES) { - const remove_chunk = this._collected[remove]; + let remove = 0; - remove += 1; + while (lines > KEEP_LOG_LINES) { + const remove_chunk = collected[remove]; - if (remove_chunk[remove_chunk.length - 1] === delimiter) - this._collected_lines -= 1; - } + remove += 1; - this._collected.splice(0, remove); - this._output.write(chunk, null); - this._read_more(); - } catch (ex) { - this._reject(ex); + if (remove_chunk.at(-1) === delimiter) + lines -= 1; } + + if (remove > 0) + collected.splice(0, remove); } - async collect() { - await this._promise; + const decoder = new TextDecoder(); - const decoder = new TextDecoder(); - return this._collected.map(line => decoder.decode(line)).join('\n'); - } + return collected.map(v => decoder.decode(v)).join(''); } export const Subprocess = GObject.registerClass({ @@ -213,10 +201,6 @@ export const Subprocess = GObject.registerClass({ ? make_subprocess_launcher_journald(this.journal_identifier) : make_subprocess_launcher_fallback(); - const launch_context = global.create_app_launch_context(0, -1); - - subprocess_launcher.set_environ(launch_context.get_environment()); - for (const extra_env of this.environ) { const split_pos = extra_env.indexOf('='); const name = extra_env.slice(0, split_pos); @@ -231,13 +215,18 @@ export const Subprocess = GObject.registerClass({ subprocess_launcher.close(); } - this.log_collector = logging_to_journald - ? new JournalctlLogCollector(journalctl, start_date, this._subprocess.get_identifier()) - : new TeeLogCollector(this._subprocess.get_stdout_pipe()); + const pid = this._subprocess.get_identifier(); + + this._get_logs = logging_to_journald + ? collect_journald_logs.bind(globalThis, journalctl, start_date, pid) + : collect_stdio_logs(this._subprocess.get_stdout_pipe()).catch(logError); + + if (!pid) + return; GnomeDesktop.start_systemd_scope( this.journal_identifier, - parseInt(this._subprocess.get_identifier(), 10), + parseInt(pid, 10), null, null, null, @@ -261,33 +250,28 @@ export const Subprocess = GObject.registerClass({ } wait(cancellable = null) { - return new Promise((resolve, reject) => { - this.g_subprocess.wait_async(cancellable, (source, result) => { - try { - resolve(source.wait_finish(result)); - } catch (ex) { - reject(ex); - } - }); - }); + const { wait_async, wait_finish } = this.g_subprocess; + + return promisify(wait_async, wait_finish).call(this.g_subprocess, cancellable); } wait_check(cancellable = null) { - return new Promise((resolve, reject) => { - this.g_subprocess.wait_check_async(cancellable, (source, result) => { - try { - resolve(source.wait_check_finish(result)); - } catch (ex) { - reject(ex); - } - }); - }); + const { wait_check_async, wait_check_finish } = this.g_subprocess; + + return promisify(wait_check_async, wait_check_finish).call(this.g_subprocess, cancellable); } terminate() { this.g_subprocess.send_signal(SIGTERM); } + get_logs() { + if (this._get_logs instanceof Function) + return this._get_logs(); + + return this._get_logs; + } + _spawn(subprocess_launcher) { log(`Starting subprocess: ${shell_join(this.argv)}`); return subprocess_launcher.spawnv(this.argv); From 3496e4e2e63585dbb39d5a6fdce6395cfa11ba08 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Tue, 14 Oct 2025 01:00:03 +0300 Subject: [PATCH 10/11] extension/install: return `Gio.DesktopAppInfo` for installed entry --- ddterm/shell/extension.js | 4 +++- ddterm/shell/install.js | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/ddterm/shell/extension.js b/ddterm/shell/extension.js index 776ba300a..212b55260 100644 --- a/ddterm/shell/extension.js +++ b/ddterm/shell/extension.js @@ -136,7 +136,7 @@ function create_panel_icon(settings, window_matcher, app_control, icon, gettext_ function install(extension, rollback) { const installer = new Installer(extension.launcher_path); - installer.install(); + const app_info = installer.install(); if (GObject.signal_lookup('shutdown', Shell.Global)) { const shutdown_handler = global.connect('shutdown', () => { @@ -161,6 +161,8 @@ function install(extension, rollback) { installer.uninstall(); }); + + return app_info; } function bind_keys(settings, app_control, rollback) { diff --git a/ddterm/shell/install.js b/ddterm/shell/install.js index 08cef7f91..a818bbc6b 100644 --- a/ddterm/shell/install.js +++ b/ddterm/shell/install.js @@ -7,6 +7,20 @@ import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import Shell from 'gi://Shell'; +import Gi from 'gi'; + +function try_require(namespace, version = undefined) { + try { + return Gi.require(namespace, version); + } catch (ex) { + logError(ex); + return null; + } +} + +const GioUnix = GLib.check_version(2, 79, 2) === null ? try_require('GioUnix') : null; +const DesktopAppInfo = GioUnix?.DesktopAppInfo ?? Gio.DesktopAppInfo; + class File { constructor(source_url, target_file, fallback_files = []) { const [source_file] = GLib.filename_from_uri( @@ -26,21 +40,24 @@ class File { get_existing_content() { for (const existing_file of [this.target_file, ...this.fallback_files]) { try { - return Shell.get_file_contents_utf8_sync(existing_file); + return { + filename: existing_file, + content: Shell.get_file_contents_utf8_sync(existing_file), + }; } catch (ex) { if (!ex.matches(GLib.file_error_quark(), GLib.FileError.NOENT)) logError(ex, `Can't read ${JSON.stringify(existing_file)}`); } } - return null; + return { filename: this.target_file, content: null }; } install() { - const existing_content = this.get_existing_content(); + const { filename, content } = this.get_existing_content(); - if (this.content === existing_content) - return false; + if (this.content === content) + return { filename, changed: false }; GLib.mkdir_with_parents( GLib.path_get_dirname(this.target_file), @@ -56,7 +73,7 @@ class File { 0o600 ); - return true; + return { filename: this.target_file, changed: true }; } uninstall() { @@ -109,9 +126,11 @@ export class Installer { } install() { - this.desktop_entry.install(); + const dbus_service = this.dbus_service.install(); + const desktop_entry = this.desktop_entry.install(); + const app_info = DesktopAppInfo.new_from_filename(desktop_entry.filename); - if (this.dbus_service.install()) { + if (dbus_service.changed) { Gio.DBus.session.call( 'org.freedesktop.DBus', '/org/freedesktop/DBus', @@ -125,6 +144,8 @@ export class Installer { null ); } + + return app_info; } uninstall() { From 3953fc8d5b274bb6bc417916395dff38de451d93 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Tue, 14 Oct 2025 01:07:20 +0300 Subject: [PATCH 11/11] extension/service: use `AppLaunchContext` again --- ddterm/shell/extension.js | 6 ++--- ddterm/shell/service.js | 49 +++++++++++++++++++++++++------------- ddterm/shell/subprocess.js | 29 +++++++--------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/ddterm/shell/extension.js b/ddterm/shell/extension.js index 212b55260..dfb252401 100644 --- a/ddterm/shell/extension.js +++ b/ddterm/shell/extension.js @@ -237,6 +237,8 @@ class EnabledExtension { ) )); + const app_info = install(this.extension, rollback); + this.notifications = new Notifications({ icon: this.symbolic_icon, gettext_domain: this.extension, @@ -249,7 +251,7 @@ class EnabledExtension { this.service = new Service({ bus: Gio.DBus.session, bus_name: APP_ID, - executable: this.extension.launcher_path, + app_info, subprocess: this.extension.app_process, }); @@ -405,8 +407,6 @@ class EnabledExtension { this.extension, rollback ); - - install(this.extension, rollback); } #set_skip_taskbar() { diff --git a/ddterm/shell/service.js b/ddterm/shell/service.js index e9f84c547..1338477ad 100644 --- a/ddterm/shell/service.js +++ b/ddterm/shell/service.js @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; @@ -24,12 +25,12 @@ export const Service = GObject.registerClass({ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, null ), - 'executable': GObject.ParamSpec.string( - 'executable', + 'app-info': GObject.ParamSpec.object( + 'app-info', null, null, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - null + Gio.AppInfo ), 'wayland': GObject.ParamSpec.boolean( 'wayland', @@ -118,8 +119,8 @@ export const Service = GObject.registerClass({ this.bus, this.bus_name, Gio.BusNameWatcherFlags.NONE, - (connection, name, owner) => this.#update_bus_name_owner(owner), - () => this.#update_bus_name_owner(null) + (connection, name, owner) => this.#update_bus_name_owner(name, owner), + (connection, name) => this.#update_bus_name_owner(name, null) ); } @@ -169,23 +170,39 @@ export const Service = GObject.registerClass({ } #create_subprocess() { - const argv = [ - this.executable, + const [, argv] = GLib.shell_parse_argv(this.app_info.get_commandline()); + + argv.push( '--gapplication-service', this.wayland ? '--allowed-gdk-backends=wayland' : '--allowed-gdk-backends=x11', - ...this.extra_argv, - ]; + ...this.extra_argv + ); + + const launch_context = global.create_app_launch_context(0, -1); + + for (const extra_env of this.extra_env) { + const split_pos = extra_env.indexOf('='); + const name = extra_env.slice(0, split_pos); + const value = extra_env.slice(split_pos + 1); + + launch_context.setenv(name, value); + } const params = { journal_identifier: this.bus_name, argv, - environ: this.extra_env, + environ: launch_context.get_environment(), }; - if (this.wayland) - return new WaylandSubprocess(params); - else - return new Subprocess(params); + launch_context.emit('launch-started', this.app_info, null); + + const proc = this.wayland ? new WaylandSubprocess(params) : new Subprocess(params); + + const platform_data = GLib.VariantDict.new(null); + platform_data.insert_value('pid', GLib.Variant.new_int32(proc.get_pid())); + launch_context.emit('launched', this.app_info, platform_data.end()); + + return proc; } async #wait_subprocess(cancellable) { @@ -202,13 +219,13 @@ export const Service = GObject.registerClass({ } } - #update_bus_name_owner(owner) { + #update_bus_name_owner(name, owner) { if (this.#bus_name_owner === owner) return; const prev_registered = this.is_registered; - log(`${this.bus_name}: name owner changed to ${JSON.stringify(owner)}`); + log(`${name}: name owner changed to ${JSON.stringify(owner)}`); this.#bus_name_owner = owner; this.notify('bus-name-owner'); diff --git a/ddterm/shell/subprocess.js b/ddterm/shell/subprocess.js index 6004f6183..c15f3a993 100644 --- a/ddterm/shell/subprocess.js +++ b/ddterm/shell/subprocess.js @@ -5,7 +5,6 @@ import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; -import GnomeDesktop from 'gi://GnomeDesktop'; import Meta from 'gi://Meta'; import Gi from 'gi'; @@ -201,15 +200,9 @@ export const Subprocess = GObject.registerClass({ ? make_subprocess_launcher_journald(this.journal_identifier) : make_subprocess_launcher_fallback(); - for (const extra_env of this.environ) { - const split_pos = extra_env.indexOf('='); - const name = extra_env.slice(0, split_pos); - const value = extra_env.slice(split_pos + 1); - - subprocess_launcher.setenv(name, value, true); - } - try { + subprocess_launcher.set_environ(this.environ); + this._subprocess = this._spawn(subprocess_launcher); } finally { subprocess_launcher.close(); @@ -220,18 +213,6 @@ export const Subprocess = GObject.registerClass({ this._get_logs = logging_to_journald ? collect_journald_logs.bind(globalThis, journalctl, start_date, pid) : collect_stdio_logs(this._subprocess.get_stdout_pipe()).catch(logError); - - if (!pid) - return; - - GnomeDesktop.start_systemd_scope( - this.journal_identifier, - parseInt(pid, 10), - null, - null, - null, - null - ); } get g_subprocess() { @@ -265,6 +246,12 @@ export const Subprocess = GObject.registerClass({ this.g_subprocess.send_signal(SIGTERM); } + get_pid() { + const pid = this.g_subprocess.get_identifier(); + + return pid ? parseInt(pid, 10) : null; + } + get_logs() { if (this._get_logs instanceof Function) return this._get_logs();