blob: 29c8e5674bbd9aa7f74a074de67b77ae2da45c4e [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/updater/linux/systemd_util.h"
#include <fcntl.h>
#include <sys/un.h>
#include <systemd/sd-daemon.h>
#include <unistd.h>
#include <optional>
#include <utility>
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/linux/ipc_constants.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/util/posix_util.h"
namespace updater {
// Allows the utility functions below to join processes. To avoid overzealously
// granting access to |base::ScopedAllowBaseSyncPrimitives|, this class must
// continue to live in a `.cc`.
class [[maybe_unused, nodiscard]] SystemctlLauncherScopedAllowBaseSyncPrimitives
: public base::ScopedAllowBaseSyncPrimitives {};
namespace {
// Location of system-scoped unit files.
const base::FilePath kSystemUnitDirectory("/etc/systemd/system");
// Location of user-scoped unit files relative to the user's home directory.
const base::FilePath kUserUnitRelativeDirectory(".local/share/systemd/user");
// Systemd unit names.
constexpr char kUpdaterServiceName[] = PRODUCT_FULLNAME_STRING ".service";
constexpr char kUpdaterSocketName[] = PRODUCT_FULLNAME_STRING ".socket";
// Systemd unit definition templates.
constexpr char kUpdaterServiceDefinitionTemplate[] =
"[Service]\n"
"ExecStart=%s\n"
"KillMode=process"; // Ensure systemd does not kill child processes when
// the main process exits.
constexpr char kUpdaterSocketDefinitionTemplate[] =
"[Socket]\n"
"ListenStream=%s\n"
"\n"
"[Install]\n"
"WantedBy=sockets.target";
// Returns the path to the systemd unit directory for the given scope.
std::optional<base::FilePath> GetUnitDirectory(UpdaterScope scope) {
base::FilePath unit_dir;
switch (scope) {
case UpdaterScope::kUser:
if (!base::PathService::Get(base::DIR_HOME, &unit_dir)) {
return std::nullopt;
}
unit_dir = unit_dir.Append(kUserUnitRelativeDirectory);
if (!base::CreateDirectory(unit_dir)) {
return std::nullopt;
}
break;
case UpdaterScope::kSystem:
unit_dir = base::FilePath(kSystemUnitDirectory);
}
return unit_dir;
}
// Returns the command `systemctl` with or without the `--user` flag.
base::CommandLine GetBaseSystemctlCommand(UpdaterScope scope) {
base::CommandLine command({"systemctl"});
if (scope == UpdaterScope::kUser) {
command.AppendSwitch("user");
}
return command;
}
// Launch the given command line with stdin, stdout, and stderr remapped to
// /dev/null.
void LaunchWithRemaps(base::CommandLine command) {
base::LaunchOptions options;
base::ScopedFD null_fd(HANDLE_EINTR(open("/dev/null", O_RDWR)));
if (null_fd.is_valid()) {
options.fds_to_remap.emplace_back(null_fd.get(), STDIN_FILENO);
}
base::Process proc = base::LaunchProcess(command, options);
if (!proc.IsValid()) {
VLOG(1) << "Could not launch " << command.GetCommandLineString();
} else {
SystemctlLauncherScopedAllowBaseSyncPrimitives allow_wait;
proc.WaitForExit(nullptr);
}
}
// Runs the daemon-reload command to have systemd reload unit files.
void ReloadUnitFiles(UpdaterScope scope) {
base::CommandLine command = GetBaseSystemctlCommand(scope);
command.AppendArg("daemon-reload");
LaunchWithRemaps(command);
}
// Enables the socket unit to be created and managed by systemd at boot.
void EnableSocketUnit(UpdaterScope scope) {
base::CommandLine command = GetBaseSystemctlCommand(scope);
command.AppendArg("enable");
command.AppendSwitch("now");
command.AppendArg(kUpdaterSocketName);
LaunchWithRemaps(command);
}
// Ensures that the update service and socket are stopped.
void StopService(UpdaterScope scope) {
base::CommandLine command = GetBaseSystemctlCommand(scope);
command.AppendArg("stop");
command.AppendArg(kUpdaterServiceName);
command.AppendArg(kUpdaterSocketName);
LaunchWithRemaps(command);
}
// Writes the contents of |unit_definition| to |unit_path|. Returns true on
// success.
[[nodiscard]] bool InstallSystemdUnit(base::FilePath unit_path,
std::string unit_definition) {
base::File unit_file(unit_path,
base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
return unit_file.IsValid() &&
unit_file.WriteAndCheck(0, base::as_byte_span(unit_definition));
}
// Returns the command line which should be used to start the updater service.
std::string GetLauncherCommandLine(UpdaterScope scope,
base::FilePath launcher) {
base::CommandLine command(launcher);
command.AppendSwitch(kServerSwitch);
command.AppendSwitchUTF8(kServerServiceSwitch,
kServerUpdateServiceSwitchValue);
if (scope == UpdaterScope::kSystem) {
command.AppendSwitch(kSystemSwitch);
}
return command.GetCommandLineString();
}
} // namespace
SystemdService::SystemdService() {
if (sd_listen_fds(0) == 1) {
server_socket_ = base::ScopedFD(SD_LISTEN_FDS_START + 0);
read_watcher_controller_ = base::FileDescriptorWatcher::WatchReadable(
server_socket_.get(),
base::BindRepeating(&SystemdService::OnSocketReadable,
weak_factory_.GetWeakPtr()));
}
}
SystemdService::~SystemdService() = default;
void SystemdService::OnSocketReadable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(server_socket_.is_valid());
base::ScopedFD remote_fd(accept(server_socket_.get(), nullptr, nullptr));
if (!remote_fd.is_valid()) {
VPLOG(1) << "Failed to accept connection on activation socket.";
}
}
bool InstallSystemdUnits(UpdaterScope scope) {
std::optional<base::FilePath> launcher_path =
GetUpdateServiceLauncherPath(scope);
std::optional<base::FilePath> unit_dir = GetUnitDirectory(scope);
if (!launcher_path || !unit_dir) {
return false;
}
// Uninstall existing units if they exist.
UninstallSystemdUnits(scope);
if (!InstallSystemdUnit(
unit_dir->Append(kUpdaterServiceName),
base::StringPrintf(
kUpdaterServiceDefinitionTemplate,
GetLauncherCommandLine(scope, *launcher_path).c_str())) ||
!InstallSystemdUnit(
unit_dir->Append(kUpdaterSocketName),
base::StringPrintf(
kUpdaterSocketDefinitionTemplate,
GetActivationSocketPath(scope).AsUTF8Unsafe().c_str()))) {
// Avoid a partial installation.
UninstallSystemdUnits(scope);
return false;
}
ReloadUnitFiles(scope);
EnableSocketUnit(scope);
LOG_IF(ERROR, !base::PathExists(GetActivationSocketPath(scope)))
<< "Activation socket file is missing post-install.";
return true;
}
bool UninstallSystemdUnits(UpdaterScope scope) {
std::optional<base::FilePath> unit_dir = GetUnitDirectory(scope);
if (!unit_dir) {
return false;
}
StopService(scope);
// Note: It is considered successful to attempt to delete units that do not
// exist.
bool success = base::DeleteFile(unit_dir->Append(kUpdaterServiceName)) &&
base::DeleteFile(unit_dir->Append(kUpdaterSocketName));
ReloadUnitFiles(scope);
return success;
}
bool SystemdUnitsInstalled(UpdaterScope scope) {
std::optional<base::FilePath> unit_dir = GetUnitDirectory(scope);
if (!unit_dir) {
return false;
}
return base::PathExists(unit_dir->Append(kUpdaterServiceName)) ||
base::PathExists(unit_dir->Append(kUpdaterSocketName));
}
} // namespace updater