1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
|
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/dbus/xdg/systemd.h"
#include <string>
#include <variant>
#include <vector>
#include "base/environment.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "build/branding_buildflags.h"
#include "components/dbus/properties/types.h"
#include "components/dbus/utils/name_has_owner.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "dbus/property.h"
namespace dbus_xdg {
// Systemd dictionaries use structs instead of dict entries for some reason.
template <typename T>
using Dict = DbusArray<DbusStruct<DbusString, T>>;
using VarDict = Dict<DbusVariant>;
using SystemdUnitCallbacks = std::vector<SystemdUnitCallback>;
using StatusOrCallbacks = std::variant<SystemdUnitStatus, SystemdUnitCallbacks>;
namespace {
constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
constexpr char kMethodGetUnit[] = "GetUnit";
constexpr char kInterfaceSystemdUnit[] = "org.freedesktop.systemd1.Unit";
constexpr char kActiveStateProp[] = "ActiveState";
constexpr char kUnitNameFormat[] = "app-$1$2-$3.scope";
constexpr char kModeReplace[] = "replace";
constexpr char kChannelEnvVar[] = "CHROME_VERSION_EXTRA";
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
constexpr char kAppNamePrefix[] = "com.google.Chrome";
#else
constexpr char kAppNamePrefix[] = "org.chromium.Chromium";
#endif
const char* GetAppNameSuffix(const std::string& channel) {
if (channel == "beta") {
return ".beta";
}
if (channel == "unstable") {
return ".unstable";
}
if (channel == "canary") {
return ".canary";
}
// No suffix for stable. Also if the channel is unknown, the most likely
// scenario is the user is running the binary directly and not getting the
// environment variable set, so assume stable to minimize potential risk of
// settings or data loss.
return "";
}
// Declare this helper for SystemdUnitActiveStateWatcher to be used.
void SetStateAndRunCallbacks(SystemdUnitStatus result);
// Watches the object to become active and fires callbacks via
// SetStateAndRunCallbacks. The callbacks are fired whenever a response with the
// state being "active" or "failed" (or similar) comes.
//
// PS firing callbacks results in destroying this object. So any references
// to this become invalid.
class SystemdUnitActiveStateWatcher : public dbus::PropertySet {
public:
SystemdUnitActiveStateWatcher(scoped_refptr<dbus::Bus> bus,
dbus::ObjectProxy* object_proxy)
: dbus::PropertySet(object_proxy,
kInterfaceSystemdUnit,
base::BindRepeating(
&SystemdUnitActiveStateWatcher::OnPropertyChanged,
base::Unretained(this))),
bus_(bus) {
RegisterProperty(kActiveStateProp, &active_state_);
ConnectSignals();
GetAll();
}
~SystemdUnitActiveStateWatcher() override {
bus_->RemoveObjectProxy(kServiceNameSystemd, object_proxy()->object_path(),
base::DoNothing());
}
private:
void OnPropertyChanged(const std::string& property_name) {
DCHECK(active_state_.is_valid());
const std::string state_value = active_state_.value();
if (callbacks_called_ || state_value == "activating" ||
state_value == "reloading") {
// Ignore if callbacks have already been fired or continue waiting until
// the state changes to something else.
return;
}
// There are other states as failed, inactive, and deactivating. Treat all
// of them as failed.
callbacks_called_ = true;
SetStateAndRunCallbacks(state_value == "active"
? SystemdUnitStatus::kUnitStarted
: SystemdUnitStatus::kFailedToStart);
MaybeDeleteSelf();
}
void OnGetAll(dbus::Response* response) override {
dbus::PropertySet::OnGetAll(response);
keep_alive_ = false;
MaybeDeleteSelf();
}
void MaybeDeleteSelf() {
if (!keep_alive_ && callbacks_called_) {
delete this;
}
}
// Registered property that this listens updates to.
dbus::Property<std::string> active_state_;
// Indicates whether callbacks for the unit's state have been called.
bool callbacks_called_ = false;
// Control variable that helps to defer the destruction of |this| as deleting
// self when the state changes to active during |OnGetAll| will result in a
// segfault.
bool keep_alive_ = true;
scoped_refptr<dbus::Bus> bus_;
};
// Global state for cached result or pending callbacks.
StatusOrCallbacks& GetUnitNameState() {
static base::NoDestructor<StatusOrCallbacks> state(
std::in_place_type<SystemdUnitCallbacks>);
return *state;
}
void SetStateAndRunCallbacks(SystemdUnitStatus result) {
auto& state = GetUnitNameState();
auto callbacks = std::move(std::get<SystemdUnitCallbacks>(state));
state = result;
for (auto& callback : callbacks) {
std::move(callback).Run(result);
}
}
void OnGetPathResponse(scoped_refptr<dbus::Bus> bus, dbus::Response* response) {
dbus::MessageReader reader(response);
dbus::ObjectPath obj_path;
if (!response || !reader.PopObjectPath(&obj_path) || !obj_path.IsValid()) {
// We didn't get a valid response. Treat this as failed service.
SetStateAndRunCallbacks(SystemdUnitStatus::kFailedToStart);
return;
}
dbus::ObjectProxy* unit_proxy =
bus->GetObjectProxy(kServiceNameSystemd, obj_path);
// Create the active state property watcher. It will destroy itself once
// it gets notified about the state change.
std::unique_ptr<SystemdUnitActiveStateWatcher> active_state_watcher =
std::make_unique<SystemdUnitActiveStateWatcher>(bus, unit_proxy);
active_state_watcher.release();
}
void WaitUnitActivateAndRunCallbacks(scoped_refptr<dbus::Bus> bus,
std::string unit_name) {
// Get the path of the unit, which looks similar to
// /org/freedesktop/systemd1/unit/app_2dorg_2echromium_2eChromium_2d3182191_2escope
// and then wait for it activation.
dbus::ObjectProxy* systemd = bus->GetObjectProxy(
kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
dbus::MethodCall method_call(kInterfaceSystemdManager, kMethodGetUnit);
dbus::MessageWriter writer(&method_call);
writer.AppendString(unit_name);
systemd->CallMethod(&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&OnGetPathResponse, std::move(bus)));
}
void OnStartTransientUnitResponse(scoped_refptr<dbus::Bus> bus,
std::string unit_name,
dbus::Response* response) {
SystemdUnitStatus result = response ? SystemdUnitStatus::kUnitStarted
: SystemdUnitStatus::kFailedToStart;
// If the start of the unit failed, immediately notify the client. Otherwise,
// wait for its activation.
if (result == SystemdUnitStatus::kFailedToStart) {
SetStateAndRunCallbacks(result);
} else {
WaitUnitActivateAndRunCallbacks(std::move(bus), unit_name);
}
}
void OnNameHasOwnerResponse(scoped_refptr<dbus::Bus> bus,
std::optional<bool> name_has_owner) {
if (!name_has_owner.value_or(false)) {
SetStateAndRunCallbacks(SystemdUnitStatus::kNoSystemdService);
return;
}
pid_t pid = getpid();
if (pid <= 1) {
SetStateAndRunCallbacks(SystemdUnitStatus::kInvalidPid);
return;
}
auto env = base::Environment::Create();
std::string channel = env->GetVar(kChannelEnvVar).value_or("");
const char* app_name_suffix = GetAppNameSuffix(channel);
// The unit naming format is specified in
// https://systemd.io/DESKTOP_ENVIRONMENTS/
auto unit_name = base::ReplaceStringPlaceholders(
kUnitNameFormat,
{kAppNamePrefix, app_name_suffix, base::NumberToString(pid)}, nullptr);
auto* systemd = bus->GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd));
dbus::MethodCall method_call(kInterfaceSystemdManager,
kMethodStartTransientUnit);
dbus::MessageWriter writer(&method_call);
writer.AppendString(unit_name);
writer.AppendString(kModeReplace);
// For now, only add this process to the new scope. It's possible to add all
// PIDs in the process tree, but there's currently not a benefit.
auto pids = MakeDbusArray(DbusUint32(pid));
VarDict properties(
MakeDbusStruct(DbusString("PIDs"), MakeDbusVariant(std::move(pids))));
properties.Write(&writer);
// No auxiliary units.
Dict<VarDict>().Write(&writer);
systemd->CallMethod(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&OnStartTransientUnitResponse, std::move(bus), unit_name));
}
} // namespace
void SetSystemdScopeUnitNameForXdgPortal(dbus::Bus* bus,
SystemdUnitCallback callback) {
#if DCHECK_IS_ON()
static base::SequenceChecker sequence_checker;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker);
#endif
auto& state = GetUnitNameState();
if (std::holds_alternative<SystemdUnitStatus>(state)) {
// If the result is already cached, run the callback immediately.
std::move(callback).Run(std::get<SystemdUnitStatus>(state));
return;
}
// Add the callback to the list of pending callbacks.
auto& callbacks = std::get<SystemdUnitCallbacks>(state);
callbacks.push_back(std::move(callback));
if (callbacks.size() > 1) {
// An operation is already in progress.
return;
}
auto env = base::Environment::Create();
if (env->HasVar("FLATPAK_SANDBOX_DIR") || env->HasVar("SNAP")) {
// xdg-desktop-portal has a separate reliable way of detecting the
// application name for Flatpak and Snap environments, so the systemd unit
// is not necessary in these cases.
SetStateAndRunCallbacks(SystemdUnitStatus::kUnitNotNecessary);
return;
}
// Check if the systemd service is available
dbus_utils::NameHasOwner(
bus, kServiceNameSystemd,
base::BindOnce(&OnNameHasOwnerResponse, base::WrapRefCounted(bus)));
}
void ResetCachedStateForTesting() {
GetUnitNameState() = SystemdUnitCallbacks();
}
} // namespace dbus_xdg
|