Skip to content

fix: linux shutdown handling - #55666

Open
YUCLing wants to merge 9 commits into
electron:mainfrom
YUCLing:fix/linux-shutdown-handling
Open

YUCLing wants to merge 9 commits into
electron:mainfrom
YUCLing:fix/linux-shutdown-handling

Conversation

@YUCLing

@YUCLing YUCLing commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Description of Change

Fixes #55560
Fixes partially #54547 (we already tries to quit normally when logout, but cannot guarantee due to race)
Related #55551

This PR moves D-Bus shutdown listener out of the PowerObserverLinux, into a new SessionEndListenerLinux class, which gets created at startup by ElectronBrowserMainParts.

After the change, Electron will always hold a shutdown inhibitor of the logind and use Chromium's dbus_xdg::InhibitSessionMonitor to listen for logouts when available.

We hold a shutdown inhibitor to ensure the clean app quit when shutting down. Previously, Electron only held this inhibitor when the app registered powerMonitor's shutdown event, and does not quit if app doesn't quit in the handler, same if the app doesn't even register the event. Now we hold it since startup, and when we receive the shutdown from logind, we emit the shutdown event and delay the shutdown if a handler calls preventDefault(), otherwise, we simply quit normally, instead of waiting for SIGTERM to kill us.

For logouts, we listen for Ending state using dbus_xdg::InhibitSessionMonitor. At this state, logout is not cancellable, and is not delayable. We still call shutdown event handlers and still wait for app code if preventDefault is called, otherwise we quit normally. There's a state QueryEnd, where we can delay the logouts, but the logouts are also cancellable. I chose the Ending because of that.

Checklist

Release Notes

Notes: Fixed an issue where Electron did not respond to shutdown on Linux.

@ckerr
ckerr self-requested a review November 9, 2026 18:29

@ckerr ckerr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for writing this. This is the right idea for the fix & moving this out of PowerObserverLinux makes sense to me.

There are two blockers, but both have easy fixes:

  1. the macOS and Windows builds break
  2. a shutdown listener added while the main script loads is never registered. The delay lock staying held after a cancelled quit also needs a decision.

I have inline suggestions for both of these + a few other nits.

Comment on lines 59 to 62
#if BUILDFLAG(IS_LINUX)
void SetListeningForShutdown(bool);
// Asked when the session starts ending. False means the app is handling it.
bool ShouldQuitForSessionEnd();
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This breaks the macOS and Windows builds. ShouldQuitForSessionEnd() is only declared on Linux but is defined for all platforms in the .cc, and electron_api_power_monitor_mac.mm:112 still binds the removed ShouldShutdown(). Keeping the original name and declaring it on all platforms fixes both:

Suggested change
#if BUILDFLAG(IS_LINUX)
void SetListeningForShutdown(bool);
// Asked when the session starts ending. False means the app is handling it.
bool ShouldQuitForSessionEnd();
#endif
// Called by native callers when the session starts ending. False means the
// app is handling it.
bool ShouldShutdown();

}

