Merge pull request #11417 from electron/power-monitor-shutdown-event-and-delay-api

[RFC] New API: powerMonitor "shutdown" event
This commit is contained in:
Cheng Zhao 2018-02-05 17:29:31 +09:00 committed by GitHub
commit ab015e573b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 187 additions and 26 deletions

View file

@ -16,6 +16,13 @@ namespace atom {
namespace api { namespace api {
PowerMonitor::PowerMonitor(v8::Isolate* isolate) { 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); base::PowerMonitor::Get()->AddObserver(this);
Init(isolate); Init(isolate);
} }
@ -24,6 +31,20 @@ PowerMonitor::~PowerMonitor() {
base::PowerMonitor::Get()->RemoveObserver(this); 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) { void PowerMonitor::OnPowerStateChange(bool on_battery_power) {
if (on_battery_power) if (on_battery_power)
Emit("on-battery"); Emit("on-battery");
@ -55,6 +76,11 @@ v8::Local<v8::Value> PowerMonitor::Create(v8::Isolate* isolate) {
void PowerMonitor::BuildPrototype( void PowerMonitor::BuildPrototype(
v8::Isolate* isolate, v8::Local<v8::FunctionTemplate> prototype) { v8::Isolate* isolate, v8::Local<v8::FunctionTemplate> prototype) {
prototype->SetClassName(mate::StringToV8(isolate, "PowerMonitor")); 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 } // namespace api
@ -68,10 +94,6 @@ using atom::api::PowerMonitor;
void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused, void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused,
v8::Local<v8::Context> context, void* priv) { v8::Local<v8::Context> context, void* priv) {
#if defined(OS_MACOSX)
base::PowerMonitorDeviceSource::AllocateSystemIOPorts();
#endif
v8::Isolate* isolate = context->GetIsolate(); v8::Isolate* isolate = context->GetIsolate();
mate::Dictionary dict(isolate, exports); mate::Dictionary dict(isolate, exports);
dict.Set("powerMonitor", PowerMonitor::Create(isolate)); dict.Set("powerMonitor", PowerMonitor::Create(isolate));

View file

@ -26,6 +26,15 @@ class PowerMonitor : public mate::TrackableObject<PowerMonitor>,
explicit PowerMonitor(v8::Isolate* isolate); explicit PowerMonitor(v8::Isolate* isolate);
~PowerMonitor() override; ~PowerMonitor() override;
// Called by native calles.
bool ShouldShutdown();
#if defined(OS_LINUX)
// Private JS APIs.
void BlockShutdown();
void UnblockShutdown();
#endif
// base::PowerObserver implementations: // base::PowerObserver implementations:
void OnPowerStateChange(bool on_battery_power) override; void OnPowerStateChange(bool on_battery_power) override;
void OnSuspend() override; void OnSuspend() override;

View file

@ -105,6 +105,9 @@ class Browser : public WindowListObserver {
LoginItemSettings GetLoginItemSettings(const LoginItemSettings& options); LoginItemSettings GetLoginItemSettings(const LoginItemSettings& options);
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
// Set the handler which decides whether to shutdown.
void SetShutdownHandler(base::Callback<bool()> handler);
// Hide the application. // Hide the application.
void Hide(); void Hide();

View file

@ -21,6 +21,10 @@
namespace atom { namespace atom {
void Browser::SetShutdownHandler(base::Callback<bool()> handler) {
[[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)];
}
void Browser::Focus() { void Browser::Focus() {
[[AtomApplication sharedApplication] activateIgnoringOtherApps:YES]; [[AtomApplication sharedApplication] activateIgnoringOtherApps:YES];
} }

View file

@ -54,16 +54,22 @@ void PowerObserverLinux::OnLoginServiceAvailable(bool service_available) {
LOG(WARNING) << kLogindServiceName << " not available"; LOG(WARNING) << kLogindServiceName << " not available";
return; 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", logind_->ConnectToSignal(kLogindManagerInterface, "PrepareForSleep",
base::Bind(&PowerObserverLinux::OnPrepareForSleep, base::Bind(&PowerObserverLinux::OnPrepareForSleep,
weak_ptr_factory_.GetWeakPtr()), weak_ptr_factory_.GetWeakPtr()),
base::Bind(&PowerObserverLinux::OnSignalConnected, base::Bind(&PowerObserverLinux::OnSignalConnected,
weak_ptr_factory_.GetWeakPtr())); weak_ptr_factory_.GetWeakPtr()));
TakeSleepLock(); // Take sleep inhibit lock
BlockSleep();
} }
void PowerObserverLinux::TakeSleepLock() { void PowerObserverLinux::BlockSleep() {
dbus::MethodCall sleep_inhibit_call(kLogindManagerInterface, "Inhibit"); dbus::MethodCall sleep_inhibit_call(kLogindManagerInterface, "Inhibit");
dbus::MessageWriter inhibit_writer(&sleep_inhibit_call); dbus::MessageWriter inhibit_writer(&sleep_inhibit_call);
inhibit_writer.AppendString("sleep"); // what inhibit_writer.AppendString("sleep"); // what
@ -78,6 +84,40 @@ void PowerObserverLinux::TakeSleepLock() {
weak_ptr_factory_.GetWeakPtr(), &sleep_lock_)); 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<bool()> handler) {
should_shutdown_ = std::move(handler);
}
void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd, void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd,
dbus::Response* response) { dbus::Response* response) {
dbus::MessageReader reader(response); dbus::MessageReader reader(response);
@ -86,20 +126,36 @@ void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd,
void PowerObserverLinux::OnPrepareForSleep(dbus::Signal* signal) { void PowerObserverLinux::OnPrepareForSleep(dbus::Signal* signal) {
dbus::MessageReader reader(signal); dbus::MessageReader reader(signal);
bool status; bool suspending;
if (!reader.PopBool(&status)) { if (!reader.PopBool(&suspending)) {
LOG(ERROR) << "Invalid signal: " << signal->ToString(); LOG(ERROR) << "Invalid signal: " << signal->ToString();
return; return;
} }
if (status) { if (suspending) {
OnSuspend(); OnSuspend();
sleep_lock_.reset(); UnblockSleep();
} else { } else {
TakeSleepLock(); BlockSleep();
OnResume(); 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, void PowerObserverLinux::OnSignalConnected(const std::string& interface,
const std::string& signal, const std::string& signal,
bool success) { bool success) {

View file

@ -20,20 +20,32 @@ class PowerObserverLinux : public base::PowerObserver {
public: public:
PowerObserverLinux(); PowerObserverLinux();
protected:
void BlockSleep();
void UnblockSleep();
void BlockShutdown();
void UnblockShutdown();
void SetShutdownHandler(base::Callback<bool()> should_shutdown);
private: private:
void TakeSleepLock();
void OnLoginServiceAvailable(bool available); void OnLoginServiceAvailable(bool available);
void OnInhibitResponse(base::ScopedFD* scoped_fd, dbus::Response* response); void OnInhibitResponse(base::ScopedFD* scoped_fd, dbus::Response* response);
void OnPrepareForSleep(dbus::Signal* signal); void OnPrepareForSleep(dbus::Signal* signal);
void OnPrepareForShutdown(dbus::Signal* signal);
void OnSignalConnected(const std::string& interface, void OnSignalConnected(const std::string& interface,
const std::string& signal, const std::string& signal,
bool success); bool success);
base::Callback<bool()> should_shutdown_;
scoped_refptr<dbus::Bus> bus_; scoped_refptr<dbus::Bus> bus_;
scoped_refptr<dbus::ObjectProxy> logind_; scoped_refptr<dbus::ObjectProxy> logind_;
std::string lock_owner_name_; std::string lock_owner_name_;
base::ScopedFD sleep_lock_; base::ScopedFD sleep_lock_;
base::ScopedFD shutdown_lock_;
base::WeakPtrFactory<PowerObserverLinux> weak_ptr_factory_; base::WeakPtrFactory<PowerObserverLinux> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(PowerObserverLinux); DISALLOW_COPY_AND_ASSIGN(PowerObserverLinux);
}; };

View file

@ -2,8 +2,9 @@
// Use of this source code is governed by the MIT license that can be // Use of this source code is governed by the MIT license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import "base/mac/scoped_sending_event.h" #include "base/callback.h"
#import "base/mac/scoped_nsobject.h" #include "base/mac/scoped_sending_event.h"
#include "base/mac/scoped_nsobject.h"
@interface AtomApplication : NSApplication<CrAppProtocol, @interface AtomApplication : NSApplication<CrAppProtocol,
CrAppControlProtocol, CrAppControlProtocol,
@ -13,10 +14,13 @@
base::scoped_nsobject<NSUserActivity> currentActivity_; base::scoped_nsobject<NSUserActivity> currentActivity_;
NSCondition* handoffLock_; NSCondition* handoffLock_;
BOOL updateReceived_; BOOL updateReceived_;
base::Callback<bool()> shouldShutdown_;
} }
+ (AtomApplication*)sharedApplication; + (AtomApplication*)sharedApplication;
- (void)setShutdownHandler:(base::Callback<bool()>)handler;
// CrAppProtocol: // CrAppProtocol:
- (BOOL)isHandlingSendEvent; - (BOOL)isHandlingSendEvent;

View file

@ -29,8 +29,17 @@ inline void dispatch_sync_main(dispatch_block_t block) {
} }
- (void)terminate:(id)sender { - (void)terminate:(id)sender {
AtomApplicationDelegate* atomDelegate = (AtomApplicationDelegate*) [NSApp delegate]; if (shouldShutdown_ && !shouldShutdown_.Run())
[atomDelegate tryToTerminateApp:self]; 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<bool()>)handler {
shouldShutdown_ = std::move(handler);
} }
- (BOOL)isHandlingSendEvent { - (BOOL)isHandlingSendEvent {

View file

@ -11,8 +11,6 @@
base::scoped_nsobject<AtomMenuController> menu_controller_; base::scoped_nsobject<AtomMenuController> menu_controller_;
} }
- (void)tryToTerminateApp:(NSApplication*)app;
// Sets the menu that will be returned in "applicationDockMenu:". // Sets the menu that will be returned in "applicationDockMenu:".
- (void)setApplicationDockMenu:(atom::AtomMenuModel*)model; - (void)setApplicationDockMenu:(atom::AtomMenuModel*)model;

View file

@ -87,13 +87,6 @@ static base::mac::ScopedObjCClassSwizzler* g_swizzle_imk_input_session;
return atom::Browser::Get()->OpenFile(filename_str) ? YES : NO; 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 - (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication
hasVisibleWindows:(BOOL)flag { hasVisibleWindows:(BOOL)flag {
atom::Browser* browser = atom::Browser::Get(); atom::Browser* browser = atom::Browser::Get();

View file

@ -39,3 +39,10 @@ Emitted when the system changes to AC power.
### Event: 'on-battery' _Windows_ ### Event: 'on-battery' _Windows_
Emitted when system changes to battery power. 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()`.

View file

@ -5,4 +5,19 @@ const {powerMonitor, PowerMonitor} = process.atomBinding('power_monitor')
Object.setPrototypeOf(PowerMonitor.prototype, EventEmitter.prototype) Object.setPrototypeOf(PowerMonitor.prototype, EventEmitter.prototype)
EventEmitter.call(powerMonitor) 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 module.exports = powerMonitor

View file

@ -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]])
})
})
})
}) })
}) })