Skip to content

app: switch to async D-Bus calls during startup#1620

Draft
amezin wants to merge 2 commits into
masterfrom
app-init-dbus-async
Draft

app: switch to async D-Bus calls during startup#1620
amezin wants to merge 2 commits into
masterfrom
app-init-dbus-async

Conversation

@amezin
Copy link
Copy Markdown
Member

@amezin amezin commented Dec 6, 2025

Should improve startup speed

@amezin amezin force-pushed the app-init-dbus-async branch 4 times, most recently from b954639 to 2ff62c9 Compare December 7, 2025 01:41
@amezin amezin force-pushed the app-init-dbus-async branch from 2ff62c9 to a432280 Compare December 26, 2025 14:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 26, 2025

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling and reporting for application startup and display configuration.
    • Enhanced lifecycle management with better error propagation and recovery.
  • Refactor

    • Refactored asynchronous initialization flow for more reliable startup sequence.
    • Optimized window and terminal creation with improved async coordination.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

The application's startup lifecycle and display configuration were refactored to async/await. Application methods (startup path, command_line, open_file, ensure_window*, restore_session) now use asynchronous flows with try/catch and hold/release sequencing; DisplayConfig unifies update paths, adds init_async(), and exposes an observable error state.

Changes

Cohort / File(s) Summary
Application lifecycle & window management
ddterm/app/application.js
Replaced synchronous startup with async vfunc_startup() and try/catch; introduced hold/release around init. Converted command_line, open_file, ensure_window, ensure_window_with_terminal, and restore_session to async; switched D-Bus proxy creation to constructor-style instantiation; awaited resource init before window/terminal operations.
Display configuration & error surface
ddterm/util/displayconfig.js
Added #error private field and public error property; merged sync/async update into update() and added init_async() for initial state awaiting; changed factory to static new(dbus_connection = null); updated error handling (CANCELLED treated non-fatal, other errors set #error and notify), cleared #error after successful parse, and adjusted create_monitor_list binding behavior.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Hold as InitHold
    participant DBus as Extension D-Bus Proxy
    participant DC as DisplayConfig
    participant Win as Window/Terminal

    Note over App: vfunc_startup() (async)

    App->>Hold: acquire
    Note right of Hold: prevent early exit

    rect rgb(240,248,255)
        App->>DBus: construct proxy (constructor) & init_async()
        DBus-->>App: ready / error
    end

    rect rgb(240,248,255)
        App->>DC: new(dbus_connection) & init_async()
        DC-->>App: initial state / error
    end

    App->>App: setup actions, bind handlers (await ensure_window())
    alt init success
        App->>Hold: release
    else init failure
        App->>App: log error & exit
    end

    Note over App,Win: User action (activate/open)
    App->>App: await ensure_window()/open_file()
    App->>Win: create/present terminal
    Win-->>App: ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: converting D-Bus calls to asynchronous during startup, which aligns with the extensive async refactoring throughout the application code.
Description check ✅ Passed The description states the intended benefit (improve startup speed) and is related to the changeset's goal of converting to async D-Bus calls, though it lacks detail about the scope of changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch app-init-dbus-async

📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a432280 and 84d0380.

📒 Files selected for processing (2)
  • ddterm/app/application.js
  • ddterm/util/displayconfig.js
🧰 Additional context used
🧬 Code graph analysis (1)
ddterm/util/displayconfig.js (1)
ddterm/app/application.js (1)
  • dbus_connection (265-265)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: ci / test (alpine-3.23)
  • GitHub Check: ci / test (alpine-3.20)
  • GitHub Check: ci / test (fedora-43)
  • GitHub Check: ci / test (alpine-3.21)
  • GitHub Check: ci / test (ubuntu-24.04)
  • GitHub Check: ci / test (ubuntu-25.10)
  • GitHub Check: ci / test (fedora-42)
  • GitHub Check: ci / test (alpine-3.22)
  • GitHub Check: ci / test (debian-13)
  • GitHub Check: ci / test (ubuntu-25.04)
  • GitHub Check: ci / test (opensuse-tumbleweed)
  • GitHub Check: ci / test (centos-10)
  • GitHub Check: ci / archlinux-package
  • GitHub Check: ci / update-translations
🔇 Additional comments (12)
ddterm/util/displayconfig.js (4)

51-57: LGTM: Error property exposure.

The new error GObject property is properly defined with GLib.Error boxed type and readable flag, enabling external listeners to observe error state changes.


108-113: LGTM: Factory method.