bool PowerMonitor::ShouldShutdown() {
bool PowerMonitor::ShouldQuitForSessionEnd() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Matches the header suggestion.

Suggested change
bool PowerMonitor::ShouldQuitForSessionEnd() {
bool PowerMonitor::ShouldShutdown() {

Comment on lines 162 to 165
auto builder =
gin_helper::EventEmitterMixin<PowerMonitor>::GetObjectTemplateBuilder(
isolate);
#if BUILDFLAG(IS_LINUX)
builder.SetMethod("setListeningForShutdown",
&PowerMonitor::SetListeningForShutdown);
#endif
return builder;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This local was only needed for the Linux SetMethod() call that was removed.

Suggested change
auto builder =
gin_helper::EventEmitterMixin<PowerMonitor>::GetObjectTemplateBuilder(
isolate);
#if BUILDFLAG(IS_LINUX)
builder.SetMethod("setListeningForShutdown",
&PowerMonitor::SetListeningForShutdown);
#endif
return builder;
return gin_helper::EventEmitterMixin<PowerMonitor>::GetObjectTemplateBuilder(
isolate);


namespace {

const char kLogindServiceName[] = "org.freedesktop.login1";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: these constants and GetExecutableBaseName() duplicate the ones in power_observer_linux.cc. Could they be shared?

bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!success) {
VLOG(1) << "Failed to connect to " << interface_name << "." << signal_name;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OnLogindAvailable() still takes the delay lock after this failure, so logind would wait for a process that can't receive PrepareForShutdown. Clearing the proxy makes RequestInhibitor() return early:

Suggested change
VLOG(1) << "Failed to connect to " << interface_name << "." << signal_name;
VLOG(1) << "Failed to connect to " << interface_name << "." << signal_name;
// Don't make logind wait for a process that can't hear the shutdown.
logind_proxy_ = nullptr;

BrowserProcessImpl* browser_process() { return fake_browser_process_.get(); }

#if BUILDFLAG(IS_LINUX)
// Null when the D-Bus buses are unavailable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The listener is created unconditionally, so this is only null before it exists.

Suggested change
// Null when the D-Bus buses are unavailable.
// Null until PostEarlyInitialization() creates the listener.

Comment on lines +680 to +688
dbus_thread_linux::Initialize(base::BindRepeating(&ExitOnSessionLoss));

session_end_listener_ = std::make_unique<SessionEndListenerLinux>(
dbus_thread_linux::GetSharedSystemBus(),
dbus_thread_linux::GetSharedSessionBus(),
base::RepeatingCallback<bool()>(), base::BindRepeating([] {
if (auto* browser = Browser::Get())
browser->Quit();
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A shutdown listener added while the main script is loading never gets registered. LoadEnvironment() runs the main script in PostEarlyInitialization(), before this listener is created, so for a top-level powerMonitor.on('shutdown', ...) the lookup in the PowerMonitor constructor returns null and nothing retries it. shutdown then never fires and the app always quits, even if its handler would have called preventDefault().

Creating the listener before the main script runs fixes this. PostEarlyInitialization() is outside this diff, so I can't use a suggestion. These statements can move to just before LoadEnvironment():

 #if BUILDFLAG(IS_LINUX)
   JoinSystemFontConfigInit();
+  dbus_thread_linux::Initialize(base::BindRepeating(&ExitOnSessionLoss));
+
+  // Must exist before the main script runs, since it may use powerMonitor.
+  session_end_listener_ = std::make_unique<SessionEndListenerLinux>(
+      dbus_thread_linux::GetSharedSystemBus(),
+      dbus_thread_linux::GetSharedSessionBus(),
+      base::RepeatingCallback<bool()>(), base::BindRepeating([] {
+        if (auto* browser = Browser::Get())
+          browser->Quit();
+      }));
 #endif
 
   // Load everything.
   node_bindings_->LoadEnvironment(node_env_.get());


SessionEndListenerLinux(scoped_refptr<dbus::Bus> system_bus,
scoped_refptr<dbus::Bus> session_bus,
base::RepeatingCallback<bool()> should_quit,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this is always empty, because PowerMonitor sets the handler through SetShouldQuitHandler(). If you drop it, also remove the base::RepeatingCallback<bool()>() argument where the listener is created.

Suggested change
base::RepeatingCallback<bool()> should_quit,

SessionEndListenerLinux::SessionEndListenerLinux(
scoped_refptr<dbus::Bus> system_bus,
scoped_refptr<dbus::Bus> session_bus,
base::RepeatingCallback<bool()> should_quit,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Goes with the should_quit suggestion in the header.

Suggested change
base::RepeatingCallback<bool()> should_quit,

base::RepeatingClosure on_quit)
: system_bus_(std::move(system_bus)),
session_bus_(std::move(session_bus)),
should_quit_(std::move(should_quit)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Goes with the should_quit suggestion in the header.

Suggested change
should_quit_(std::move(should_quit)),

YUCLing and others added 8 commits November 10, 2026 21:53
ShouldQuitForSessionEnd() was declared for Linux only but defined
unconditionally, and electron_api_power_monitor_mac.mm still binds
ShouldShutdown(). Restore the original name and declare it on all
platforms.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
LoadEnvironment() runs the app's main script in PostEarlyInitialization(),
so a top-level powerMonitor.on('shutdown') was registered before the
listener existed and never reached it. Move the D-Bus setup and the
listener to PostEarlyInitialization(), and drop the listener constructor
argument that was always empty.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
If PrepareForShutdown could not be subscribed, taking the delay lock
would only make logind wait for a process that cannot react to it.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
A "before-quit", "will-quit" or window close handler can cancel the
quit started for a session end, leaving logind waiting for a process
that is not exiting. Notify the listener from Browser and drop the
delay lock.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
Note in the breaking changes that shutdown is also emitted for a portal
logout and that Electron quits unless a handler prevents it.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
Share the logind service, object path and interface constants and the
executable name helper between the power observer and the session end
listener. Include <string> where it is used, remove the unused testing
accessors, and drop the object template builder local.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
The fake session bus portal only implemented FileChooser, so the
InhibitSessionMonitor path never ran. Add org.freedesktop.portal.Inhibit
with CreateMonitor, a session state signal and session objects, and test
that a portal logout quits the app, and that a shutdown handler that
prevents it keeps the app alive. Rename the template since it now hosts
more than FileChooser.

Co-authored-by: Charles Kerr <70381+ckerr@users.noreply.github.com>
@YUCLing
YUCLing requested a review from a team as a code owner November 10, 2026 14:11

This branch has not been deployed

No deployments
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.

[Feature Request]: Quit on logind's shutdown announcement by default on Linux, as Chrome does

2 participants