Implement bfebbx: Betaflight blackbox extractor
Cross-platform (macOS + Linux) C++17 CLI that extracts a Betaflight FC's blackbox log in one command: auto-detect the serial port, read craft_name over the Betaflight CLI, send `msc` to reboot into USB mass storage, wait for the volume to mount, copy btfl_all.bbl to <craft_name>_<YYYYMMDD>_<HHMMSS>.bbl, and eject. Native throughout (termios serial, getmntinfo/proc-mounts mount detection, DiskArbitration/umount eject); the only subprocess is the Linux udisksctl eject fallback. Includes CMake/ctest build, unit tests for the pure logic (naming, craft_name parsing, mount diff, serial via pty, CLI parsing), README with a manual hardware integration procedure, and design/plan docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
107
src/eject.cpp
Normal file
107
src/eject.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "eject.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
|
||||
namespace bfebbx {
|
||||
|
||||
namespace {
|
||||
struct EjectCtx {
|
||||
bool done = false;
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
void unmountCallback(DADiskRef, DADissenterRef dissenter, void* c) {
|
||||
auto* ctx = static_cast<EjectCtx*>(c);
|
||||
ctx->ok = (dissenter == nullptr);
|
||||
ctx->done = true;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ejectVolume(const MountEntry& vol) {
|
||||
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
|
||||
if (!session) throw std::runtime_error("DASessionCreate failed");
|
||||
DASessionScheduleWithRunLoop(session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
||||
|
||||
CFURLRef url = CFURLCreateFromFileSystemRepresentation(
|
||||
kCFAllocatorDefault,
|
||||
reinterpret_cast<const UInt8*>(vol.mountpoint.c_str()),
|
||||
static_cast<CFIndex>(vol.mountpoint.size()), true);
|
||||
DADiskRef disk = url ? DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url)
|
||||
: nullptr;
|
||||
|
||||
EjectCtx ctx;
|
||||
if (disk) {
|
||||
DADiskUnmount(disk, kDADiskUnmountOptionWhole, unmountCallback, &ctx);
|
||||
// Bound the wait: DiskArbitration's callback normally always fires, but a
|
||||
// daemon crash or invalidated disk could otherwise hang the process forever.
|
||||
const CFTimeInterval kUnmountTimeout = 15.0; // seconds, total
|
||||
while (!ctx.done) {
|
||||
CFRunLoopRunResult r =
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, kUnmountTimeout, true);
|
||||
if (r == kCFRunLoopRunTimedOut || r == kCFRunLoopRunFinished) break;
|
||||
// kCFRunLoopRunStopped (callback called CFRunLoopStop) or
|
||||
// kCFRunLoopRunHandledSource: re-check ctx.done and continue.
|
||||
}
|
||||
CFRelease(disk);
|
||||
}
|
||||
if (url) CFRelease(url);
|
||||
DASessionUnscheduleFromRunLoop(session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
||||
CFRelease(session);
|
||||
|
||||
if (!disk) throw std::runtime_error("could not resolve disk for " + vol.mountpoint);
|
||||
if (!ctx.done) throw std::runtime_error("unmount timed out for " + vol.mountpoint);
|
||||
if (!ctx.ok) throw std::runtime_error("unmount denied for " + vol.mountpoint);
|
||||
}
|
||||
|
||||
} // namespace bfebbx
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
#include <sys/mount.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
namespace bfebbx {
|
||||
|
||||
namespace {
|
||||
// Run a command with no shell; returns the exit status (or -1 on spawn failure).
|
||||
int runNoShell(const char* const argv[]) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
execvp(argv[0], const_cast<char* const*>(argv));
|
||||
_exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
while (waitpid(pid, &status, 0) < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ejectVolume(const MountEntry& vol) {
|
||||
if (umount(vol.mountpoint.c_str()) == 0) return;
|
||||
if (errno == EPERM || errno == EACCES) {
|
||||
const char* argv[] = {"udisksctl", "unmount", "-b", vol.device.c_str(), nullptr};
|
||||
if (runNoShell(argv) == 0) return;
|
||||
throw std::runtime_error("umount denied and udisksctl fallback failed for " +
|
||||
vol.mountpoint);
|
||||
}
|
||||
throw std::runtime_error("umount " + vol.mountpoint + ": " + std::strerror(errno));
|
||||
}
|
||||
|
||||
} // namespace bfebbx
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user