The static factory method provides a clean way to instantiate DisplayConfig with an optional D-Bus connection, defaulting to the session bus.


191-194: LGTM: Error state clearing on success.

Clearing #error and emitting notification after a successful state parse ensures listeners are informed when the system recovers from a prior error.


207-214: LGTM: Consistent error notification in unwatch().

The unwatch() method correctly sets a CANCELLED error and uses this.notify('error') with the property name string, which is the proper GObject notification pattern.

ddterm/app/application.js (8)

70-77: LGTM: Async-compatible proxy construction.

Using the Gio.DBusProxy constructor instead of new_sync allows the D-Bus connection to be established asynchronously via init_async(), supporting the async startup flow.


246-252: LGTM: Error handling for command-line.

The command-line handler properly catches promise rejections, logs errors, and calls command_line.done() to signal completion even on failure.


263-275: LGTM: Async initialization promises stored for later await.

Storing _extension_dbus_init and _display_config_init promises allows startup to continue while D-Bus operations complete in the background. These are properly awaited in ensure_window() before creating the window.


510-576: LGTM: Async command_line implementation.

The method properly awaits ensure_window(), handles the wait mode with child-exited signals, and manages exit status correctly. Errors propagate to the caller which has proper .catch() handling.


578-597: LGTM: Async open_file implementation.

The method correctly awaits window creation before accessing the notebook. Error handling is the responsibility of the caller (the 'open' signal handler, which needs improvement as noted separately).


653-676: LGTM: Hold/release pattern with race condition protection.

The hold()/release() pattern correctly keeps the application alive during async initialization. The finally block ensures release() is called even if initialization fails. Re-checking this.window after await properly handles potential race conditions where another caller may have created the window.


696-700: LGTM: Async ensure_window_with_terminal.

The method correctly awaits window creation before calling ensure_terminal(). Returning this.window is equivalent to returning the awaited result since ensure_window() sets the property.


790-825: LGTM: Async restore_session with proper error handling.

The method properly awaits window creation and has comprehensive error handling internally. File-not-found is gracefully ignored while other errors are logged and the session file is cleaned up.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad1c2ab and a432280.

📒 Files selected for processing (2)
  • ddterm/app/application.js
  • ddterm/util/displayconfig.js
🧰 Additional context used
🧬 Code graph analysis (1)
ddterm/app/application.js (2)
ddterm/util/displayconfig.js (3)
  • DisplayConfig (25-216)
  • DisplayConfig (25-216)
  • i (340-340)
ddterm/app/appwindow.js (4)
  • action (307-307)
  • notebook (398-403)
  • AppWindow (49-760)
  • AppWindow (49-760)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: ci / test (ubuntu-24.04)
  • GitHub Check: ci / test (ubuntu-25.10)
  • GitHub Check: ci / test (debian-13)
  • GitHub Check: ci / test (fedora-43)
  • GitHub Check: ci / test (ubuntu-25.04)
  • GitHub Check: ci / test (centos-10)
  • GitHub Check: ci / test (alpine-3.21)
  • GitHub Check: ci / test (fedora-42)
  • GitHub Check: ci / test (alpine-3.22)
  • GitHub Check: ci / test (alpine-3.23)
  • GitHub Check: ci / test (opensuse-tumbleweed)
  • GitHub Check: ci / test (alpine-3.20)
  • GitHub Check: ci / archlinux-package
  • GitHub Check: ci / update-translations
🔇 Additional comments (10)
ddterm/util/displayconfig.js (3)

51-57: LGTM! Error property registration.

The new error property properly exposes the internal error state with the correct GLib.Error boxed type.


140-168: Approve the unified update() method with error state management.

The consolidation of update_sync/update_async into a single update() method is a good simplification. The error state is properly cleared on successful parse (lines 191-194), and the cancellation check correctly avoids spurious error logging.

Also applies to: 191-194


207-215: Approve unwatch() error signaling for pending init_async consumers.

Setting a CANCELLED error and notifying ensures that any pending init_async() callers will properly reject rather than hang indefinitely.

ddterm/app/application.js (7)

70-77: LGTM! Proxy construction without sync call.

Switching from Gio.DBusProxy.new_sync to constructing the proxy object directly and deferring initialization to init_async is the right approach for async startup.


246-253: Exit status returned before async command handling completes.

The command-line signal handler returns command_line.get_exit_status() synchronously at line 252, but the actual command processing happens asynchronously. At the time of return, the exit status may not yet be set by the async command_line() method.

