fix(tray): tokenize NSSM AppParameters with CommandLineToArgvW semantics (BACKLOG #1565) - #1200
Conversation
…ics (BACKLOG #1565) Tray discovery split AppParameters with str.split(), which kept the quote characters and still split inside them. A quoted --service-config path became two dead fragments, the engine's settings then read as absent, and the tray fell back to its default for the served scheme. A quoted path with no space broke identically, so the defect is the quoting rather than the space; quoted --host and --port values failed the host regex and int() the same way. _split_command_line implements the Win32 argument rules in pure Python, and _iter_options calls it instead of str.split(). The = split is unchanged and now runs on a dequoted token, so the quoted equals form works for free. shlex cannot stand in for this: posix=False keeps the quotes and still splits inside them, and posix=True eats the backslashes, turning the unquoted C:\data\x.toml that works today into C:datax.toml. engine_serves_https is deliberately untouched. Its absent-settings-imply-https answer is BACKLOG #1126 and ADR 0172, not a second defect.
… case table A /simplify pass over the previous commit. No behavior change: the tokenizer still matches shell32.CommandLineToArgvW on 26 curated and 40,000 fuzz cases, re-measured after the restructure. - the backslash branch assigns the cursor once instead of in three arms - the doubled-quote branch falls through to the single in_quotes toggle rather than clearing the flag itself - the separator break no longer advances a cursor the outer loop re-skips - the oracle test runs over the unit test's own case table, so a case added to one is checked by both; _ORACLE_EXTRA holds only the lines where the real function is the expectation - a stale comment cited a fuzz count from a development run that is not in the tree; the load-bearing half moved into the docstring The docstring also records which parser the tray is mirroring, and why that is a real question. NSSM hands AppParameters to CreateProcess, so the engine's own sys.argv comes from the C runtime, whose rules differ on doubled quotes inside a run. Measured against a real child process on twelve command lines: every one carrying a path, host or port agreed, and the three that diverged were built only of quotes, which no path or host can be.
Lander reviewThe finding is better than the fix, and it is the one I would carry out of this PR. The brief said a hand-rolled tokenizer already matched The original measurement was not wrong about what it ran. It was wrong about what that licensed. A 14-case corpus is too small to contain the discriminating shape, and a clean result from an underpowered instrument is indistinguishable from a clean result from a sufficient one. That is the same family as a bounded query's false zero, one level up — and it is the third distinct instance of that family I have seen today, after a An executable oracle caught what review could not. The Builder's own hand-written expectation predicted Verified rather than read
Pinned against the real function, not a model of it. The cross-parser divergence is the right kind of recorded non-findingThe tray models Putting that in the docstring rather than a PR comment is correct — the next reader would otherwise re-derive it, and the PR body will not be in front of them. Your half-wrong bullet, handled the right way
Legs I will read after the factThe Windows leg carries the oracle entirely. Full suite not run, 29-36 minutes, and the PR body says so. Verdict: merge. |
|
CI failed while this pull request was in the merge queue, so the queue ejected it. Its own head can still be green: the queue revalidates the merge, and the path gates that skip on a pull request run there. Read the run before retrying. https://github.com/MEFORORG/MessageFoundry/actions/runs/35137321302 |
LANDER INSPECTION -- labelled self-review, not a peer reviewPosted under the korus The defect and the two rejected alternatives, verified by executionI ran all three claims rather than reading them: The defect is exactly as described. Both One imprecision in the docstring, which does not change the conclusion
Measured, The rejection still stands on the first half alone: retained quote characters break the path. Worth The part that shows real care
Identifying an ambiguity between two real parsers, naming the single point of divergence, and then The reason for not reaching into VerdictVerdict: merge. |
|
CI failed while this pull request was in the merge queue, so the queue ejected it. Its own head can still be green: the queue revalidates the merge, and the path gates that skip on a pull request run there. Read the run before retrying. https://github.com/MEFORORG/MessageFoundry/actions/runs/35167160562 |
Closes the tokenizer half of BACKLOG #1565. Two commits: the fix, then a
/simplifypass over it.What was wrong
messagefoundry/tray/config.pytokenized the NSSMAppParametersstring withstr.split(). That keeps the quote characters AND still splits inside them, so--service-config "C:\Program Files\MF\x.toml"became"C:\ProgramandFiles\MF\x.toml"— two paths that cannot exist. The engine's settings then read as absent, and the tray fell back to its default for the served scheme.Both of the row's corrections hold against the tree, and both are now pinned:
--hostand--portvalues failed_VALID_HOSTandint(), so the discovered URL was wrong by more than its schemeThe fix
One new private helper,
_split_command_line, and one line changed in_iter_options._iter_optionsis defined once and called twice, both inside this file, so that is the whole radius. The=split is unchanged and now runs on an already-dequoted token, which makes the quoted equals form work for free.Why
shlexwas rejected, in both modesThe row offered
shlex(posix=False)orCommandLineToArgvWsemantics as alternatives. They are not alternatives — neithershlexmode works:--service-config="C:\Program Files\MF\x.toml"C:\data\x.tomlposix=Falseposix=TrueC:datax.tomlposix=Trueeats the backslashes, which breaks the unquoted Windows path that works today. So the Win32 rules are hand-rolled in pure Python. A ctypes call intoshell32was also rejected for the shipped path: the tray is stdlib-only and this module's pure core must stay unit-testable on any OS, so a ctypes tokenizer would red the Linux leg.Correctness is measured, not argued
The tokenizer is pinned against the real
shell32.CommandLineToArgvWthrough ctypes, in a test guarded byskipif(sys.platform != "win32"). ctypes appears only there, never in the shipped path./simplifyrestructure.""inside a quoted run staying quoted (the MSVCRT rule).CommandLineToArgvWends the run there. 156 of 40,000 fuzz cases diverged on that one point and nothing else."a""b" cto give['a"b', 'c']. The real function gives['a"b c'], because the later quote reopens a run. The code was right and my reading was wrong.The oracle test now runs over the unit test's own case table, so a case added to one is checked by both.
engine_serves_httpsis deliberately untouchedThe row's heading reads like two defects. It is one, and the row body says so: "The scheme flip is the consequence, not the bug." Misparsing loses the config file; no file means no settings; no settings imply https.
That last step is correct and must stay. Since ADR 0172 the engine always mints, so absent or unreadable settings must answer True — the old cert-path predicate is BACKLOG #1126, and it rendered a running engine as WEDGED on the commonest posture. Three rows of
test_engine_serves_httpspin it ({"api": "not-a-table"},{},None). Changing it would red those three and reopen a closed defect.The row's two acceptance bullets for an operator certificate and a declared upstream terminator were already met by that same test, including the ordering case where a cert set alongside a declared proxy still returns True. Not rebuilt.
Which parser this mirrors, and why that is a real question
NSSM hands
AppParameterstoCreateProcess, so the engine's ownsys.argvcomes from the C runtime, not fromshell32. The MSVCRT rules differ fromCommandLineToArgvWon exactly one point: a doubled quote inside a run stays quoted there and ends the run here.I measured it against a real child process on twelve command lines. Every line carrying a path, a host or a port agreed. The only three that diverged were built purely of quotes (
"a""b" c,a """ b,"""""), and none of those can reach anything this file acts on: a quote is not a legal Windows filename character,_VALID_HOSTrejects it, and--portgoes throughint().CommandLineToArgvWstays the model, and the docstring now records the divergence so the next reader does not re-derive it.What this does NOT cover
Named deliberately, so none of it reads as an oversight:
'C:\x'is not a Windows quoting form; the realCommandLineToArgvWkeeps the apostrophes, so the path genuinely is named with them. Pinned as a test row because it looks like a gap and is not one.--configand--dbquoted values remain unparsed. Harmless: neither flag is inwanted, so their fragments were skipped before and their dequoted values are skipped now."C:\MF\\"resolves to a directory._read_tomlthen fails andengine_serves_https(None)answers True. Fail-soft, unchanged, intended.scripts/service/install-service.ps1:426writes no--service-configat all. Its quoted--config/--dbvalues did fragment under the old split, but harmlessly, and--host/--portsat outside the quotes. I ran the shipped line through both tokenizers with a spaced install path:('127.0.0.1', 8765)and no--service-configeither way. So a stock install parses identically before and after this change, and the defect bites a hand-editedAppParameters.Severity
No live exposure — zero deployments (CLAUDE.md section 0). Conditionally: a deploying site that had declared
tls_terminated_upstreamand hand-edited a quoted--service-configpath would have had its tray probe https against an engine deliberately speaking plaintext to its proxy, and render a running engine as WEDGED.test_load_config_finds_a_quoted_service_config_path_with_a_spaceis that case end to end.Checks
Run in this worktree's
.venv, foreground:ruff check messagefoundry tests— passedruff format --check— passedmypy messagefoundry(strict) — passed, 275 source filespytest tests/test_tray_config.py— 86 passedtest_cert_cli.pyandtest_service_control.py— 281 passed, 1 skipped-k oracle -v, 0.02s call), since a skip reads as a passLocal runs report
INCOMPLETE RUN -- coverage was NOT collectedbecause thevaultextra is absent from this interpreter. Expected, and named here rather than hidden.Not run: the full suite. The Windows leg is a 29-36 minute job and would not finish inside one Builder turn. The change is confined to one private helper whose only consumers are the two functions above it in the same file (verified by grep across
messagefoundry/,tests/,harness/,ide/,scripts/).Legs to read after my process exits: the Windows leg, because
test_split_command_line_matches_the_win32_oracleruns nowhere else — the Ubuntu leg skips it and pays nothing.Proposed ledger banner text for #1565
Not done
docs/BACKLOG.mdis untouched — it is the 23-line public stub, and the ledger is vault-only.