Skip to content
Merged
56 changes: 25 additions & 31 deletions binlog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,37 +37,27 @@ BinlogWriter::~BinlogWriter()
close();
}

bool BinlogWriter::open(uint32_t port2, unsigned session_n, const char *base_dir)
bool BinlogWriter::open(uint32_t port2, const char *base_dir)
{
if (fp != nullptr) {
return true;
}

time_t now = time(nullptr);
struct tm tm_now;
localtime_r(&now, &tm_now);

char dir[768];
snprintf(dir, sizeof(dir), "%s/%u/%04d-%02d-%02d",
base_dir, port2,
tm_now.tm_year + 1900,
tm_now.tm_mon + 1,
tm_now.tm_mday);
snprintf(dir, sizeof(dir), "%s/%u/%s", base_dir, port2, datedir_.c_str());

if (mkpath_0700(dir) < 0) {
::printf("binlog: mkdir %s failed: %s\n", dir, strerror(errno));
return false;
}

char path[1024];
snprintf(path, sizeof(path), "%s/session%u.bin", dir, session_n);

// O_RDWR via "rb+"-then-"wb+" dance: blocks arrive out of order so
// we need to seek + write to arbitrary offsets. fopen "ab" forces
// every write to the end. Use "wb+" to truncate and create, or
// "rb+" to keep an existing file (rare — child crashed mid-session
// and a new fork is reusing the same N? next_session_n() rules
// that out, but defensively allow it).
snprintf(path, sizeof(path), "%s/%s.bin", dir, name_.c_str());

// O_RDWR via "wb+": blocks arrive out of order so we seek + write to
// arbitrary offsets ("ab" would force every write to the end). The
// basename is made unique before the session starts, so this creates
// a fresh file rather than truncating a live one.
fp = fopen(path, "wb+");
if (fp == nullptr) {
::printf("binlog: fopen %s failed: %s\n", path, strerror(errno));
Expand Down Expand Up @@ -181,7 +171,7 @@ void BinlogWriter::mark_seqno_seen(uint32_t seqno)
seen_bitmap[byte] |= uint8_t(1u << (seqno & 7));
}

void BinlogWriter::handle_block(uint32_t port2, unsigned session_n,
void BinlogWriter::handle_block(uint32_t port2,
const mavlink_message_t &msg)
{
mavlink_remote_log_data_block_t blk {};
Expand Down Expand Up @@ -225,11 +215,9 @@ void BinlogWriter::handle_block(uint32_t port2, unsigned session_n,
}
return;
}
unsigned n = (pending_session_n_ != 0) ? pending_session_n_ : session_n;
if (!open(port2, n)) {
if (!open(port2)) {
return;
}
pending_session_n_ = 0;
gated_block_count_ = 0;
}

Expand Down Expand Up @@ -606,14 +594,20 @@ bool BinlogWriter::rotate_for_reboot()
// pre-stream loop; the 5 s keep-alive (re-armed above) covers
// reboot recovery.

// Pre-compute the next session N but do NOT open the file yet.
// The handle_block() strict-start gate (fp == nullptr + seqno!=0
// is dropped) keeps the file unopened until a fresh seqno=0 from
// the rebooted vehicle, so any delayed pre-reboot blocks still
// in flight can't sparse-write into sessionN+1.bin. The open()
// happens lazily in handle_block using pending_session_n_.
pending_session_n_ = next_session_n(port2_, base_dir_.c_str());
::printf("binlog: rotation armed; next open will be session%u.bin "
"(awaiting fresh seqno=0)\n", pending_session_n_);
// Rebuild datedir_/name_ with a fresh reboot-time timestamp but do
// NOT open the file yet. The handle_block() strict-start gate
// (fp == nullptr + seqno!=0 dropped) keeps the file unopened until a
// fresh seqno=0 from the rebooted vehicle, so delayed pre-reboot
// blocks can't sparse-write into the new file. The open() happens
// lazily in handle_block using the new datedir_/name_.
char datedir[16], name[64];
session_time_strings(time(nullptr), tz_use_offset_, tz_offset_hours_,
datedir, sizeof(datedir), name, sizeof(name));
session_unique_basename(base_dir_.c_str(), port2_, datedir,
name, sizeof(name));
datedir_ = datedir;
name_ = name;
::printf("binlog: rotation armed; next open will be %s/%s.bin "
"(awaiting fresh seqno=0)\n", datedir_.c_str(), name_.c_str());
return true;
}
72 changes: 48 additions & 24 deletions binlog.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,35 @@ class BinlogWriter {
BinlogWriter &operator=(const BinlogWriter &) = delete;

/*
open logs/<port2>/<YYYY-MM-DD>/sessionN.bin. Caller supplies
session_n via the shared next_session_n() helper so the .bin
and .tlog for one child fork share their N.
open logs/<port2>/<datedir>/<name>.bin. The timestamp datedir +
basename come from set_session_paths() (set once at fork so the
.bin and .tlog for one child share their name), or are recomputed
by rotate_for_reboot() for the post-reboot file.
*/
bool open(uint32_t port2, unsigned session_n,
const char *base_dir = "logs");
bool open(uint32_t port2, const char *base_dir = "logs");
bool is_open() const { return fp != nullptr; }

/*
Set the timestamp date subdir + basename for the (lazy) initial
open. Called once by the parent at fork with the shared session
name so .bin and .tlog pair up. rotate_for_reboot() overwrites
these with the reboot-time name for the next file.
*/
void set_session_paths(const char *datedir, const char *name) {
datedir_ = datedir;
name_ = name;
}

/*
Log-naming timezone for the reboot-rotated file's name: whether to
apply the fixed GMT offset (KEY_FLAG_USE_TZ) and the offset itself.
Sourced from the KeyEntry at fork.
*/
void set_tz(bool use_offset, double offset_hours) {
tz_use_offset_ = use_offset;
tz_offset_hours_ = offset_hours;
}

/*
Decode a REMOTE_LOG_DATA_BLOCK message and process it.

Expand All @@ -66,12 +87,10 @@ class BinlogWriter {
jump (seqno > highest_seen + 1) the gap is recorded for NACK
in tick().

port2 + session_n are used only on the (lazy) open. They're
passed every call rather than stored so BinlogWriter doesn't
need to copy strings.
port2 is used only on the (lazy) open; the datedir/name come from
set_session_paths().
*/
void handle_block(uint32_t port2, unsigned session_n,
const mavlink_message_t &msg);
void handle_block(uint32_t port2, const mavlink_message_t &msg);

/*
Observe an arbitrary user-side MAVLink message for FC-reboot
Expand Down Expand Up @@ -247,12 +266,22 @@ class BinlogWriter {
double last_stop_sent_s = 0.0;

// Captured on the first successful open() so rotate_for_reboot()
// can re-scan the per-day dir for a fresh session N without
// dragging port2/base_dir through every observe() / handle_block()
// signature.
// can rebuild paths without dragging port2/base_dir through every
// observe() / handle_block() signature.
uint32_t port2_ = 0;
std::string base_dir_;

// Timestamp date subdir ("YYYY-MM-DD") and basename
// ("YYYY_MM_DD_HH:MM:SS") for the file. Set once at fork via
// set_session_paths() so .bin and .tlog pair up; rebuilt by
// rotate_for_reboot() for the post-reboot file.
std::string datedir_;
std::string name_;
// Log-naming timezone for rotate's new name: whether the fixed GMT
// offset is in use (KEY_FLAG_USE_TZ) and its value.
bool tz_use_offset_ = false;
double tz_offset_hours_ = 0.0;

// Current size of fp (= the largest seqno+1 we've written * 200).
// Tracked locally rather than fstat'ing on every write.
off_t current_file_size_ = 0;
Expand Down Expand Up @@ -280,17 +309,12 @@ class BinlogWriter {
// SYSTEM_TIME can't trigger a spurious reboot).
uint32_t last_system_time_boot_ms_ = 0;

// Close + reset per-log state + arm pending_session_n_ for the
// NEXT open. Used when observe() decides the FC has rebooted.
// Does NOT re-open here — the new file is only opened when a
// fresh seqno=0 arrives, so the existing strict-start gate keeps
// protecting the rotated file from delayed pre-reboot blocks.
// Does NOT reset per-vehicle state (target_system/component,
// Close + reset per-log state + rebuild datedir_/name_ for the NEXT
// open (a fresh reboot-time timestamp). Used when observe() decides
// the FC has rebooted. Does NOT re-open here — the new file is only
// opened when a fresh seqno=0 arrives, so the existing strict-start
// gate keeps protecting the rotated file from delayed pre-reboot
// blocks. Does NOT reset per-vehicle state (target_system/component,
// fc_sysid_filter_).
bool rotate_for_reboot();

// Session N to use on the NEXT open(), set by rotate_for_reboot()
// and consumed on the gate-triggered open in handle_block.
// 0 = no pending rotation; use the caller's session_n argument.
unsigned pending_session_n_ = 0;
};
4 changes: 3 additions & 1 deletion keydb.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#define KEY_FLAG_BIDI_SIGN (1u << 1) // require signed MAVLink on the user side too
#define KEY_FLAG_TLOG (1u << 2) // record per-connection MAVProxy-format tlogs
#define KEY_FLAG_BINLOG (1u << 3) // record ArduPilot bin logs over MAVLink
#define KEY_FLAG_USE_TZ (1u << 4) // name logs with tz_offset_hours; else server local

struct KeyEntry {
uint64_t magic;
Expand All @@ -46,7 +47,8 @@ struct KeyEntry {
uint32_t flags;
float log_retention_days; // tlog + bin; 0.0 = forever; fractional values allowed for tests
uint32_t fc_sysid; // 0 = match any; otherwise only monitor packets from this MAVLink sysid (binlog reboot detection)
uint32_t reserved[15];
float tz_offset_hours; // log naming: GMT offset in hours (fractional allowed), used only when KEY_FLAG_USE_TZ is set
uint32_t reserved[14];
};

/*
Expand Down
15 changes: 15 additions & 0 deletions keydb.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def main():
'setflag', 'clearflag', 'flags',
'setretention',
'setsysid',
'settz',
'stats'],
help="action to perform")
parser.add_argument("args", default=[], nargs=argparse.REMAINDER)
Expand Down Expand Up @@ -135,6 +136,20 @@ def main():
else:
print("Set fc_sysid=%u for %s" % (sysid, ke))

elif args.action == "settz":
_expect(args.args, 2,
"keydb.py settz PORT2 HOURS "
"(GMT offset in hours, fractional ok; enables use_tz. "
"Clear the use_tz flag to revert to server-local naming.)")
try:
hours = float(args.args[1])
except ValueError:
raise CLIError("HOURS must be a number, got %r"
% args.args[1])
ke = keydb_lib.set_timezone(db, int(args.args[0]), hours)
print("Set log timezone=%s for %s"
% (keydb_lib.format_tz_offset(hours), ke))

elif args.action == "stats":
# Live-connection stats from connections.tdb (sibling of
# keys.tdb), joined with each entry's name from this DB.
Expand Down
Loading
Loading