This may be intentional if GLib's command-line handling expects the handler to return immediately and relies on command_line.done() for completion signaling. Please verify this is the expected behavior.


263-274: Approve async D-Bus initialization before GTK startup.

Starting D-Bus proxy and display config initialization before calling super.vfunc_startup() (which initializes GTK) is a good optimization. The async operations can proceed in parallel with GTK init.


423-426: System.exit(1) in vfunc_startup may cause abrupt termination.

Calling System.exit(1) will terminate the process immediately without running cleanup handlers or the normal shutdown sequence. Consider using this.quit() followed by setting an exit code, or at minimum ensure this is the intended behavior for startup failures.


653-676: Approve hold/release pattern for async window creation.

Using this.hold() and this.release() in a try/finally ensures the application stays alive while awaiting D-Bus and display config initialization. The second check for this.window after awaits (line 663) correctly handles the case where another caller created the window while waiting.


696-700: Approve async ensure_window_with_terminal.

The refactor to await ensure_window() and then call ensure_terminal() is correct. The method returns this.window which will be set after the await completes.


790-825: Approve async restore_session with proper error handling.

The method correctly awaits ensure_window() and has comprehensive error handling for file read failures.

Comment thread ddterm/app/application.js
Comment on lines 233 to 235
this.connect('activate', () => {
this.ensure_window_with_terminal().present();
this.ensure_window_with_terminal().then(win => win.present());
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled promise rejection in 'activate' handler.

Similar to other handlers, the ensure_window_with_terminal().then(...) call lacks error handling.

🔎 Proposed fix
         this.connect('activate', () => {
-            this.ensure_window_with_terminal().then(win => win.present());
+            this.ensure_window_with_terminal().then(win => win.present()).catch(ex => logError(ex));
         });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ddterm/app/application.js around lines 233 to 235, the 'activate' signal
handler calls ensure_window_with_terminal().then(win => win.present()) without
handling promise rejections; wrap the async call to handle errors (either
convert the handler to async and use try/catch around await
ensure_window_with_terminal() or append .catch(...) to the promise) and ensure
any caught error is logged or reported and does not cause an unhandled
rejection, while preserving the existing success path that calls win.present().

Comment thread ddterm/app/application.js
Comment on lines +255 to 260
this.connect('open', async (_, files) => {
for (const file of files)
this.open_file(file);
await this.open_file(file); // eslint-disable-line no-await-in-loop

this.activate();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Async 'open' handler: unhandled rejection.

The open signal callback is marked async but the signal connection does not handle potential rejections. If open_file throws, the error will become an unhandled promise rejection.

🔎 Proposed fix to add error handling
-        this.connect('open', async (_, files) => {
-            for (const file of files)
-                await this.open_file(file); // eslint-disable-line no-await-in-loop
-
-            this.activate();
-        });
+        this.connect('open', (_, files) => {
+            (async () => {
+                for (const file of files)
+                    await this.open_file(file); // eslint-disable-line no-await-in-loop
+
+                this.activate();
+            })().catch(ex => logError(ex));
+        });
🤖 Prompt for AI Agents
In ddterm/app/application.js around lines 255 to 260, the async 'open' signal
handler can produce unhandled promise rejections if await this.open_file(file)
throws; wrap the async handler body in a try/catch so any error from opening
files is caught, log or propagate the error in the catch (e.g. console.error or
emit an 'error' event on the application) and ensure activate() is only called
on success or handle failure path explicitly to avoid unhandled promise
rejections.

Comment thread ddterm/app/application.js
Comment on lines +330 to +335
this.simple_action('toggle', () => {
this.ensure_window_with_terminal().then(win => win.toggle());
});
this.simple_action('show', () => {
this.ensure_window_with_terminal().then(win => win.present());
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled promise rejections in action callbacks.

The toggle and show actions call ensure_window_with_terminal().then(...) without a .catch() handler. If the promise rejects (e.g., D-Bus initialization fails), the error will be unhandled.

🔎 Proposed fix to add error handling
         this.simple_action('toggle', () => {
-            this.ensure_window_with_terminal().then(win => win.toggle());
+            this.ensure_window_with_terminal().then(win => win.toggle()).catch(ex => logError(ex));
         });
         this.simple_action('show', () => {
-            this.ensure_window_with_terminal().then(win => win.present());
+            this.ensure_window_with_terminal().then(win => win.present()).catch(ex => logError(ex));
         });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ddterm/app/application.js around lines 330 to 335, the action callbacks call
ensure_window_with_terminal().then(...) without handling rejections; add error
handling so promise rejections are caught and surfaced. Replace the .then(...)
calls with either async functions using try/catch around await
ensure_window_with_terminal() or append .catch(err => /* handle */) to the
promise chain; in the catch handler log the error (or surface it to the UI) and
avoid throwing unhandled rejections so failures during D-Bus initialization or
window creation are reported safely.

Comment thread ddterm/app/application.js
this.window.disconnect(this._save_session_handler);
this._save_session_handler = null;
}
this.restore_session();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fire-and-forget async call: restore_session() not awaited.

restore_session() is now async but is called without await in vfunc_startup(). This means:

  1. Session restoration happens in the background
  2. Startup proceeds immediately, potentially before session is restored
  3. Any errors are not caught by the surrounding try/catch

If this is intentional (non-blocking session restore), add .catch(ex => logError(ex)) to handle potential rejections. Otherwise, consider if this should block startup.

🔎 Proposed fix if fire-and-forget is intentional
-            this.restore_session();
+            this.restore_session().catch(ex => logError(ex));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.restore_session();
this.restore_session().catch(ex => logError(ex));

Comment on lines +83 to +106
init_async() {
const handlers = [];

return new Promise((resolve, reject) => {
if (this.#error)
reject(this.#error);

if (this.#current_state)
resolve(this.#current_state);

handlers.push(this.connect('notify::error', () => {
if (this.#error)
reject(this.#error);
}));

handlers.push(this.connect('notify::current-state', () => {
if (this.#current_state)
resolve(this.#current_state);
}));
}).finally(() => {
while (handlers.length)
this.disconnect(handlers.pop());
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing early returns cause Promise to never settle in some cases.

When this.#error or this.#current_state is already set, you call reject() or resolve() but then continue executing to set up signal handlers. Since there's no return after these calls, the Promise executor continues and the handlers array gets populated. The finally block will run after the Promise settles, but the logic flow is confusing and wasteful.

More critically: if neither condition is true initially, but then both signals fire in quick succession, both resolve and reject could be called (though only the first takes effect).

🔎 Proposed fix to add early returns
     init_async() {
         const handlers = [];

         return new Promise((resolve, reject) => {
-            if (this.#error)
-                reject(this.#error);
+            if (this.#error) {
+                reject(this.#error);
+                return;
+            }

-            if (this.#current_state)
-                resolve(this.#current_state);
+            if (this.#current_state) {
+                resolve(this.#current_state);
+                return;
+            }

             handlers.push(this.connect('notify::error', () => {
                 if (this.#error)
                     reject(this.#error);
             }));

             handlers.push(this.connect('notify::current-state', () => {
                 if (this.#current_state)
                     resolve(this.#current_state);
             }));
         }).finally(() => {
             while (handlers.length)
                 this.disconnect(handlers.pop());
         });
     }
🤖 Prompt for AI Agents
In ddterm/util/displayconfig.js around lines 83–106, add early returns and a
settled guard so the Promise cannot continue registering handlers after already
resolving/rejecting and to prevent multiple settle attempts: after the initial
this.#error reject and this.#current_state resolve, immediately return;
introduce a local boolean (e.g., settled = false) and in each signal handler
check if settled before calling resolve/reject, set settled = true when calling,
and ensure handlers are cleaned up after the first settlement (the existing
finally can still disconnect remaining handlers). This prevents redundant
handler registration and multiple resolve/reject calls.

Comment on lines +158 to +164
if (error instanceof GLib.Error &&
error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED))
return;

logError(error);
this.#error = error;
this.notify(error);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Typo: this.notify(error) should be this.notify('error').

Line 164 passes the error object itself instead of the property name string 'error'. This will not properly notify listeners of the error property change.

🔎 Proposed fix
                     logError(error);
                     this.#error = error;
-                    this.notify(error);
+                    this.notify('error');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error instanceof GLib.Error &&
error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED))
return;
logError(error);
this.#error = error;
this.notify(error);
if (error instanceof GLib.Error &&
error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED))
return;
logError(error);
this.#error = error;
this.notify('error');
🤖 Prompt for AI Agents
In ddterm/util/displayconfig.js around lines 158 to 164, the code calls
this.notify(error) passing the error object instead of the property name; change
the notify call to this.notify('error') so listeners receive a property-change
notification for the "error" property, and ensure no other calls use the object
instead of the property string.

Start async D-Bus calls before initializing Gtk.
@amezin amezin force-pushed the app-init-dbus-async branch from a432280 to 84d0380 Compare December 30, 2025 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant