fwt_software/src/core/SingleInstance.cpp

113 lines
3.6 KiB
C++

#include "fgc/SingleInstance.h"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <system_error>
#include <utility>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
namespace fgc {
namespace {
// PID currently recorded in an already-locked file, or 0 if it can't be read.
// Best-effort: the message is nicer with a PID but correctness doesn't need it.
int readPid(int fd) {
char buf[32] = {};
ssize_t n = ::pread(fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) return 0;
buf[n] = '\0';
return static_cast<int>(std::strtol(buf, nullptr, 10));
}
} // namespace
std::string SingleInstance::defaultLockPath() {
const std::string uid = std::to_string(::getuid());
// Deliberately derived from the uid rather than read from $XDG_RUNTIME_DIR:
// that variable is set in an interactive ssh shell but *not* in a
// non-interactive `ssh host cmd` one, so reading it would give the two
// launch paths two different lockfiles - and a lock the launchers disagree
// about is no lock at all. The directory itself is there either way.
std::error_code ec;
const std::string run_dir = "/run/user/" + uid;
if (std::filesystem::is_directory(run_dir, ec))
return run_dir + "/fire_gimbal_control.lock";
return "/tmp/fire_gimbal_control-" + uid + ".lock";
}
SingleInstance SingleInstance::acquire(const std::string& path) {
SingleInstance self;
self.path_ = path;
// O_NOFOLLOW: the /tmp fallback path is world-writable, so refuse to follow a
// symlink another user could have planted there. O_CLOEXEC keeps the lock
// from leaking into anything we exec.
int fd = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600);
if (fd < 0) {
self.error_ = "cannot open lockfile " + path + ": " + std::strerror(errno);
return self;
}
if (::flock(fd, LOCK_EX | LOCK_NB) != 0) {
if (errno == EWOULDBLOCK) {
self.holder_pid_ = readPid(fd);
self.error_ = "another instance is already running";
if (self.holder_pid_ > 0) self.error_ += " (PID " + std::to_string(self.holder_pid_) + ")";
self.error_ += "; attach to it with: scripts/fgc attach";
} else {
self.error_ = "cannot lock " + path + ": " + std::strerror(errno);
}
::close(fd);
return self;
}
// Record our PID for the benefit of the next launch's error message and of
// `fgc status`. Failures here are not fatal - the lock itself is what counts.
const std::string pid = std::to_string(::getpid()) + "\n";
if (::ftruncate(fd, 0) == 0) {
ssize_t ignored = ::pwrite(fd, pid.data(), pid.size(), 0);
(void)ignored;
}
self.fd_ = fd;
return self;
}
SingleInstance::~SingleInstance() { release(); }
SingleInstance::SingleInstance(SingleInstance&& other) noexcept
: fd_(std::exchange(other.fd_, -1)),
holder_pid_(other.holder_pid_),
path_(std::move(other.path_)),
error_(std::move(other.error_)) {}
SingleInstance& SingleInstance::operator=(SingleInstance&& other) noexcept {
if (this != &other) {
release();
fd_ = std::exchange(other.fd_, -1);
holder_pid_ = other.holder_pid_;
path_ = std::move(other.path_);
error_ = std::move(other.error_);
}
return *this;
}
void SingleInstance::release() {
if (fd_ < 0) return;
// Closing the descriptor releases the flock. The file is left in place;
// recreating it on every launch would race with a concurrent acquire.
::close(fd_);
fd_ = -1;
}
} // namespace fgc