blob: 2b104027d701c3ecc57e371cc0bb4843d65c1cab [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/device/wake_lock/power_save_blocker/power_save_blocker.h"
#include <stdint.h>
#include <memory>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "ui/display/screen.h"
#include "ui/gfx/switches.h"
namespace device {
namespace {
enum DBusAPI {
GNOME_API, // org.gnome.SessionManager
FREEDESKTOP_POWER_API, // org.freedesktop.PowerManagement
FREEDESKTOP_SCREENSAVER_API, // org.freedesktop.ScreenSaver
};
// Inhibit flags defined in the org.gnome.SessionManager interface.
// Can be OR'd together and passed as argument to the Inhibit() method
// to specify which power management features we want to suspend.
enum GnomeAPIInhibitFlags {
INHIBIT_LOGOUT = 1,
INHIBIT_SWITCH_USER = 2,
INHIBIT_SUSPEND_SESSION = 4,
INHIBIT_MARK_SESSION_IDLE = 8
};
const char kGnomeAPIServiceName[] = "org.gnome.SessionManager";
const char kGnomeAPIInterfaceName[] = "org.gnome.SessionManager";
const char kGnomeAPIObjectPath[] = "/org/gnome/SessionManager";
const char kFreeDesktopAPIPowerServiceName[] =
"org.freedesktop.PowerManagement";
const char kFreeDesktopAPIPowerInterfaceName[] =
"org.freedesktop.PowerManagement.Inhibit";
const char kFreeDesktopAPIPowerObjectPath[] =
"/org/freedesktop/PowerManagement/Inhibit";
const char kFreeDesktopAPIScreenServiceName[] = "org.freedesktop.ScreenSaver";
const char kFreeDesktopAPIScreenInterfaceName[] = "org.freedesktop.ScreenSaver";
const char kFreeDesktopAPIScreenObjectPath[] = "/org/freedesktop/ScreenSaver";
const char kDbusMethodNameHasOwnerMethod[] = "NameHasOwner";
bool ServiceNameHasOwner(dbus::Bus* bus, const char* service_name) {
dbus::ObjectProxy* dbus_proxy =
bus->GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
dbus::MethodCall name_has_owner_call(DBUS_INTERFACE_DBUS,
kDbusMethodNameHasOwnerMethod);
dbus::MessageWriter writer(&name_has_owner_call);
writer.AppendString(service_name);
std::unique_ptr<dbus::Response> name_has_owner_response =
dbus_proxy
->CallMethodAndBlock(&name_has_owner_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)
.value_or(nullptr);
dbus::MessageReader reader(name_has_owner_response.get());
bool owned = false;
return name_has_owner_response && reader.PopBool(&owned) && owned;
}
bool ShouldPreventDisplaySleep(mojom::WakeLockType type) {
switch (type) {
case mojom::WakeLockType::kPreventAppSuspension:
return false;
case mojom::WakeLockType::kPreventDisplaySleep:
case mojom::WakeLockType::kPreventDisplaySleepAllowDimming:
return true;
}
NOTREACHED();
return false;
}
const char* GetUninhibitMethodName(DBusAPI api) {
switch (api) {
case GNOME_API:
return "Uninhibit";
case FREEDESKTOP_POWER_API:
case FREEDESKTOP_SCREENSAVER_API:
return "UnInhibit";
}
NOTREACHED();
return nullptr;
}
void GetDbusStringsForApi(DBusAPI api,
const char** service_name,
const char** interface_name,
const char** object_path) {
switch (api) {
case GNOME_API:
*service_name = kGnomeAPIServiceName;
*interface_name = kGnomeAPIInterfaceName;
*object_path = kGnomeAPIObjectPath;
return;
case FREEDESKTOP_POWER_API:
*service_name = kFreeDesktopAPIPowerServiceName;
*interface_name = kFreeDesktopAPIPowerInterfaceName;
*object_path = kFreeDesktopAPIPowerObjectPath;
return;
case FREEDESKTOP_SCREENSAVER_API:
*service_name = kFreeDesktopAPIScreenServiceName;
*interface_name = kFreeDesktopAPIScreenInterfaceName;
*object_path = kFreeDesktopAPIScreenObjectPath;
return;
}
NOTREACHED();
}
} // namespace
class PowerSaveBlocker::Delegate
: public base::RefCountedThreadSafe<PowerSaveBlocker::Delegate> {
public:
// Picks an appropriate D-Bus API to use based on the desktop environment.
Delegate(mojom::WakeLockType type,
const std::string& description,
scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> blocking_task_runner);
Delegate(const Delegate&) = delete;
Delegate& operator=(const Delegate&) = delete;
// Post a task to initialize the delegate on the UI thread, which will itself
// then post a task to apply the power save block on the blocking task runner.
void Init();
// Post a task to remove the power save block on the blocking task runner,
// unless it hasn't yet been applied, in which case we just prevent it from
// applying.
void CleanUp();
private:
friend class base::RefCountedThreadSafe<Delegate>;
struct InhibitCookie {
DBusAPI api;
uint32_t cookie;
};
~Delegate() = default;
// Returns true if ApplyBlock() / RemoveBlock() should be called.
bool ShouldBlock() const;
// Apply or remove the power save block, respectively. These methods should be
// called once each, on the same thread, per instance. They block waiting for
// the action to complete (with a timeout); the thread must thus allow I/O.
void ApplyBlock();
void RemoveBlock();
// Makes the Inhibit method call. Returns true and saves an entry to
// |inhibit_cookies_| on success.
bool Inhibit(DBusAPI api);
// Makes the Uninhibit method call given an InhibitCookie saved by a prior
// call to Inhibit().
void Uninhibit(const InhibitCookie& inhibit_cookie);
void SetScreenSaverSuspended(bool suspend);
const mojom::WakeLockType type_;
const std::string description_;
scoped_refptr<dbus::Bus> bus_;
std::vector<InhibitCookie> inhibit_cookies_;
scoped_refptr<base::SequencedTaskRunner> ui_task_runner_;
scoped_refptr<base::SingleThreadTaskRunner> blocking_task_runner_;
std::unique_ptr<display::Screen::ScreenSaverSuspender>
screen_saver_suspender_;
};
PowerSaveBlocker::Delegate::Delegate(
mojom::WakeLockType type,
const std::string& description,
scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> blocking_task_runner)
: type_(type),
description_(description),
ui_task_runner_(ui_task_runner),
blocking_task_runner_(blocking_task_runner) {
// We're on the client's thread here, so we don't allocate the dbus::Bus
// object yet. We'll do it later in ApplyBlock(), on the blocking task runner.
}
void PowerSaveBlocker::Delegate::Init() {
if (ShouldBlock()) {
blocking_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&Delegate::ApplyBlock, this));
}
ui_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&Delegate::SetScreenSaverSuspended, this, true));
}
void PowerSaveBlocker::Delegate::CleanUp() {
if (ShouldBlock()) {
blocking_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&Delegate::RemoveBlock, this));
}
ui_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&Delegate::SetScreenSaverSuspended, this, false));
}
bool PowerSaveBlocker::Delegate::ShouldBlock() const {
// Power saving APIs are not accessible in headless mode.
return !base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kHeadless);
}
void PowerSaveBlocker::Delegate::ApplyBlock() {
DCHECK(blocking_task_runner_->RunsTasksInCurrentSequence());
DCHECK(!bus_); // ApplyBlock() should only be called once.
dbus::Bus::Options options;
options.bus_type = dbus::Bus::SESSION;
options.connection_type = dbus::Bus::PRIVATE;
bus_ = base::MakeRefCounted<dbus::Bus>(options);
// First try to inhibit using the GNOME API, since we can inhibit both the
// screensaver and power with one method call.
if (!Inhibit(GNOME_API)) {
// Couldn't inhibit using GNOME, so try the Freedesktop ScreenSaver API
// next, if necessary.
if (ShouldPreventDisplaySleep(type_))
Inhibit(FREEDESKTOP_SCREENSAVER_API);
// For every WakeLockType, we want to inhibit suspend/sleep/etc.
Inhibit(FREEDESKTOP_POWER_API);
}
}
void PowerSaveBlocker::Delegate::RemoveBlock() {
DCHECK(blocking_task_runner_->RunsTasksInCurrentSequence());
DCHECK(bus_); // RemoveBlock() should only be called once.
for (const auto& inhibit_cookie : inhibit_cookies_)
Uninhibit(inhibit_cookie);
inhibit_cookies_.clear();
bus_->ShutdownAndBlock();
bus_.reset();
}
bool PowerSaveBlocker::Delegate::Inhibit(DBusAPI api) {
const char* service_name;
const char* interface_name;
const char* object_path;
GetDbusStringsForApi(api, &service_name, &interface_name, &object_path);
// Check that the service name has an owner before making any calls,
// Otherwise if the service does not exist, we will block for the default DBus
// timeout, which can be large on some systems.
if (!ServiceNameHasOwner(bus_.get(), service_name))
return false;
dbus::ObjectProxy* object_proxy =
bus_->GetObjectProxy(service_name, dbus::ObjectPath(object_path));
auto method_call =
std::make_unique<dbus::MethodCall>(interface_name, "Inhibit");
auto message_writer =
std::make_unique<dbus::MessageWriter>(method_call.get());
switch (api) {
case GNOME_API:
// The arguments of the method are:
// app_id: The application identifier
// toplevel_xid: The toplevel X window identifier
// reason: The reason for the inhibit
// flags: Flags that spefify what should be inhibited
message_writer->AppendString(
base::CommandLine::ForCurrentProcess()->GetProgram().value());
message_writer->AppendUint32(0); // should be toplevel_xid
message_writer->AppendString(description_);
{
uint32_t flags = 0;
switch (type_) {
case mojom::WakeLockType::kPreventDisplaySleep:
case mojom::WakeLockType::kPreventDisplaySleepAllowDimming:
flags |= INHIBIT_MARK_SESSION_IDLE;
flags |= INHIBIT_SUSPEND_SESSION;
break;
case mojom::WakeLockType::kPreventAppSuspension:
flags |= INHIBIT_SUSPEND_SESSION;
break;
}
message_writer->AppendUint32(flags);
}
break;
case FREEDESKTOP_POWER_API:
case FREEDESKTOP_SCREENSAVER_API:
// The arguments of the method are:
// app_id: The application identifier
// reason: The reason for the inhibit
message_writer->AppendString(
base::CommandLine::ForCurrentProcess()->GetProgram().value());
message_writer->AppendString(description_);
break;
}
std::unique_ptr<dbus::Response> response =
object_proxy
->CallMethodAndBlock(method_call.get(),
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)
.value_or(nullptr);
uint32_t cookie;
if (response) {
// The method returns an inhibit_cookie, used to uniquely identify
// this request. It should be used as an argument to Uninhibit()
// in order to remove the request.
dbus::MessageReader message_reader(response.get());
if (!message_reader.PopUint32(&cookie)) {
LOG(ERROR) << "Invalid Inhibit() response: " << response->ToString();
return false;
}
} else {
LOG(ERROR) << "No response to Inhibit() request!";
return false;
}
inhibit_cookies_.push_back({api, cookie});
return true;
}
void PowerSaveBlocker::Delegate::Uninhibit(
const InhibitCookie& inhibit_cookie) {
const char* service_name;
const char* interface_name;
const char* object_path;
GetDbusStringsForApi(inhibit_cookie.api, &service_name, &interface_name,
&object_path);
DCHECK(ServiceNameHasOwner(bus_.get(), service_name));
dbus::ObjectProxy* object_proxy =
bus_->GetObjectProxy(service_name, dbus::ObjectPath(object_path));
auto method_call = std::make_unique<dbus::MethodCall>(
interface_name, GetUninhibitMethodName(inhibit_cookie.api));
auto message_writer =
std::make_unique<dbus::MessageWriter>(method_call.get());
message_writer->AppendUint32(inhibit_cookie.cookie);
std::unique_ptr<dbus::Response> response =
object_proxy
->CallMethodAndBlock(method_call.get(),
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)
.value_or(nullptr);
// We don't care about checking the result. We assume it works; we can't
// really do anything about it anyway if it fails.
if (!response)
LOG(ERROR) << "No response to Uninhibit() request!";
}
void PowerSaveBlocker::Delegate::SetScreenSaverSuspended(bool suspend) {
if (suspend) {
DCHECK(!screen_saver_suspender_);
// The screen can be nullptr in tests.
if (auto* const screen = display::Screen::GetScreen())
screen_saver_suspender_ = screen->SuspendScreenSaver();
} else {
screen_saver_suspender_.reset();
}
}
PowerSaveBlocker::PowerSaveBlocker(
mojom::WakeLockType type,
mojom::WakeLockReason reason,
const std::string& description,
scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> blocking_task_runner)
: delegate_(base::MakeRefCounted<Delegate>(type,
description,
ui_task_runner,
blocking_task_runner)),
ui_task_runner_(ui_task_runner),
blocking_task_runner_(blocking_task_runner) {
delegate_->Init();
}
PowerSaveBlocker::~PowerSaveBlocker() {
delegate_->CleanUp();
}
} // namespace device