fix(updates): корректно завершать self-update при переиспользовании pid воркера (issue #31)
This commit is contained in:
@@ -614,12 +614,79 @@ export const TachyonShellMethods = {
|
||||
expectedLatestVersion?: string,
|
||||
) => {
|
||||
const jobStartedAt = Date.now();
|
||||
const isSelfUpdate =
|
||||
component === 'tachyon' &&
|
||||
(action === 'install' || action === 'reinstall');
|
||||
// Version before the update started: lets the fallback confirm an install
|
||||
// even when the expected target version is unknown.
|
||||
let baselineVersion = '';
|
||||
if (isSelfUpdate) {
|
||||
baselineVersion = await readTachyonVersion();
|
||||
}
|
||||
let selfUpdateVersionMatchedAt = 0;
|
||||
let lastStatusRefreshAt = 0;
|
||||
const transientRpc = createTransientRpcGraceTracker(
|
||||
COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS,
|
||||
);
|
||||
|
||||
const confirmSelfUpdateInstalled = async () => {
|
||||
if (!isSelfUpdate) return '';
|
||||
if (
|
||||
Date.now() - jobStartedAt <
|
||||
COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const installedVersion = await readTachyonVersion();
|
||||
if (!installedVersion) return '';
|
||||
const targetVersion = expectedLatestVersion || '';
|
||||
if (targetVersion && installedVersion === targetVersion) {
|
||||
return installedVersion;
|
||||
}
|
||||
if (
|
||||
!targetVersion &&
|
||||
baselineVersion &&
|
||||
installedVersion !== baselineVersion
|
||||
) {
|
||||
return installedVersion;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const settleSelfUpdate = async (installedVersion: string) => {
|
||||
if (!selfUpdateVersionMatchedAt) {
|
||||
selfUpdateVersionMatchedAt = Date.now();
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
Date.now() - selfUpdateVersionMatchedAt <
|
||||
COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const settled = await confirmSelfUpdateInstalled();
|
||||
if (!settled || settled !== installedVersion) {
|
||||
selfUpdateVersionMatchedAt = 0;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const selfUpdateResult = (installedVersion: string) =>
|
||||
({
|
||||
success: true,
|
||||
data: {
|
||||
success: true,
|
||||
component,
|
||||
action,
|
||||
message: translate('Tachyon has been installed'),
|
||||
current_version: installedVersion,
|
||||
latest_version: expectedLatestVersion,
|
||||
changed: true,
|
||||
status: 'latest',
|
||||
},
|
||||
}) as Tachyon.MethodSuccessResponse<Tachyon.ComponentActionResult>;
|
||||
|
||||
while (true) {
|
||||
await sleep(COMPONENT_ACTION_POLL_INTERVAL_MS);
|
||||
|
||||
@@ -628,6 +695,18 @@ export const TachyonShellMethods = {
|
||||
if (stateResponse) {
|
||||
if (!stateResponse.running) {
|
||||
transientRpc.reset();
|
||||
if (isSelfUpdate && stateResponse.success === false) {
|
||||
// The worker died before writing its result (service restart
|
||||
// during a self-install). The installed version is the truth:
|
||||
// treat a matched version as success instead of a stale error.
|
||||
const installedVersion = await confirmSelfUpdateInstalled();
|
||||
if (installedVersion) {
|
||||
if (await settleSelfUpdate(installedVersion)) {
|
||||
return selfUpdateResult(installedVersion);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: stateResponse,
|
||||
@@ -640,6 +719,16 @@ export const TachyonShellMethods = {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSelfUpdate) {
|
||||
// The backend may be stuck reporting the job as running (worker
|
||||
// pid recycled by the service restart). The installed version is
|
||||
// the ground truth: confirm it on every refresh tick.
|
||||
const installedVersion = await confirmSelfUpdateInstalled();
|
||||
if (installedVersion && (await settleSelfUpdate(installedVersion))) {
|
||||
return selfUpdateResult(installedVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastStatusRefreshAt = Date.now();
|
||||
@@ -656,6 +745,17 @@ export const TachyonShellMethods = {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSelfUpdate) {
|
||||
const installedVersion = await confirmSelfUpdateInstalled();
|
||||
if (installedVersion && (await settleSelfUpdate(installedVersion))) {
|
||||
return selfUpdateResult(installedVersion);
|
||||
}
|
||||
// A self-update replaces the package mid-flight: RPC failures are
|
||||
// expected during the swap window and must not fail the operation.
|
||||
// The version confirmation above is the only exit that matters.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await isComponentActionStillRunning(jobId, component, action)) {
|
||||
transientRpc.reset();
|
||||
continue;
|
||||
@@ -667,53 +767,30 @@ export const TachyonShellMethods = {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
component === 'tachyon' &&
|
||||
(action === 'install' || action === 'reinstall')
|
||||
) {
|
||||
const elapsed = Date.now() - jobStartedAt;
|
||||
|
||||
if (elapsed >= COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS) {
|
||||
const installedVersion = await readTachyonVersion();
|
||||
const targetVersion = expectedLatestVersion || installedVersion;
|
||||
|
||||
if (targetVersion && installedVersion === targetVersion) {
|
||||
if (!selfUpdateVersionMatchedAt) {
|
||||
selfUpdateVersionMatchedAt = Date.now();
|
||||
}
|
||||
|
||||
if (
|
||||
Date.now() - selfUpdateVersionMatchedAt >=
|
||||
COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
success: true,
|
||||
component,
|
||||
action,
|
||||
message: translate('Tachyon has been installed'),
|
||||
current_version: installedVersion,
|
||||
latest_version: expectedLatestVersion,
|
||||
changed: true,
|
||||
status: 'latest',
|
||||
},
|
||||
} as Tachyon.MethodSuccessResponse<Tachyon.ComponentActionResult>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return failure;
|
||||
}
|
||||
|
||||
transientRpc.reset();
|
||||
if (parsedResponse.running) {
|
||||
if (isSelfUpdate) {
|
||||
const installedVersion = await confirmSelfUpdateInstalled();
|
||||
if (installedVersion && (await settleSelfUpdate(installedVersion))) {
|
||||
return selfUpdateResult(installedVersion);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSelfUpdate && parsedResponse.success === false) {
|
||||
const installedVersion = await confirmSelfUpdateInstalled();
|
||||
if (installedVersion) {
|
||||
if (await settleSelfUpdate(installedVersion)) {
|
||||
return selfUpdateResult(installedVersion);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedResponse,
|
||||
|
||||
@@ -344,4 +344,214 @@ describe('TachyonShellMethods.componentAction', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('completes a Tachyon self-update when the backend is stuck reporting the job running (worker pid recycled)', async () => {
|
||||
mocks.fsRead.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
running: true,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message: 'Component action is running',
|
||||
job_id: 'job-1',
|
||||
}),
|
||||
);
|
||||
|
||||
mocks.executeShellCommand.mockImplementation(({ args }) => {
|
||||
if (args[0] === 'component_action_status') {
|
||||
return Promise.resolve({
|
||||
stdout: JSON.stringify({
|
||||
success: true,
|
||||
running: true,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message: 'Component action is running',
|
||||
job_id: 'job-1',
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (args[0] === 'show_version') {
|
||||
return Promise.resolve({
|
||||
stdout: '1.2.78\n',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: 'Unexpected command',
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
|
||||
const responsePromise = TachyonShellMethods.waitComponentActionJob(
|
||||
'job-1',
|
||||
'tachyon',
|
||||
'reinstall',
|
||||
'1.2.78',
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10000);
|
||||
|
||||
await expect(responsePromise).resolves.toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
success: true,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message: 'Tachyon has been installed',
|
||||
current_version: '1.2.78',
|
||||
latest_version: '1.2.78',
|
||||
changed: true,
|
||||
status: 'latest',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a stale job state as success for a Tachyon self-update when the installed version matches', async () => {
|
||||
let stateReads = 0;
|
||||
|
||||
const stateFactory = () =>
|
||||
stateReads++ < 4
|
||||
? {
|
||||
success: true,
|
||||
running: true,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message: 'Component action is running',
|
||||
job_id: 'job-1',
|
||||
}
|
||||
: {
|
||||
success: false,
|
||||
running: false,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message:
|
||||
'Component action job is stale or the worker process exited unexpectedly',
|
||||
job_id: 'job-1',
|
||||
};
|
||||
|
||||
mocks.fsRead.mockImplementation(() =>
|
||||
Promise.resolve(JSON.stringify(stateFactory())),
|
||||
);
|
||||
|
||||
mocks.executeShellCommand.mockImplementation(({ args }) => {
|
||||
if (args[0] === 'component_action_status') {
|
||||
return Promise.resolve({
|
||||
stdout: JSON.stringify(stateFactory()),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (args[0] === 'show_version') {
|
||||
return Promise.resolve({
|
||||
stdout: '1.2.78\n',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: 'Unexpected command',
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
|
||||
const responsePromise = TachyonShellMethods.waitComponentActionJob(
|
||||
'job-1',
|
||||
'tachyon',
|
||||
'reinstall',
|
||||
'1.2.78',
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10000);
|
||||
|
||||
await expect(responsePromise).resolves.toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
success: true,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message: 'Tachyon has been installed',
|
||||
current_version: '1.2.78',
|
||||
latest_version: '1.2.78',
|
||||
changed: true,
|
||||
status: 'latest',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps reporting the stale error when the installed version did not change', async () => {
|
||||
mocks.fsRead.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
running: false,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message:
|
||||
'Component action job is stale or the worker process exited unexpectedly',
|
||||
job_id: 'job-1',
|
||||
}),
|
||||
);
|
||||
|
||||
mocks.executeShellCommand.mockImplementation(({ args }) => {
|
||||
if (args[0] === 'component_action_status') {
|
||||
return Promise.resolve({
|
||||
stdout: JSON.stringify({
|
||||
success: false,
|
||||
running: false,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message:
|
||||
'Component action job is stale or the worker process exited unexpectedly',
|
||||
job_id: 'job-1',
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (args[0] === 'show_version') {
|
||||
return Promise.resolve({
|
||||
stdout: '1.2.77\n',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: 'Unexpected command',
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
|
||||
const responsePromise = TachyonShellMethods.waitComponentActionJob(
|
||||
'job-1',
|
||||
'tachyon',
|
||||
'reinstall',
|
||||
'1.2.78',
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
await expect(responsePromise).resolves.toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
success: false,
|
||||
running: false,
|
||||
component: 'tachyon',
|
||||
action: 'reinstall',
|
||||
message:
|
||||
'Component action job is stale or the worker process exited unexpectedly',
|
||||
job_id: 'job-1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1189,6 +1189,17 @@ function pid_running(pid) {
|
||||
return job_pid_valid(pid) && command_success_from_args([ "kill", "-0", pid ]);
|
||||
}
|
||||
|
||||
// A bare liveness check is not enough: after a tachyon self-reinstall the
|
||||
// service restart recycles the worker's pid quickly, and any new process would
|
||||
// keep the job "running" forever. The pid must still belong to our worker.
|
||||
function pid_is_component_worker(pid) {
|
||||
pid = as_string(pid);
|
||||
if (!job_pid_valid(pid))
|
||||
return false;
|
||||
let cmdline = fs.readfile("/proc/" + pid + "/cmdline");
|
||||
return index(as_string(cmdline), "component-action-worker") >= 0;
|
||||
}
|
||||
|
||||
function write_subscription_stale_job_state(path) {
|
||||
let value = object_or_empty(read_json_file(path));
|
||||
return write_state_file(path, subscription_stale_job_state_value(
|
||||
@@ -1628,14 +1639,14 @@ function refresh_component_running_job_state(path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid_running(pid) || within_grace)
|
||||
if ((pid_running(pid) && pid_is_component_worker(pid)) || within_grace)
|
||||
return;
|
||||
|
||||
command_success_from_args([ "sleep", "1" ]);
|
||||
value = read_json_file(path);
|
||||
if (type(value) != "object" || value.running !== true)
|
||||
return;
|
||||
if (pid_running(pid))
|
||||
if (pid_running(pid) && pid_is_component_worker(pid))
|
||||
return;
|
||||
|
||||
write_component_stale_job_state(path);
|
||||
|
||||
@@ -909,8 +909,16 @@ function component_action_running_for(component) {
|
||||
component = as_string(component);
|
||||
for (let path in fs.glob(COMPONENT_ACTION_DIR + "/*.json")) {
|
||||
let value = read_json_file(path);
|
||||
if (type(value) == "object" && value.running === true && as_string(value.component) == component)
|
||||
return true;
|
||||
if (type(value) != "object" || value.running !== true || as_string(value.component) != component)
|
||||
continue;
|
||||
// A recycled pid must not keep the job "running" forever: the process
|
||||
// still has to be our component-action worker.
|
||||
let pid = as_string(value.pid || "");
|
||||
if (!job_pid_valid(pid))
|
||||
continue;
|
||||
if (index(as_string(fs.readfile("/proc/" + pid + "/cmdline")), "component-action-worker") < 0)
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# A Tachyon self-update restarts the service mid-worker. If the worker dies
|
||||
# before writing its result, the job state stays "running" with a dead pid.
|
||||
# The stale detector used a bare `kill -0` liveness check: the recycled pid of
|
||||
# the next process would keep the job running forever, and LuCI's progress
|
||||
# modal hung for minutes over a finished install (issue #31). The pid must
|
||||
# still belong to the component-action worker (checked via /proc cmdline).
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
UPDATES_UC="$TACHYON_LIB/components/updates.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── the identity check exists and is used ────────────────────────────────────
|
||||
grep -q 'function pid_is_component_worker' "$UPDATES_UC" \
|
||||
|| fail "pid_is_component_worker() is gone; a recycled pid can keep a dead job running forever"
|
||||
grep -q 'pid_is_component_worker(pid)' "$UPDATES_UC" \
|
||||
|| fail "refresh_component_running_job_state no longer verifies the worker identity"
|
||||
|
||||
# ── behavior: extract the stale-detection block verbatim and drive it ────────
|
||||
STALE_UC="$WORK_DIR/stale.uc"
|
||||
cat > "$STALE_UC" <<'PRELUDE'
|
||||
let fs = require("fs");
|
||||
function now_seconds() { return int(time()); }
|
||||
function as_string(v) { return v == null ? "" : "" + v; }
|
||||
function arg_number(v) { return int(v); }
|
||||
function arg_bool(v) { return v == true || v == "1" || v == 1; }
|
||||
function command_success_from_args(args) {
|
||||
let parts = [];
|
||||
for (let a in args)
|
||||
push(parts, "'" + replace(as_string(a), /'/g, "'\\''") + "'");
|
||||
return system(join(" ", parts) + " >/dev/null 2>&1") == 0;
|
||||
}
|
||||
function read_json_file(path) {
|
||||
let raw = fs.readfile(path);
|
||||
if (raw == null) return null;
|
||||
let value = json(raw);
|
||||
return type(value) == "object" ? value : null;
|
||||
}
|
||||
let stale_calls = [];
|
||||
function write_component_stale_job_state(path) { push(stale_calls, path); }
|
||||
PRELUDE
|
||||
|
||||
sed -n '/^const COMPONENT_JOB_STALE_GRACE_SECONDS/,/^$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
sed -n '/^function job_started_at_within_grace/,/^}$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
sed -n '/^function job_pid_valid/,/^}$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
sed -n '/^function pid_running/,/^}$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
sed -n '/^function pid_is_component_worker/,/^}$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
sed -n '/^function refresh_component_running_job_state/,/^}$/p' "$UPDATES_UC" >> "$STALE_UC"
|
||||
|
||||
cat >> "$STALE_UC" <<'DRIVER'
|
||||
function write_state(path, pid, started_at) {
|
||||
let tmp = path + ".tmp";
|
||||
let payload = sprintf('{ "running": true, "pid": "%s", "started_at": %d }', pid, started_at);
|
||||
if (!fs.writefile(tmp, payload))
|
||||
return false;
|
||||
return fs.rename(tmp, path);
|
||||
}
|
||||
let state_path = ARGV[0];
|
||||
let pid = ARGV[1];
|
||||
let started_at = int(ARGV[2] || (time() - 60));
|
||||
write_state(state_path, pid, started_at);
|
||||
refresh_component_running_job_state(state_path);
|
||||
print((length(stale_calls) > 0 ? "stale" : "running") + "\n");
|
||||
DRIVER
|
||||
|
||||
# A live worker is not stale.
|
||||
ln -s /bin/sleep "$WORK_DIR/component-action-worker"
|
||||
"$WORK_DIR/component-action-worker" 30 &
|
||||
worker_pid=$!
|
||||
actual="$(ucode -L "$TACHYON_LIB" "$STALE_UC" "$WORK_DIR/job.json" "$worker_pid")"
|
||||
[ "$actual" = "running" ] || fail "a live component-action worker was marked stale (pid $worker_pid): '$actual'"
|
||||
kill "$worker_pid" 2>/dev/null || true
|
||||
|
||||
# The regression: a recycled pid — a live process that is NOT our worker —
|
||||
# must be marked stale, not kept "running" forever.
|
||||
sleep 30 &
|
||||
alien_pid=$!
|
||||
actual="$(ucode -L "$TACHYON_LIB" "$STALE_UC" "$WORK_DIR/job.json" "$alien_pid")"
|
||||
[ "$actual" = "stale" ] || fail "a dead job whose pid was recycled is never marked stale (alien pid $alien_pid): '$actual'"
|
||||
kill "$alien_pid" 2>/dev/null || true
|
||||
|
||||
# A dead pid is stale too.
|
||||
sleep 30 &
|
||||
dead_pid=$!
|
||||
kill "$dead_pid" 2>/dev/null || true
|
||||
wait "$dead_pid" 2>/dev/null || true
|
||||
actual="$(ucode -L "$TACHYON_LIB" "$STALE_UC" "$WORK_DIR/job.json" "$dead_pid")"
|
||||
[ "$actual" = "stale" ] || fail "a dead worker pid is not marked stale (pid $dead_pid): '$actual'"
|
||||
|
||||
# Grace still protects a fresh job: an alien pid inside the grace window is
|
||||
# left alone, so a worker that just started is not raced by the detector.
|
||||
sleep 30 &
|
||||
fresh_pid=$!
|
||||
fresh_start="$(($(date +%s) - 5))"
|
||||
actual="$(ucode -L "$TACHYON_LIB" "$STALE_UC" "$WORK_DIR/job.json" "$fresh_pid" "$fresh_start")"
|
||||
[ "$actual" = "running" ] || fail "a fresh job was marked stale inside the grace window (pid $fresh_pid): '$actual'"
|
||||
kill "$fresh_pid" 2>/dev/null || true
|
||||
|
||||
# ── ui.uc's running-for must not report a recycled pid as a running action ──
|
||||
UI_UC="$TACHYON_LIB/service/ui.uc"
|
||||
grep -q 'component-action-worker' "$UI_UC" \
|
||||
|| fail "ui.uc component_action_running_for no longer verifies the worker identity"
|
||||
|
||||
printf 'component job stale-pid checks passed\n'
|
||||
@@ -287,12 +287,16 @@ NODE
|
||||
|
||||
component_job="$TACHYON_UI_COMPONENT_ACTION_DIR/component-1.json"
|
||||
mkdir -p "$TACHYON_UI_COMPONENT_ACTION_DIR"
|
||||
printf '%s\n' '{"running":true,"component":"sing_box","started_at":100}' >"$component_job"
|
||||
ln -s /bin/sleep "$TACHYON_UI_COMPONENT_ACTION_DIR/component-action-worker"
|
||||
"$TACHYON_UI_COMPONENT_ACTION_DIR/component-action-worker" 30 &
|
||||
worker_pid=$!
|
||||
printf '%s\n' '{"running":true,"component":"sing_box","pid":"'"$worker_pid"'","started_at":100}' >"$component_job"
|
||||
ui_ucode component-action-running-for sing_box >/dev/null ||
|
||||
fail "component-action-running-for should detect running sing-box component action"
|
||||
if ui_ucode component-action-running-for zapret >/dev/null 2>&1; then
|
||||
fail "component-action-running-for should ignore other components"
|
||||
fi
|
||||
kill "$worker_pid" 2>/dev/null || true
|
||||
|
||||
ui_ucode action-ack service "$job_id" >/dev/null ||
|
||||
fail "action-ack should acknowledge finished service action"
|
||||
|
||||
Reference in New Issue
Block a user