Skip to content

Commit 694344f

Browse files
authored
Merge pull request #1366 from ako/main
2 parents bb4f581 + 9d32ace commit 694344f

39 files changed

Lines changed: 632 additions & 118 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area":"cmd/mxcli","date":"2026-10-03","symptom":"`go test ./cmd/mxcli/` on any OS appended session_start/session_end lines to the developer's real ~/.mxcli/logs/mxcli-<date>.log, and could delete their older log files","cause":"in-process command tests (e.g. runCheckFiles in TestCheck_TestFileResolvesTheRunnersModule) reach newLoggedExecutorTo -> diaglog.Init, which writes to logDirectory() (MXCLI_LOG_DIR, else ~/.mxcli/logs) and runs cleanOldLogs on it. The MXCLI_LOG_DIR hook existed but only the two subprocess tests set it, and the HOME-only overrides in other tests do not move the log on Windows (os.UserHomeDir reads USERPROFILE)","file":"`cmd/mxcli/testmain_test.go` (TestMain); `mdl/diaglog/diaglog.go` (logDirectory, cleanOldLogs)","insight":"A package-level TestMain that points MXCLI_LOG_DIR at a temp dir (unless already set) is the one place that covers every present and future in-process test; per-test Setenv only protects the tests whose author remembered. cleanOldLogs makes the leak destructive, not just noisy. Prove it with a sentinel home: run go test with HOME, USERPROFILE, APPDATA and LOCALAPPDATA all pointed at a fresh temp dir and find it for .mxcli/logs before and after (before: one mxcli-<date>.log with one session in check mode, because diaglog is one session per process; after: nothing). Setting HOME alone proves nothing on Windows","refs":["mendixlabs/mxcli#1273","mendixlabs/mxcli#1256"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "mendixlabs/mxcli#1284: on Windows `mxcli test --attach` always says the app that published `.mxcli/test-endpoint.json` \"is no longer running\" although the pid is alive; the dev loop's `run-local.json` \"already serving\" detection never fires either", "cause": "pid liveness was `os.FindProcess(pid)` + `Signal(0)`; Go supports no signal but Kill on Windows and returns EWINDOWS for the rest, so every pid read dead", "file": "`internal/procalive/` (Alive, build-tagged unix/windows); callers `cmd/mxcli/testrunner/handshake.go`, `cmd/mxcli/devloop_handshake.go`, `cmd/mxcli/docker/procgroup_windows.go`", "insight": "The correct Windows check (OpenProcess(SYNCHRONIZE) + WaitForSingleObject(h, 0) == WAIT_TIMEOUT) already existed in docker/procgroup_windows.go since the waitReady incident, but two pid-based copies of the Signal(0) idiom were written separately and never moved over; Linux-only CI could not see it. Four tests were already red on a native Windows run (TestHandshakeRoundTrip, TestAttachUsesTheAdminPasswordNotTheEndpointToken, TestDevLoopHandshake_RoundTrip, TestWarnIfDevLoopServing_FiresForALiveDevLoop). Grep for `Signal(syscall.Signal(0))` / `Signal(0)` before trusting any new liveness check; use procalive.Alive for a bare pid.", "refs": ["mendixlabs/mxcli#1284", "#897"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "cmd/mxcli", "date": "2026-10-09", "symptom": "Windows: `mxcli run stop -p App.mpr` prints \"failed: 1 process(es) of pid N's run are still alive: [N]\" (exit 1) for a run that is shutting down or gone; a few seconds later the pid no longer exists and no ports are held", "cause": "docker.PidAlive (session_other.go, used by run stop / run status) treated \"os.FindProcess can open the pid\" as alive on Windows. Windows keeps an exited process object, and so an openable pid, for as long as anyone holds a handle to it (the parent that started it, typically), so a terminated run read as alive. It arrived after the #1284 handshake fix was written, so procalive did not cover it", "fix": "PidAlive delegates to internal/procalive.Alive: OpenProcess(SYNCHRONIZE) + WaitForSingleObject(h, 0) == WAIT_TIMEOUT on Windows, the same Signal(0) as before elsewhere off Linux; Linux keeps its /proc PidAlive", "insight": "\"Can I open it\" is not \"is it running\" on Windows: an exited process is openable until the last handle closes. The regression test holds its own SYNCHRONIZE handle, kills the child, waits for WAIT_OBJECT_0, then asks; the old PidAlive says alive. When porting a liveness fix onto a newer main, grep for every pid-liveness helper (processAlive, PidAlive, Signal(0)), not just the ones the original PR named. E2E on Windows 11 / Mendix 11.13.0: `run stop` went from \"failed: … still alive\" on every stop to \"stopped: pid N in 400ms\"", "file": "cmd/mxcli/docker/session_other.go (PidAlive)", "test": "cmd/mxcli/docker/session_windows_test.go (TestPidAlive_ExitedProcessWithAnOpenHandleIsNotAlive)", "refs": ["mendixlabs/mxcli#1284"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area":"mdl/executor","date":"2026-10-09","symptom":"`create or replace navigation` turning page item 'Work' into a sub-menu 'Work' reports success (plus `kept the stored action of menu item 'Work' (Forms$FormAction): MDL cannot express it`), then `mx check` fails with CE0548 \"Items with subitems cannot have an action themselves.\" (v0.25.0 regression; a different sub-menu caption avoids it)","cause":"`keepStoredMenuAction` had a sub-menu branch that kept ANY stored non-NoAction, on the reasoning that a sub-menu \"takes no OnClick, so a stored action on one is never stated\". Pairing is by caption only, so the stored page item's action was carried onto the new sub-menu. Navigation profiles and menu documents share the decision","file":"`mdl/executor/cmd_navigation.go` (`keepStoredMenuAction`)","insight":"**A keep-what-MDL-cannot-say rule must only keep what the target element can legally hold.** \"The script states no action\" is not \"the script wants the stored one\" when the element's kind changed under the same caption — a sub-menu can hold no action at all, so the carry is never right there. The tell in the output was the false reason: `Forms$FormAction` is printable MDL, yet the message said MDL cannot express it. Guards `TestNavigationRewrite_SubMenuDoesNotKeepAStoredAction` / `TestCreateMenu_SubMenuDoesNotKeepAStoredAction` (control: a leaf still keeps an unprintable action, #980). Measured on 11.14.0 with `mdl-examples/bug-tests/1341-navigation-submenu-keeps-page-action.mdl`: unfixed binary -> docker check CE0548; fixed -> 0 errors","refs":["mendixlabs/mxcli#1341","ako/mxcli#980"],"ce":["CE0548"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area":"internal/testutil","date":"2026-10-02","symptom":"On Windows, `go test` of cmd/mxcli, cmd/mxcli/docker, cmd/mxcli/marketplace, internal/auth and mdl/executor wrote into the developer's REAL `%USERPROFILE%\\.mxcli` (a fixture PAT in `auth.json`, fake `mxbuild/99.99.98`, `mxbuild/99.99.99`, `runtime/99.99.99`, `marketplace-catalog-default.json`), and `TestAuthList_EmptyStore`, `TestResolve_NoCredential`, `TestClientFor_NoCredential` failed because an earlier run had left a credential behind","cause":"Thirteen test files isolated the home directory with `t.Setenv(\"HOME\", dir)`. `os.UserHomeDir()` reads `USERPROFILE` on Windows and `HOME` everywhere else, so on Windows the override was ignored and the tests used the real profile; the leaked state persisted between runs, which made the failures order- and history-dependent","file":"`internal/testutil/home.go` (`SetHome` sets HOME and USERPROFILE); `internal/testutil/guard_test.go` (`TestNoBareHomeSetenv`)","insight":"It went unnoticed because CI runs the tests on Linux, where HOME is what UserHomeDir reads, and the Windows CI job is `-run`-scoped (#897) so it never executes these tests. Two files (`mdl/diaglog`, `cmd/mxcli/docker/download_test.go`) had already been fixed locally with a per-OS helper, but a copy-local fix teaches the next test author nothing: the repair is one shared helper plus a guard that fails on a bare `Setenv(\"HOME\"`. To prove a home-isolation fix, run the suite with USERPROFILE and HOME pointing at an empty sentinel directory and list `.mxcli` in it afterwards (before the fix: auth.json, mxbuild, runtime, catalog cache; after: only `logs/`, written by `TestCheck_TestFileResolvesTheRunnersModule`, which never sets a home at all). `diaglog.logDirectory` also honours `MXCLI_LOG_DIR`, the other lever for tests that execute cobra commands","refs":["mendixlabs/mxcli#1256","#897"]}

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2828

2929
### Fixed
3030

31+
- **A navigation sub-menu no longer keeps the action of the page item it replaces** (mendixlabs/mxcli#1341) — turning menu item `'Work'` into sub-menu `'Work'` with `create or replace navigation` (or `create or modify menu`) paired the two by caption and carried the page item's `Forms$FormAction` onto the sub-menu, reporting it as an action "MDL cannot express"; `mx check` then failed with CE0548 "Items with subitems cannot have an action themselves." A sub-menu is now always written with no action (measured on 11.14.0).
32+
- **`mxcli test --attach` and the dev loop's "already serving" detection see a live app on Windows** (mendixlabs/mxcli#1284) — both read a pid from a handshake file (`.mxcli/test-endpoint.json`, `.mxcli/run-local.json`) and tested it with `Signal(0)`, which Go supports only as Kill on Windows, so every pid read as dead: `test --attach` always refused with "the app that published … is no longer running" while the app was up, and a command that looks for a dev loop already serving the project (the recompile warning, for one) never found it. Liveness is now `internal/procalive.Alive`, which uses OpenProcess and WaitForSingleObject on Windows (the check `run --local` already used for mxbuild) and `Signal(0)` elsewhere. No change on Linux or macOS.
33+
- **`run stop` on Windows no longer reports a run that has shut down as still alive** — it printed "failed: 1 process(es) of pid N's run are still alive" and exited 1 for a run that had stopped, because its liveness check counted any pid Windows could still open as alive, and an exited process stays openable while its parent holds a handle to it. It now uses the same check as `test --attach` (above), and reports "stopped: pid N".
3134
- **`retrieve $u from $obj/System.owner` passed check and exec, then failed the build with CE0136** (mendixlabs/mxcli#1358) — `owner` and `changedBy` are entity flags, not modelled associations: mxbuild resolves the name in a retrieve by association but derives no entity from it, `[CE0136] "Retrieve object must specify the 'Entity' property."`, with or without `owner: AutoOwner` (measured on 11.12.2, `System.changedBy` alike). `check` and `exec` now refuse it as **MDL-RETRIEVE02** and name the form that builds: `retrieve $u from System.User where [id = $obj/System.owner] first;`.
3235
- **`run --local` and `test --local` work on Windows without Developer Mode or admin rights** (mendixlabs/mxcli#1286) — the first run for a new Mendix version failed with `linking runtime into mxbuild cache: symlink …: A required privilege is not held by the client.` mxcli links the downloaded runtime into the mxbuild cache, and an ordinary Windows user may create a symbolic link only with Developer Mode or an elevated token. When Windows refuses the symlink, mxcli now makes a directory junction instead, which needs no privilege (the same link `mklink /J` makes; it needs `cmd.exe`, which every Windows has). The other link site, the PAD files `docker build` links into the mxbuild cache, gets the same fallback. A cache that was already linked by hand with `New-Item -ItemType Junction` is left as it is.
36+
- **`run --local --watch` applies a domain model change on Windows** (mendixlabs/mxcli#1342) — page and microflow edits hot-reloaded, but adding an entity or an attribute failed every rebuild with "The process cannot access the file '…\deployment\web' because it is being used by another process", and the app then served 404 until `run --local` was restarted. A domain model change makes mxbuild recreate `deployment/web`, and the incremental web client bundler runs inside it, which Windows will not let anyone delete. On that failure the watch loop now stops the bundler, builds again, and starts a fresh bundler on the new `web/`; page edits keep the incremental path. Applies to Mendix versions whose deployment has `web/rollup.config.mjs` (reported on 11.12.4, reproduced on 11.13.0); 11.12.6 and 11.14+ bundle in mxbuild and were never affected.
3337
- **`run stop` and `run status` find a detached run's processes on every machine** — the guard against a reused session id rebuilt each process's start time from the kernel's boot time, which is whole seconds, and allowed only one second of slack. On a machine that booted late in a second, a run's own processes read as older than the run and were skipped, so `run stop` could report "was not running" for a live run. The slack is now two seconds. This was also the intermittent `TestRunStop_*` / `TestSessionMembers_*` failure on CI.
3438
- **The catalog names a call's and a delete's target** (mendixlabs/mxcli#1305) — `activities_for()` and `CATALOG.ACTIVITIES` returned every microflow, nanoflow, Java action and JavaScript action call with `action_ref=""`, and every delete with `entity_ref=""`, although `refs_from()` had both targets; a loop-scoped lint rule could not follow a call out of the loop. `action_ref` / `ActionRef` is now the called document, `entity_ref` / `EntityRef` a delete's entity (when the flow types the variable: a parameter, a create or retrieve output, a loop iterator), and the new `queue_ref` / `QueueRef` the task queue a microflow or Java action call runs in, so a rule can skip a call that runs asynchronously. A nanoflow's JavaScript action call now has a `refs_from()` `call` row (`target_type` `"JAVASCRIPT_ACTION"`), so `show callers` sees it. The catalog schema is bumped to 23; a cached catalog rebuilds.
3539
- **A DataGrid 2 column's `Visible:` expression is written** — `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)` and `Visible: [cond]` on a column passed `check` and `exec` and were stored as `true`, so the column was always visible; only the old quoted `Visible: '<expr>'` was kept. `alter page … set (Visible: <expression>) on grid column(…)` was refused as "column property VisibleIf not found", and an inserted column dropped `Visible: false` too. `describe` now prints the bare expression. A column's visibility is evaluated once for the grid, with no row object, so `$currentObject` there is CE0117 at build; `check` refuses it as **MDL-WIDGET43** (measured on 11.14.0). Use a page variable or parameter. Projects regenerate their widget definitions (generator version 18).

‎cmd/mxcli/cmd_auth_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616

1717
"github.com/mendixlabs/mxcli/internal/auth"
18+
"github.com/mendixlabs/mxcli/internal/testutil"
1819
"github.com/spf13/cobra"
1920
"github.com/spf13/pflag"
2021
)
@@ -24,7 +25,7 @@ import (
2425
func withTestHome(t *testing.T) string {
2526
t.Helper()
2627
home := t.TempDir()
27-
t.Setenv("HOME", home)
28+
testutil.SetHome(t, home)
2829
t.Setenv(auth.EnvPAT, "")
2930
t.Setenv(auth.EnvProfile, "")
3031
return home

‎cmd/mxcli/cmd_marketplace_install_file_test.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313

1414
"github.com/mendixlabs/mxcli/internal/marketplace"
15+
"github.com/mendixlabs/mxcli/internal/testutil"
1516
"github.com/spf13/cobra"
1617
)
1718

@@ -48,7 +49,7 @@ func buildLocalMPK(t *testing.T, name string, entries map[string]string) string
4849
// placeholder .mpr — the widget path only checks that the project exists.
4950
func runInstallFile(t *testing.T, args ...string) (string, error) {
5051
t.Helper()
51-
t.Setenv("HOME", t.TempDir())
52+
testutil.SetHome(t, t.TempDir())
5253

5354
origFactory := marketplaceClientFactory
5455
marketplaceClientFactory = func(_ context.Context, _ *cobra.Command) (*marketplace.Client, error) {
@@ -183,7 +184,7 @@ func TestInstallFile_NoContentIDAndNoFile(t *testing.T) {
183184
// and still reaches the client factory (which this helper makes fatal).
184185
func TestInstallFile_ContentIDStillUsesTheClient(t *testing.T) {
185186
mpr := placeholderProject(t)
186-
t.Setenv("HOME", t.TempDir())
187+
testutil.SetHome(t, t.TempDir())
187188
called := false
188189
origFactory := marketplaceClientFactory
189190
marketplaceClientFactory = func(_ context.Context, _ *cobra.Command) (*marketplace.Client, error) {

‎cmd/mxcli/cmd_marketplace_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313

1414
"github.com/mendixlabs/mxcli/internal/marketplace"
15+
"github.com/mendixlabs/mxcli/internal/testutil"
1516
"github.com/spf13/cobra"
1617
"github.com/spf13/pflag"
1718
)
@@ -37,7 +38,7 @@ func runMarketplace(t *testing.T, handler http.HandlerFunc, args ...string) (str
3738
t.Helper()
3839
// Isolate the catalog cache (~/.mxcli/...) into a temp HOME so tests never
3940
// read or write the real user cache.
40-
t.Setenv("HOME", t.TempDir())
41+
testutil.SetHome(t, t.TempDir())
4142
ts := httptest.NewServer(handler)
4243
t.Cleanup(ts.Close)
4344

‎cmd/mxcli/devloop_handshake.go‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
"fmt"
88
"os"
99
"path/filepath"
10-
"syscall"
1110
"time"
11+
12+
"github.com/mendixlabs/mxcli/internal/procalive"
1213
)
1314

1415
// devLoopHandshakeName is what `mxcli run --local` publishes so another mxcli
@@ -95,22 +96,9 @@ func readDevLoopHandshake(projectPath string) (devLoopHandshake, error) {
9596
if err := json.Unmarshal(body, &h); err != nil {
9697
return h, fmt.Errorf("%s is not valid JSON: %w", path, err)
9798
}
98-
if !processAlive(h.PID) {
99+
if !procalive.Alive(h.PID) {
99100
return h, fmt.Errorf("%s refers to process %d, which is no longer running\n"+
100101
" (the dev loop was stopped without cleaning up; start a new one)", path, h.PID)
101102
}
102103
return h, nil
103104
}
104-
105-
// processAlive reports whether a pid exists. Signal 0 is delivered to no one but
106-
// still performs the existence and permission checks.
107-
func processAlive(pid int) bool {
108-
if pid <= 0 {
109-
return false
110-
}
111-
p, err := os.FindProcess(pid)
112-
if err != nil {
113-
return false
114-
}
115-
return p.Signal(syscall.Signal(0)) == nil
116-
}

0 commit comments

Comments
 (0)