diff --git a/atom/browser/api/atom_api_power_monitor.h b/atom/browser/api/atom_api_power_monitor.h index 94717e4c9089..1a5f1ec6bd3b 100644 --- a/atom/browser/api/atom_api_power_monitor.h +++ b/atom/browser/api/atom_api_power_monitor.h @@ -6,8 +6,8 @@ #define ATOM_BROWSER_API_ATOM_API_POWER_MONITOR_H_ #include "atom/browser/api/trackable_object.h" +#include "atom/browser/lib/power_observer.h" #include "base/compiler_specific.h" -#include "base/power_monitor/power_observer.h" #include "native_mate/handle.h" namespace atom { @@ -15,7 +15,7 @@ namespace atom { namespace api { class PowerMonitor : public mate::TrackableObject, - public base::PowerObserver { + public PowerObserver { public: static v8::Local Create(v8::Isolate* isolate); diff --git a/atom/browser/lib/power_observer.h b/atom/browser/lib/power_observer.h new file mode 100644 index 000000000000..0935b221e33d --- /dev/null +++ b/atom/browser/lib/power_observer.h @@ -0,0 +1,26 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_LIB_POWER_OBSERVER_H_ +#define ATOM_BROWSER_LIB_POWER_OBSERVER_H_ + +#include "base/macros.h" + +#if defined(OS_LINUX) +#include "atom/browser/lib/power_observer_linux.h" +#else +#include "base/power_monitor/power_observer.h" +#endif // defined(OS_LINUX) + +namespace atom { + +#if defined(OS_LINUX) +typedef PowerObserverLinux PowerObserver; +#else +typedef base::PowerObserver PowerObserver; +#endif // defined(OS_LINUX) + +} // namespace atom + +#endif // ATOM_BROWSER_LIB_POWER_OBSERVER_H_ diff --git a/atom/browser/lib/power_observer_linux.cc b/atom/browser/lib/power_observer_linux.cc new file mode 100644 index 000000000000..da4b8ea82a7b --- /dev/null +++ b/atom/browser/lib/power_observer_linux.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. +#include "atom/browser/lib/power_observer_linux.h" + +#include +#include +#include + +#include "base/bind.h" +#include "device/bluetooth/dbus/dbus_thread_manager_linux.h" + +namespace { + +const char kLogindServiceName[] = "org.freedesktop.login1"; +const char kLogindObjectPath[] = "/org/freedesktop/login1"; +const char kLogindManagerInterface[] = "org.freedesktop.login1.Manager"; + +std::string get_executable_basename() { + char buf[4096]; + size_t buf_size = sizeof(buf); + std::string rv("electron"); + if (!uv_exepath(buf, &buf_size)) { + rv = strrchr(static_cast(buf), '/') + 1; + } + return std::move(rv); +} + +} // namespace + +namespace atom { + +PowerObserverLinux::PowerObserverLinux() + : lock_owner_name_(get_executable_basename()), weak_ptr_factory_(this) { + auto dbus_thread_manager = bluez::DBusThreadManagerLinux::Get(); + if (dbus_thread_manager) { + bus_ = dbus_thread_manager->GetSystemBus(); + if (bus_) { + logind_ = bus_->GetObjectProxy(kLogindServiceName, + dbus::ObjectPath(kLogindObjectPath)); + logind_->WaitForServiceToBeAvailable( + base::Bind(&PowerObserverLinux::OnLoginServiceAvailable, + weak_ptr_factory_.GetWeakPtr())); + } else { + LOG(WARNING) << "Failed to get system bus connection"; + } + } else { + LOG(WARNING) << "DBusThreadManagerLinux instance isn't available"; + } +} + +void PowerObserverLinux::OnLoginServiceAvailable(bool service_available) { + if (!service_available) { + LOG(WARNING) << kLogindServiceName << " not available"; + return; + } + // listen sleep + logind_->ConnectToSignal(kLogindManagerInterface, "PrepareForSleep", + base::Bind(&PowerObserverLinux::OnPrepareForSleep, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&PowerObserverLinux::OnSignalConnected, + weak_ptr_factory_.GetWeakPtr())); + TakeSleepLock(); +} + +void PowerObserverLinux::TakeSleepLock() { + dbus::MethodCall sleep_inhibit_call(kLogindManagerInterface, "Inhibit"); + dbus::MessageWriter inhibit_writer(&sleep_inhibit_call); + inhibit_writer.AppendString("sleep"); // what + // Use the executable name as the lock owner, which will list rebrands of the + // electron executable as separate entities. + inhibit_writer.AppendString(lock_owner_name_); // who + inhibit_writer.AppendString("Application cleanup before suspend"); // why + inhibit_writer.AppendString("delay"); // mode + logind_->CallMethod(&sleep_inhibit_call, + dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, + base::Bind(&PowerObserverLinux::OnInhibitResponse, + weak_ptr_factory_.GetWeakPtr(), &sleep_lock_)); +} + +void PowerObserverLinux::OnInhibitResponse(base::ScopedFD* scoped_fd, + dbus::Response* response) { + dbus::MessageReader reader(response); + reader.PopFileDescriptor(scoped_fd); +} + +void PowerObserverLinux::OnPrepareForSleep(dbus::Signal* signal) { + dbus::MessageReader reader(signal); + bool status; + if (!reader.PopBool(&status)) { + LOG(ERROR) << "Invalid signal: " << signal->ToString(); + return; + } + if (status) { + OnSuspend(); + sleep_lock_.reset(); + } else { + TakeSleepLock(); + OnResume(); + } +} + +void PowerObserverLinux::OnSignalConnected(const std::string& interface, + const std::string& signal, + bool success) { + LOG_IF(WARNING, !success) << "Failed to connect to " << signal; +} + +} // namespace atom diff --git a/atom/browser/lib/power_observer_linux.h b/atom/browser/lib/power_observer_linux.h new file mode 100644 index 000000000000..60691f721183 --- /dev/null +++ b/atom/browser/lib/power_observer_linux.h @@ -0,0 +1,42 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_LIB_POWER_OBSERVER_LINUX_H_ +#define ATOM_BROWSER_LIB_POWER_OBSERVER_LINUX_H_ + +#include + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/power_monitor/power_observer.h" +#include "dbus/bus.h" +#include "dbus/message.h" +#include "dbus/object_proxy.h" + +namespace atom { + +class PowerObserverLinux : public base::PowerObserver { + public: + PowerObserverLinux(); + + private: + void TakeSleepLock(); + void OnLoginServiceAvailable(bool available); + void OnInhibitResponse(base::ScopedFD* scoped_fd, dbus::Response* response); + void OnPrepareForSleep(dbus::Signal* signal); + void OnSignalConnected(const std::string& interface, + const std::string& signal, + bool success); + + scoped_refptr bus_; + scoped_refptr logind_; + std::string lock_owner_name_; + base::ScopedFD sleep_lock_; + base::WeakPtrFactory weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(PowerObserverLinux); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_LIB_POWER_OBSERVER_LINUX_H_ diff --git a/filenames.gypi b/filenames.gypi index b2ab8a36103f..e9f0f43b97ec 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -218,6 +218,9 @@ 'atom/browser/javascript_environment.h', 'atom/browser/lib/bluetooth_chooser.cc', 'atom/browser/lib/bluetooth_chooser.h', + 'atom/browser/lib/power_observer.h', + 'atom/browser/lib/power_observer_linux.h', + 'atom/browser/lib/power_observer_linux.cc', 'atom/browser/loader/layered_resource_handler.cc', 'atom/browser/loader/layered_resource_handler.h', 'atom/browser/login_handler.cc', diff --git a/script/lib/dbus_mock.py b/script/lib/dbus_mock.py new file mode 100644 index 000000000000..76f39fc26973 --- /dev/null +++ b/script/lib/dbus_mock.py @@ -0,0 +1,13 @@ +from dbusmock import DBusTestCase + +import atexit + +def cleanup(): + DBusTestCase.stop_dbus(DBusTestCase.system_bus_pid) + + +atexit.register(cleanup) +DBusTestCase.start_system_bus() +# create a mock for "org.freedesktop.login1" using python-dbusmock +# preconfigured template +(logind_mock, logind) = DBusTestCase.spawn_server_template('logind') diff --git a/script/test.py b/script/test.py index b09cff582806..67292997348b 100755 --- a/script/test.py +++ b/script/test.py @@ -10,6 +10,20 @@ from lib.config import enable_verbose_mode from lib.util import electron_gyp, execute_stdout, rm_rf +if sys.platform == 'linux2': + # On Linux we use python-dbusmock to create a fake system bus and test + # powerMonitor interaction with org.freedesktop.login1 service. The + # dbus_mock module takes care of setting up the fake server with mock, + # while also setting DBUS_SYSTEM_BUS_ADDRESS environment variable, which + # will be picked up by electron. + try: + import lib.dbus_mock + except ImportError: + # If not available, the powerMonitor tests will be skipped since + # DBUS_SYSTEM_BUS_ADDRESS will not be set + pass + + SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) PROJECT_NAME = electron_gyp()['project_name%'] diff --git a/spec/api-power-monitor-spec.js b/spec/api-power-monitor-spec.js new file mode 100644 index 000000000000..5b8b84fc655d --- /dev/null +++ b/spec/api-power-monitor-spec.js @@ -0,0 +1,89 @@ +// For these tests we use a fake DBus daemon to verify powerMonitor module +// interaction with the system bus. This requires python-dbusmock installed and +// running (with the DBUS_SYSTEM_BUS_ADDRESS environment variable set). +// script/test.py will take care of spawning the fake DBus daemon and setting +// DBUS_SYSTEM_BUS_ADDRESS when python-dbusmock is installed. +// +// See https://pypi.python.org/pypi/python-dbusmock for more information about +// python-dbusmock. +const assert = require('assert') +const dbus = require('dbus-native') +const Promise = require('bluebird') + +const skip = process.platform !== 'linux' || !process.env.DBUS_SYSTEM_BUS_ADDRESS; + +(skip ? describe.skip : describe)('powerMonitor', () => { + let logindMock, powerMonitor, getCalls, emitSignal, reset + + before(async () => { + const systemBus = dbus.systemBus() + const loginService = systemBus.getService('org.freedesktop.login1') + const getInterface = Promise.promisify(loginService.getInterface, {context: loginService}) + logindMock = await getInterface('/org/freedesktop/login1', 'org.freedesktop.DBus.Mock') + getCalls = Promise.promisify(logindMock.GetCalls, {context: logindMock}) + emitSignal = Promise.promisify(logindMock.EmitSignal, {context: logindMock}) + reset = Promise.promisify(logindMock.Reset, {context: logindMock}) + }) + + after(async () => { + await reset() + }) + + describe('when powerMonitor module is loaded', () => { + function onceMethodCalled (done) { + function cb () { + logindMock.removeListener('MethodCalled', cb) + } + done() + return cb + } + + before((done) => { + logindMock.on('MethodCalled', onceMethodCalled(done)) + // lazy load powerMonitor after we listen to MethodCalled mock signal + powerMonitor = require('electron').remote.powerMonitor + }) + + it('should call Inhibit to delay suspend', async () => { + const calls = await getCalls() + assert.equal(calls.length, 1) + assert.deepEqual(calls[0].slice(1), [ + 'Inhibit', [ + [[{type: 's', child: []}], ['sleep']], + [[{type: 's', child: []}], ['electron']], + [[{type: 's', child: []}], ['Application cleanup before suspend']], + [[{type: 's', child: []}], ['delay']] + ] + ]) + }) + + describe('when PrepareForSleep(true) signal is sent by logind', () => { + it('should emit "suspend" event', (done) => { + powerMonitor.once('suspend', () => done()) + emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep', + 'b', [['b', true]]) + }) + + describe('when PrepareForSleep(false) signal is sent by logind', () => { + it('should emit "resume" event', (done) => { + powerMonitor.once('resume', () => done()) + emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep', + 'b', [['b', false]]) + }) + + it('should have called Inhibit again', async () => { + const calls = await getCalls() + assert.equal(calls.length, 2) + assert.deepEqual(calls[1].slice(1), [ + 'Inhibit', [ + [[{type: 's', child: []}], ['sleep']], + [[{type: 's', child: []}], ['electron']], + [[{type: 's', child: []}], ['Application cleanup before suspend']], + [[{type: 's', child: []}], ['delay']] + ] + ]) + }) + }) + }) + }) +}) diff --git a/spec/package.json b/spec/package.json index de51b96468d9..b873554cac52 100644 --- a/spec/package.json +++ b/spec/package.json @@ -5,8 +5,10 @@ "version": "0.1.0", "devDependencies": { "basic-auth": "^1.0.4", + "bluebird": "^3.5.1", "chai": "^4.1.2", "coffee-script": "1.12.7", + "dbus-native": "^0.2.3", "graceful-fs": "^4.1.9", "mkdirp": "^0.5.1", "mocha": "^3.1.0",