113 lines
3.6 KiB
C++
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
|