diff --git a/atom/browser/api/atom_api_power_monitor.cc b/atom/browser/api/atom_api_power_monitor.cc index 429d3fc26e80..90f32393f168 100644 --- a/atom/browser/api/atom_api_power_monitor.cc +++ b/atom/browser/api/atom_api_power_monitor.cc @@ -16,6 +16,13 @@ namespace atom { namespace api { PowerMonitor::PowerMonitor(v8::Isolate* isolate) { +#if defined(OS_LINUX) + SetShutdownHandler(base::Bind(&PowerMonitor::ShouldShutdown, + base::Unretained(this))); +#elif defined(OS_MACOSX) + Browser::Get()->SetShutdownHandler(base::Bind(&PowerMonitor::ShouldShutdown, + base::Unretained(this))); +#endif base::PowerMonitor::Get()->AddObserver(this); Init(isolate); } @@ -24,6 +31,20 @@ PowerMonitor::~PowerMonitor() { base::PowerMonitor::Get()->RemoveObserver(this); } +bool PowerMonitor::ShouldShutdown() { + return !Emit("shutdown"); +} + +#if defined(OS_LINUX) +void PowerMonitor::BlockShutdown() { + PowerObserverLinux::BlockShutdown(); +} + +void PowerMonitor::UnblockShutdown() { + PowerObserverLinux::UnblockShutdown(); +} +#endif + void PowerMonitor::OnPowerStateChange(bool on_battery_power) { if (on_battery_power) Emit("on-battery"); @@ -55,6 +76,11 @@ v8::Local PowerMonitor::Create(v8::Isolate* isolate) { void PowerMonitor::BuildPrototype( v8::Isolate* isolate, v8::Local prototype) { prototype->SetClassName(mate::StringToV8(isolate, "PowerMonitor")); +#if defined(OS_LINUX) + mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) + .SetMethod("blockShutdown", &PowerMonitor::BlockShutdown) + .SetMethod("unblockShutdown", &PowerMonitor::UnblockShutdown); +#endif } } // namespace api @@ -68,10 +94,6 @@ using atom::api::PowerMonitor; void Initialize(v8::Local exports, v8::Local unused, v8::Local context, void* priv) { -#if defined(OS_MACOSX) - base::PowerMonitorDeviceSource::AllocateSystemIOPorts(); -#endif - v8::Isolate* isolate = context->GetIsolate(); mate::Dictionary dict(isolate, exports); dict.Set("powerMonitor", PowerMonitor::Create(isolate)); diff --git a/atom/browser/api/atom_api_power_monitor.h b/atom/browser/api/atom_api_power_monitor.h index 1a5f1ec6bd3b..90971556de83 100644 --- a/atom/browser/api/atom_api_power_monitor.h +++ b/atom/browser/api/atom_api_power_monitor.h @@ -26,6 +26,15 @@ class PowerMonitor : public mate::TrackableObject, explicit PowerMonitor(v8::Isolate* isolate); ~PowerMonitor() override; + // Called by native calles. + bool ShouldShutdown(); + +#if defined(OS_LINUX) + // Private JS APIs. + void BlockShutdown(); + void UnblockShutdown(); +#endif + // base::PowerObserver implementations: void OnPowerStateChange(bool on_battery_power) override; void OnSuspend() override; diff --git a/atom/browser/browser.h b/atom/browser/browser.h index b5e31160c8f8..553d3648afa3 100644 --- a/atom/browser/browser.h +++ b/atom/browser/browser.h @@ -105,6 +105,9 @@ class Browser : public WindowListObserver { LoginItemSettings GetLoginItemSettings(const LoginItemSettings& options); #if defined(OS_MACOSX) + // Set the handler which decides whether to shutdown. + void SetShutdownHandler(base::Callback handler); + // Hide the application. void Hide(); diff --git a/atom/browser/browser_mac.mm b/atom/browser/browser_mac.mm index d12de2c14c9d..0faeee36f939 100644 --- a/atom/browser/browser_mac.mm +++ b/atom/browser/browser_mac.mm @@ -21,6 +21,10 @@ namespace atom { +void Browser::SetShutdownHandler(base::Callback handler) { + [[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)]; +} + void Browser::Focus() { [[AtomApplication sharedApplication] activateIgnoringOtherApps:YES]; } diff --git a/atom/browser/lib/power_observer_linux.cc b/atom/browser/lib/power_observer_linux.cc index da4b8ea82a7b..27da2b917624 100644 --- a/atom/browser/lib/power_observer_linux.cc +++ b/atom/browser/lib/power_observer_linux.cc @@ -54,16 +54,22 @@ void PowerObserverLinux::OnLoginServiceAvailable(bool service_available) { LOG(WARNING) << kLogindServiceName << " not available"; return; } - // listen sleep + // Connect to PrepareForShutdown/PrepareForSleep signals + logind_->ConnectToSignal(kLogindManagerInterface, "PrepareForShutdown", + base::Bind(&PowerObserverLinux::OnPrepareForShutdown, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&PowerObserverLinux::OnSignalConnected, + weak_ptr_factory_.GetWeakPtr())); logind_->ConnectToSignal(kLogindManagerInterface, "PrepareForSleep", base::Bind(&PowerObserverLinux::OnPrepareForSleep, weak_ptr_factory_.GetWeakPtr()), base::Bind(&PowerObserverLinux::OnSignalConnected, weak_ptr_factory_.GetWeakPtr())); - TakeSleepLock(); + // Take sleep inhibit lock + BlockSleep(); } -void PowerObserverLinux::TakeSleepLock() { +void PowerObserverLinux::BlockSleep() { dbus::MethodCall sleep_inhibit_call(kLogindManagerInterface, "Inhibit"); dbus::MessageWriter inhibit_writer(&sleep_inhibit_call); inhibit_writer.AppendString("sleep"); // what @@ -78,6 +84,40 @@ void PowerObserverLinux::TakeSleepLock() { weak_ptr_factory_.GetWeakPtr(), &sleep_lock_)); } +void PowerObserverLinux::UnblockSleep() { + sleep_lock_.reset(); +} + +void PowerObserverLinux::BlockShutdown() { + if (shutdown_lock_.is_valid()) { + LOG(WARNING) << "Trying to subscribe to shutdown multiple times"; + return; + } + dbus::MethodCall shutdown_inhibit_call(kLogindManagerInterface, "Inhibit"); + dbus::MessageWriter inhibit_writer(&shutdown_inhibit_call); + inhibit_writer.AppendString("shutdown"); // what + inhibit_writer.AppendString(lock_owner_name_); // who + inhibit_writer.AppendString("Ensure a clean shutdown"); // why + inhibit_writer.AppendString("delay"); // mode + logind_->CallMethod( + &shutdown_inhibit_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, + base::Bind(&PowerObserverLinux::OnInhibitResponse, + weak_ptr_factory_.GetWeakPtr(), &shutdown_lock_)); +} + +void PowerObserverLinux::UnblockShutdown() { + if (!shutdown_lock_.is_valid()) { + LOG(WARNING) + << "Trying to unsubscribe to shutdown without being subscribed"; + return; + } + shutdown_lock_.reset(); +} + +void PowerObserverLinux::SetShutdownHandler(base::Callback handler) { + should_shutdown_ = std::move(handler); +} + void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd, dbus::Response* response) { dbus::MessageReader reader(response); @@ -86,20 +126,36 @@ void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd, void PowerObserverLinux::OnPrepareForSleep(dbus::Signal* signal) { dbus::MessageReader reader(signal); - bool status; - if (!reader.PopBool(&status)) { + bool suspending; + if (!reader.PopBool(&suspending)) { LOG(ERROR) << "Invalid signal: " << signal->ToString(); return; } - if (status) { + if (suspending) { OnSuspend(); - sleep_lock_.reset(); + UnblockSleep(); } else { - TakeSleepLock(); + BlockSleep(); OnResume(); } } +void PowerObserverLinux::OnPrepareForShutdown(dbus::Signal* signal) { + dbus::MessageReader reader(signal); + bool shutting_down; + if (!reader.PopBool(&shutting_down)) { + LOG(ERROR) << "Invalid signal: " << signal->ToString(); + return; + } + if (shutting_down) { + if (!should_shutdown_ || should_shutdown_.Run()) { + // The user didn't try to prevent shutdown. Release the lock and allow the + // shutdown to continue normally. + shutdown_lock_.reset(); + } + } +} + void PowerObserverLinux::OnSignalConnected(const std::string& interface, const std::string& signal, bool success) { diff --git a/atom/browser/lib/power_observer_linux.h b/atom/browser/lib/power_observer_linux.h index 60691f721183..c2cb1d801508 100644 --- a/atom/browser/lib/power_observer_linux.h +++ b/atom/browser/lib/power_observer_linux.h @@ -20,20 +20,32 @@ class PowerObserverLinux : public base::PowerObserver { public: PowerObserverLinux(); + protected: + void BlockSleep(); + void UnblockSleep(); + void BlockShutdown(); + void UnblockShutdown(); + + void SetShutdownHandler(base::Callback should_shutdown); + private: - void TakeSleepLock(); void OnLoginServiceAvailable(bool available); void OnInhibitResponse(base::ScopedFD* scoped_fd, dbus::Response* response); void OnPrepareForSleep(dbus::Signal* signal); + void OnPrepareForShutdown(dbus::Signal* signal); void OnSignalConnected(const std::string& interface, const std::string& signal, bool success); + base::Callback should_shutdown_; + scoped_refptr bus_; scoped_refptr logind_; std::string lock_owner_name_; base::ScopedFD sleep_lock_; + base::ScopedFD shutdown_lock_; base::WeakPtrFactory weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(PowerObserverLinux); }; diff --git a/atom/browser/mac/atom_application.h b/atom/browser/mac/atom_application.h index 45bb011cd772..243667b46039 100644 --- a/atom/browser/mac/atom_application.h +++ b/atom/browser/mac/atom_application.h @@ -2,8 +2,9 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. -#import "base/mac/scoped_sending_event.h" -#import "base/mac/scoped_nsobject.h" +#include "base/callback.h" +#include "base/mac/scoped_sending_event.h" +#include "base/mac/scoped_nsobject.h" @interface AtomApplication : NSApplication currentActivity_; NSCondition* handoffLock_; BOOL updateReceived_; + base::Callback shouldShutdown_; } + (AtomApplication*)sharedApplication; +- (void)setShutdownHandler:(base::Callback)handler; + // CrAppProtocol: - (BOOL)isHandlingSendEvent; diff --git a/atom/browser/mac/atom_application.mm b/atom/browser/mac/atom_application.mm index 96c99f5d22f1..a7ea338857f4 100644 --- a/atom/browser/mac/atom_application.mm +++ b/atom/browser/mac/atom_application.mm @@ -29,8 +29,17 @@ inline void dispatch_sync_main(dispatch_block_t block) { } - (void)terminate:(id)sender { - AtomApplicationDelegate* atomDelegate = (AtomApplicationDelegate*) [NSApp delegate]; - [atomDelegate tryToTerminateApp:self]; + if (shouldShutdown_ && !shouldShutdown_.Run()) + return; // User will call Quit later. + + // We simply try to close the browser, which in turn will try to close the + // windows. Termination can proceed if all windows are closed or window close + // can be cancelled which will abort termination. + atom::Browser::Get()->Quit(); +} + +- (void)setShutdownHandler:(base::Callback)handler { + shouldShutdown_ = std::move(handler); } - (BOOL)isHandlingSendEvent { diff --git a/atom/browser/mac/atom_application_delegate.h b/atom/browser/mac/atom_application_delegate.h index cbc5c71d0a73..777475213ecf 100644 --- a/atom/browser/mac/atom_application_delegate.h +++ b/atom/browser/mac/atom_application_delegate.h @@ -11,8 +11,6 @@ base::scoped_nsobject menu_controller_; } -- (void)tryToTerminateApp:(NSApplication*)app; - // Sets the menu that will be returned in "applicationDockMenu:". - (void)setApplicationDockMenu:(atom::AtomMenuModel*)model; diff --git a/atom/browser/mac/atom_application_delegate.mm b/atom/browser/mac/atom_application_delegate.mm index 5664501d1aa3..e210ad2b6a77 100644 --- a/atom/browser/mac/atom_application_delegate.mm +++ b/atom/browser/mac/atom_application_delegate.mm @@ -87,13 +87,6 @@ static base::mac::ScopedObjCClassSwizzler* g_swizzle_imk_input_session; return atom::Browser::Get()->OpenFile(filename_str) ? YES : NO; } -// We simply try to close the browser, which in turn will try to close the windows. -// Termination can proceed if all windows are closed or window close can be cancelled -// which will abort termination. -- (void)tryToTerminateApp:(NSApplication*)app { - atom::Browser::Get()->Quit(); -} - - (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication hasVisibleWindows:(BOOL)flag { atom::Browser* browser = atom::Browser::Get(); diff --git a/docs/api/power-monitor.md b/docs/api/power-monitor.md index 0c012c64ac8c..5d600da48a94 100644 --- a/docs/api/power-monitor.md +++ b/docs/api/power-monitor.md @@ -39,3 +39,10 @@ Emitted when the system changes to AC power. ### Event: 'on-battery' _Windows_ Emitted when system changes to battery power. + +### Event: 'shutdown' _Linux_ _macOS_ + +Emitted when the system is about to reboot or shut down. If the event handler +invokes `e.preventDefault()`, Electron will attempt to delay system shutdown in +order for the app to exit cleanly. If `e.preventDefault()` is called, the app +should exit as soon as possible by calling something like `app.quit()`. diff --git a/lib/browser/api/power-monitor.js b/lib/browser/api/power-monitor.js index e1dff2c3b73c..deeda3bd4d4d 100644 --- a/lib/browser/api/power-monitor.js +++ b/lib/browser/api/power-monitor.js @@ -5,4 +5,19 @@ const {powerMonitor, PowerMonitor} = process.atomBinding('power_monitor') Object.setPrototypeOf(PowerMonitor.prototype, EventEmitter.prototype) EventEmitter.call(powerMonitor) +// On Linux we need to call blockShutdown() to subscribe to shutdown event. +if (process.platform === 'linux') { + powerMonitor.on('newListener', (event) => { + if (event === 'shutdown' && powerMonitor.listenerCount('shutdown') === 0) { + powerMonitor.blockShutdown() + } + }) + + powerMonitor.on('removeListener', (event) => { + if (event === 'shutdown' && powerMonitor.listenerCount('shutdown') === 0) { + powerMonitor.unblockShutdown() + } + }) +} + module.exports = powerMonitor diff --git a/spec/api-power-monitor-spec.js b/spec/api-power-monitor-spec.js index 5b8b84fc655d..3b8d9ab5e4a9 100644 --- a/spec/api-power-monitor-spec.js +++ b/spec/api-power-monitor-spec.js @@ -85,5 +85,34 @@ const skip = process.platform !== 'linux' || !process.env.DBUS_SYSTEM_BUS_ADDRES }) }) }) + + describe('when a listener is added to shutdown event', () => { + before(async () => { + const calls = await getCalls() + assert.equal(calls.length, 2) + powerMonitor.once('shutdown', () => { }) + }) + + it('should call Inhibit to delay shutdown', async () => { + const calls = await getCalls() + assert.equal(calls.length, 3) + assert.deepEqual(calls[2].slice(1), [ + 'Inhibit', [ + [[{type: 's', child: []}], ['shutdown']], + [[{type: 's', child: []}], ['electron']], + [[{type: 's', child: []}], ['Ensure a clean shutdown']], + [[{type: 's', child: []}], ['delay']] + ] + ]) + }) + + describe('when PrepareForShutdown(true) signal is sent by logind', () => { + it('should emit "shutdown" event', (done) => { + powerMonitor.once('shutdown', () => { done() }) + emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', + 'b', [['b', true]]) + }) + }) + }) }) })