#include "fgc/SingleInstance.h" #include #include #include #include #include #include #include #include #include #include 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(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