Skip to content

the upgrade tick opens install/version.txt without asking its kind — a FIFO there stops the daemon #2125

Closed

Description

@argszero

Where — the two readers of ~/.emrg/install/version.txt, both reached by the daemon's own tick:

site subject what a FIFO there stops
emrg/server/upgrade.py:186 (_installed_versions, called by prune_upgrade_backups ← UpgradeManager.tick, every 5 minutes) install/version.txt and install/previous-version.txt the tick's retention sweep stops, and with it the tick
emrg/server/upgrade.py:413 (UpgradeManager._read_local_version, the same tick) install/version.txt the tick stops before it can decide anything

The rule (issue #2062's family): whatever is handed a path asks its kind before opening it. An open is not a read — read_text on a FIFO waits for a writer and raises nothing, so the except OSError written around it can never fire and the call never returns. The rest of the family has closed one site at a time (#2062, #2073, #2076, #2078, #2083, #2094, #2096, #2098, #2100, #2102, #2104, #2106, #2108, #2112, #2117, #2118, #2120, #2122, #2124); these two are the sites #2122's body handed over, and the sweep had not reached them.

Measured 2026-10-12 (cyc20261012-091639) on master aff0d59a, one child per arm under a 10 s cap, the module constants redirected to a temp directory so the host's install dir was never touched:

subject FIFO directory regular absent
_installed_versions() DID NOT RETURN within 10 s () ('0.3.9',) ()
_read_local_version() DID NOT RETURN within 10 s '' '0.3.9' ''

Why it matters — tick() is called by the daemon on its own schedule, so the block is the whole daemon and the only recovery is a restart, which belongs to the host. This is also the chain the host's red line covers (MANIFESTO.md 第四条附则三): its failure mode is invisible from inside a green run, which is exactly what a FIFO at this path is — nothing raises, the tick simply never returns.

The two sites do not take the same answer, and the directory column is why. The family's rule is to ask what does the caller do with the answer?, and here the same shape feeds two different decisions:

  • _installed_versions() feeds keep_versions — which snapshots the retention must not delete. A non-regular version.txt answered as "no version named" would drop the running version's snapshot from the protected set, and that snapshot can be the one a failed upgrade is restored from. So it must refuse rather than degrade: raise the OSError naming the path and the kind. The caller already has the handler — tick() wraps prune_upgrade_backups() in try/except Exception and logs "backup retention failed", so refusing skips the sweep and loses nothing.
  • _read_local_version()'s docstring already promises "" on failure, and "" is what a missing file gives today (the tick then treats the target as not-yet-installed, which is the module's documented "version.txt unchanged → trigger again" semantics). So it keeps that answer and names the kind in a warning, rather than inventing a new outcome.

Fix — ask the kind first at both sites, with the shared emrg.tools.base.non_regular_kind (the path-taking reader the whitelist is stated once in, which emrg/config.py, emrg/memory.py, emrg/connect.py and emrg/sessions_index.py already reach). Absence must keep reaching the write-free path it reaches today: non_regular_kind stats, so its FileNotFoundError is the branch that preserves "nothing installed yet".

Activity

  1. argszero commented on Oct 12, 2026

    @argszero
    OwnerAuthor

    Handled by #2126

  2. pm25coder commented on Oct 12, 2026

    @pm25coder
    Collaborator

    Handled by #2127

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions