fix(service): prevent recursive flock deadlock and crontab wipe; increase component RPC timeout

- initd.uc: retry_start_on_wan_up now calls BIN_PATH directly instead of
  SERVICE_INIT to avoid the rc.common flock-1000 self-deadlock that was
  hanging Backend CI for the full 1-hour runner timeout
- initd.uc: schedule_start_retry uses background_command() so inherited
  fd 1000 (procd lock) is closed before the sleep starts
- updates.uc, parental_quota.uc: read /etc/crontabs/root via fs.readfile
  instead of crontab -l — BusyBox exits 1 on an empty file causing
  command_output_from_args to return '' and wipe all user cron entries
- shell/index.ts: COMPONENT_ACTION_RPC_TIMEOUT_MS 10s -> 45s to prevent
  false RPC timeout errors during opkg installs on slow routers
This commit is contained in:
Dushnilin
2026-09-15 11:16:50 +03:00
parent aba424426b
commit 03c51d7606
5 changed files with 31 additions and 8 deletions
+8 -2
View File
@@ -923,7 +923,11 @@ function log_cron_apply_result(result) {
}
function remove_cron_jobs(list_marker, subscription_marker, component_marker) {
let crontab = command_output_from_args([ "crontab", "-l" ]);
// BusyBox `crontab -l` exits with code 1 when the crontab is empty or
// absent; command_output_from_args returns "" in that case and a
// subsequent write would wipe all user cron lines. Read the file
// directly to avoid this.
let crontab = as_string(fs.readfile("/etc/crontabs/root") || "");
let markers = [ list_marker, subscription_marker, component_marker ];
let result = {
crontab: strip_unmarked_tachyon_cron_lines(
@@ -938,10 +942,12 @@ function remove_cron_jobs(list_marker, subscription_marker, component_marker) {
}
function refresh_cron_from_sources(settings, sections, bin, list_marker, subscription_marker, component_marker) {
// Same as remove_cron_jobs: read /etc/crontabs/root directly to avoid
// BusyBox's `crontab -l` exit-code-1 on an empty or absent file.
let result = cron_refresh_apply_result(
settings,
sections,
command_output_from_args([ "crontab", "-l" ]),
as_string(fs.readfile("/etc/crontabs/root") || ""),
bin,
list_marker,
subscription_marker,
+16 -3
View File
@@ -284,10 +284,19 @@ function schedule_start_retry(path, delay_seconds) {
if (!ensure_parent_dir(path))
return false;
// Build the retry worker. The inner command sleeps, removes the pid file,
// then runs the retry action. We wrap everything in background_command() so
// that all inherited descriptors (in particular procd's flock fd 1000) are
// closed before the sleep starts — without that the background subshell
// would pin the lock file and prevent any subsequent service operation that
// also tries to acquire flock 1000.
let worker = command_from_args([ "sleep", delay_seconds ]) +
"; " + command_from_args([ "rm", "-f", path ]) +
"; exec " + command_from_args([ SERVICE_INIT, "retry_start_on_wan_up" ]);
let result = command_capture(command_from_args([ "sh", "-c", worker ]) + " >/dev/null 2>&1 & echo $!");
"; " + command_from_args([ SERVICE_INIT, "retry_start_on_wan_up" ]);
let bg = background_command(worker);
// background_command() already appends " &"; we need $! so run it inside a
// subshell that prints the pid back to us.
let result = command_capture("{ " + bg + " } ; echo $!");
let pid = trim(result.output);
if (result.status != 0 || !numeric_text(pid))
return false;
@@ -494,7 +503,11 @@ function retry_start_on_wan_up(owner_pid) {
return 0;
command_success_from_args([ "logger", "-t", SERVICE_NAME, "[info] Retrying failed Tachyon start after WAN came up" ]);
return command_status_from_args([ SERVICE_INIT, "restart", "triggered" ]);
// Do NOT call SERVICE_INIT here: rc.common holds flock 1000 for the whole
// duration of the init.d script, so a nested /etc/init.d/tachyon call would
// block forever waiting for that same lock (recursive flock self-deadlock).
// Call BIN_PATH directly, the same way start_service and stop_service do.
return command_status_from_args([ BIN_PATH, "restart" ]);
}
function active_service_action_value() {
@@ -347,7 +347,11 @@ function sync_enforcement(blocked_macs, blocked_ips) {
}
function crontab_lines() {
return split(command_output_from_args([ "crontab", "-l" ]), "\n");
// BusyBox's `crontab -l` exits with code 1 when the file is empty or
// absent, making command_output_from_args() return "" and causing a
// subsequent crontab write to discard every user cron line. Read the
// file directly instead; null (no file yet) is treated as empty string.
return split(as_string(fs.readfile("/etc/crontabs/root") || ""), "\n");
}
function cron_line() {