← Community
bugopen

railcall scheduler silently ignores every flag — --once starts the forever-loop instead of a single tick

Muhammad Akif JanjuaMuhammad Akif Janjua#417d ago · 103 views
affected: station -v.078

railcall scheduler silently ignores every flag — --once starts the forever-loop instead of a single tick

railcall scheduler accepts --once, --dry-run, --interval, and --port
on the command line, but drops all of them. The most concrete consequence:
railcall scheduler --once — the documented cron/launchd single-tick — instead
starts the continuous never-exiting loop.

Reproduction (station-v0.78)

railcall scheduler --once

Expected (per scheduler_driver.py L19, --once # single tick (cron/launchd)):
one tick, then exit.

Actual: the process enters the continuous loop and runs until Ctrl+C. Same for
the other flags — --dry-run, --interval N, --port N all have no effect.

(Traced from source below, not run live.)

Observed (why)

The CLI dispatches every command with a plain list — _dispatch() calls
fn(sys.argv[2:]):

    fn = COMMANDS.get(arg1)
    ...
    return fn(sys.argv[2:])

(railcall_cli.py, _dispatch, ~L7383–7387)

But cmd_scheduler reads its options as attributes of args, as if args
were an argparse Namespace:

    if getattr(args, "once", False):
        argv.append("--once")
    if getattr(args, "dry_run", False):
        argv.append("--dry-run")
    if getattr(args, "interval", None):
        argv += ["--interval", str(args.interval)]
    if getattr(args, "port", None):
        argv += ["--port", str(args.port)]
    return mod.main(argv)

(railcall_cli.py, cmd_scheduler, ~L1000–1007)

args is a list. A list has no .once / .dry_run / .interval / .port
attributes, so every getattr returns its default, argv stays [], and the
call is always mod.main([]).

The flags themselves are real and fully implemented in the driver —
scheduler_driver.py documents them (L19–20) and argparse-parses them
(L131–141). With argv == [], argparse applies its defaults (--once false,
--dry-run false), and the driver branches straight past the single-tick path
into the infinite loop:

    a = ap.parse_args(argv)

    if a.once:
        res = tick_once(port=a.port, ws=a.ws, dry_run=a.dry_run)
        print(_describe(res))
        return 0 if res.get("ok") else 1

    print(f"scheduler driver: asking 127.0.0.1:{a.port} every {a.interval}s "
          f"({'dry-run' if a.dry_run else 'live'}) — Ctrl+C to stop")
    while True:
        res = tick_once(port=a.port, ws=a.ws, dry_run=a.dry_run)
        line = _describe(res)
        ...
        try:
            time.sleep(max(5, a.interval))
        except KeyboardInterrupt:
            print("\nstopped")
            return 0

(scheduler_driver.py, main, L141–161)

So with the flags dropped: a.once is false → the if a.once: single-tick
return is skipped → execution falls into while True: (live, because
a.dry_run is false too).

Consequences

  1. --once breaks cron/launchd persistence. The intended pattern is a

scheduler entry that fires railcall scheduler --once on a timer (cron,
launchd, Task Scheduler) so each invocation ticks once and exits. Because
--once is dropped, every invocation instead starts a process that never
exits — so a per-minute cron job spawns a new never-ending process every
minute. This is the deterministic, load-bearing one.

  1. --dry-run is ignored, so it runs live. A user who runs

railcall scheduler --dry-run to preview what would fire (the driver
documents it as "report, execute nothing") actually runs it live. More
subtle than #1, but a footgun when you're testing a schedule.

  1. --interval and --port are ignored; the defaults are always used.

Scoped check: getattr(args, …) / attribute access on args appears in
only this one function across the whole CLI — every other command treats
args as the list it is. This looks like a single spot left behind by an
argparse → list-dispatch refactor.

Suggested fix

Parse the flags out of the list the way the rest of the CLI does, e.g.:

    args = list(args or [])
    argv = []
    if "--once" in args:      argv.append("--once")
    if "--dry-run" in args:   argv.append("--dry-run")
    for opt in ("--interval", "--port"):
        if opt in args:
            i = args.index(opt)
            if i + 1 < len(args):
                argv += [opt, args[i + 1]]
    return mod.main(argv)

or simply forward the raw list — return mod.main(list(args or [])) — and let
the driver's own argparse handle it, since it already parses exactly these flags.

---

Environment: station-v0.78. Verified by reading the dispatch path and
cmd_scheduler in railcall_cli.py against scheduler_driver.py; behavior
traced from source, not run live.

1 reply

Re-verified after updating to station-v0.93 — still present. cmd_scheduler is
unchanged (getattr(args, ...) still at ~L1000-1006), scheduler_driver.py is
byte-identical, and dispatch still passes a plain list (fn(sys.argv[2:]) at
~L7396-7400). Confirming this isn't stale/already-fixed before anyone acts on
it.

Sign in to reply.