v0.77.0 — a running bridge stops reporting as dead
A running bridge stops reporting as dead
moshcode dns status on a desktop whose Moshpit bridge had been up the whole time:
bridge NOT running — stale pidfile for 641911
routing configured (/etc/systemd/resolved.conf.d/moshpit.conf)
! routing is in place but nothing answers on 127.0.0.1:5354 — Moshpit names will fail.
fix with: moshcode dns enable
The pidfile was not stale. The process was there, and kill -9 on it said so — operation not permitted, which is what the kernel returns for a process you may not signal. A process that is actually gone returns no such process instead. Those are different answers to different questions, and isAlive was throwing away the difference:
try { process.kill(pid, 0); return true; }
catch { return false; }process.kill(pid, 0) fails two ways. ESRCH means the pid is gone. EPERM means it is right there and belongs to somebody else. Catching both as "dead" is wrong in exactly the case this tool manufactures for itself: dns enable escalates, so the bridge it starts is root's, while every status asking after it later is not. From that moment the bridge was invisible to the tool that started it, for the rest of its life.
Three callers then drew progressively worse conclusions from the same bad answer. status printed the stale-pidfile line above and advised dns enable. stopDaemon deleted the pidfile and reported that it had cleared a stale entry, while the daemon it named kept running — now orphaned, with nothing on disk pointing at it. And startDaemon saw nothing running and started a second bridge on 127.0.0.1, underneath the working one on 0.0.0.0.
That last one is an outage, not an untidiness. The kernel delivers to the more specific socket, so the new bridge shadows the old one and every lookup on a machine with catch-all routing goes to whichever of the two is less able to answer. bridgePresence already carries a long comment about this exact failure. It was reachable by following our own advice.
So only ESRCH is dead now. EPERM is alive and someone else's, and callers are told that rather than having it smoothed over: stop returns Operation not permitted instead of silently orphaning a daemon, and start short-circuits on alreadyRunning instead of shadowing one.
Worth being clear about what this does not fix. The port probe was never part of the bug — bridgePresence asks the socket directly, precisely so that a bridge moshcode did not start still counts, and on the machine above it was right that nothing was answering on 5354. The bridge had died earlier for its own reasons. What the liveness check contributed was a confident wrong story about why, and a suggested fix that would have made it worse. Both halves of status now have to agree before it accuses anything of being stale.
The bridge still is not a systemd unit and still does not survive a reboot, which is the thing desktops actually trip over. docs/hosting-a-moshpit-name.md covers the unit to install; shipping one is separate work.
isAlive also takes an injectable kill now, so both branches are testable without privileges — the same way every other system call in this file is reached. The suite gained four cases, three of which fail without the fix.
A flat fee is billed once, not every hour
A rate with a price and no stated period — $5000, 250 USDC — is a flat project fee. parseRate knew that. Its own comment warned that defaulting such a rate to per-hour "would silently multiply the invoice by every hour tracked", and then the guard underneath set rate.per = "hour".
Since hour was already the default, the guard was a no-op against the bug it was written to prevent. parseRate("$5000") returned per: "hour", and chargeFor duly scaled it by tracked time: a $5,000 project fee came out of a ten-hour job as $50,000.
Bare fees now parse as per: "project" and bill once. An explicitly stated period is left exactly as it was. (#455)