diff --git a/.claude/skills/mendix/run-local/SKILL.md b/.claude/skills/mendix/run-local/SKILL.md index d33a46caf..626ca0859 100644 --- a/.claude/skills/mendix/run-local/SKILL.md +++ b/.claude/skills/mendix/run-local/SKILL.md @@ -304,7 +304,7 @@ run's process session), and `run status` reports them. Do **not** | `--web-client-timeout` | `$MXCLI_WEB_CLIENT_TIMEOUT`, else `5m` | Limit for one web client bundle build; on timeout the tail of `deployment/log/web-client-build.log` is printed | | `--ensure-db` | off | Provision local Postgres + app database if missing | | `--app-port` / `--admin-port` / `--serve-port` | 8080 / 8090 / 6543 | Ports | -| `--listen-addr` | `127.0.0.1` | IP the app binds to. `0.0.0.0` (or `::`) makes it reachable from other machines; the network URLs are printed once it runs. Pair it with `--runtime-setting ApplicationRootUrl=http://:/` (or a real host in the configuration) or generated absolute URLs name the wildcard — a warning says so. The M2EE admin API always stays on loopback | +| `--listen-addr` | `127.0.0.1` | IP the app binds to. `0.0.0.0` (or `::`) makes it reachable from other machines; the network URLs are printed once it runs. If the app builds absolute URLs (SSO redirects, email links), pair it with `--runtime-setting ApplicationRootUrl=http://:/` (or a real host in the configuration): the runtime does not derive one from the bind address — a warning says so. The M2EE admin API always stays on loopback | | `--db-host` / `--db-name` / `--db-user` / `--db-password` | 127.0.0.1:5432 / derived / mendix / mendix | Database; bracket IPv6 endpoints (`[::1]:5432`) | | `--db-type` | `postgresql` | `hsqldb` runs on the runtime's built-in file database: no database server, no `--ensure-db`, works offline. Data lives in `/deployment/data/database/hsqldb/`. Refused together with `--db-host`/`--db-user`/`--db-password`/`--ensure-db`. Local development only | | `--setup` | off | Cache MxBuild+runtime + ensure DB, then exit (SessionStart bring-up) | diff --git a/CHANGELOG.md b/CHANGELOG.md index f592fc1a4..b8d55ec81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added -- **`mxcli run --local --listen-addr` — reach a local app from another machine** (mendixlabs/mxcli#1248) — the app always bound `127.0.0.1`, so a phone or a second laptop on the LAN could not open it. `--listen-addr 0.0.0.0` (or `::`) binds every interface and prints the URLs the app is reachable at; the default stays loopback. The M2EE admin API is unaffected and stays on `127.0.0.1` whatever the flag says — before, the one option set both binds, so widening the app would have put the admin API on the network too. mxcli itself still dials the app on loopback under a wildcard (health check, screenshots, "App is running at"), and brackets an IPv6 address. The value must be an IP address and is refused before the build otherwise. With no application root URL the runtime derives one from the bind address, so a wildcard bind warns at boot and names the `--runtime-setting ApplicationRootUrl=…` to pass. +- **`mxcli run --local --listen-addr` — reach a local app from another machine** (mendixlabs/mxcli#1248) — the app always bound `127.0.0.1`, so a phone or a second laptop on the LAN could not open it. `--listen-addr 0.0.0.0` (or `::`) binds every interface and prints the URLs the app is reachable at; the default stays loopback. The M2EE admin API is unaffected and stays on `127.0.0.1` whatever the flag says — before, the one option set both binds, so widening the app would have put the admin API on the network too. mxcli itself still dials the app on loopback under a wildcard (health check, screenshots, "App is running at"), and brackets an IPv6 address. The value must be an IP address and is refused before the build otherwise. The runtime does not derive an application root URL from the bind address (measured on 11.12.1: unset stays `null` under `0.0.0.0`), so a wildcard bind with none warns at boot and names the `--runtime-setting ApplicationRootUrl=…` to pass. - **`mxcli search --fail-on-match` turns the search into a gate** — a pipeline that must refuse a change when a forbidden word turns up in the project's text had no exit status to read: `search` exited 0 whether it found something or not. With the flag it exits 1 when there is at least one hit and 0 when there is none, printing the results as before (a JSON array under `--json`). A search that cannot run, such as an unbalanced `"` or an unknown project, exits 2 so that 1 always means a match; the `SEARCH` statement still answers an unrunnable query with "No matches found.". Without the flag a hit exits 0 as before. Rules: docs-site "Full-Text Search". - **`filemanager` — the classic file manager (`Forms$FileManager`) in MDL** (mendixlabs/mxcli#151) — `filemanager fileManager1 (Label: 'File', Type: Upload, MaxFileSize: 10, AllowedExtensions: '.xlsx', ShowFileInBrowser: false)` inside a data view on a `System.FileDocument` specialization. Before, there was no way to author one: `pluggablewidget 'Forms$FileManager'` had no definition, the bare keyword was refused as MDL-WIDGET25, and `describe page` printed a stored one as a NOT re-executable comment, so describe → exec dropped it. Omitted properties take Studio Pro's defaults (`Both`, 5 MB, any file, false); the written BSON has the property set of a Studio Pro-authored file manager. `alter page … set` changes all four properties. A file manager in no data container is MDL-WIDGET45 at `check`, and one in a data view on an entity that is not a FileDocument is refused by `exec` — both were mxbuild's CE6810 after a clean `exec`. diff --git a/cmd/mxcli/cmd_run.go b/cmd/mxcli/cmd_run.go index d56e30411..481e13fd0 100644 --- a/cmd/mxcli/cmd_run.go +++ b/cmd/mxcli/cmd_run.go @@ -388,7 +388,7 @@ func init() { runCmd.Flags().Bool("setup", false, "Prepare prerequisites (cache MxBuild+runtime, ensure DB) and exit without booting — for a SessionStart hook") runCmd.Flags().Int("app-port", 0, "HTTP port for the app (default 8080)") runCmd.Flags().Int("admin-port", 0, "M2EE admin API port (default 8090)") - runCmd.Flags().String("listen-addr", "", "Address the app binds to (default 127.0.0.1, loopback only). Must be an IP address. Set 0.0.0.0 (or :: for IPv6 too) to reach the app from other machines on the network; the URLs it is reachable at are printed once it runs. Pair it with a reachable root URL — an ApplicationRootUrl naming a real host in the model, or --runtime-setting ApplicationRootUrl=http://:/ — because with none the runtime derives its root URL from this address, which no browser can open (a warning says so). The M2EE admin API is unaffected and always stays on 127.0.0.1.") + runCmd.Flags().String("listen-addr", "", "Address the app binds to (default 127.0.0.1, loopback only). Must be an IP address. Set 0.0.0.0 (or :: for IPv6 too) to reach the app from other machines on the network; the URLs it is reachable at are printed once it runs. If the app builds absolute URLs (SSO redirects, links in emails), pair it with a reachable root URL — an ApplicationRootUrl naming a real host in the model, or --runtime-setting ApplicationRootUrl=http://:/ — because the runtime does not derive one from this address (a warning says so). The M2EE admin API is unaffected and always stays on 127.0.0.1.") runCmd.Flags().Int("serve-port", 0, "mxbuild --serve port (default 6543)") runCmd.Flags().String("mxbuild-path", "", "Path to the mxbuild to build with, overriding resolution (Studio Pro's bundled mxbuild on macOS/Windows, the cached CDN download on Linux)") runCmd.Flags().String("db-type", "", "Database type for a local run: postgresql (default) or hsqldb (the runtime's built-in file database — no server, no --db-host/--db-user/--db-password, data under /deployment/data/database/hsqldb/)") diff --git a/cmd/mxcli/docker/localboot.go b/cmd/mxcli/docker/localboot.go index c4874048a..3abf310e1 100644 --- a/cmd/mxcli/docker/localboot.go +++ b/cmd/mxcli/docker/localboot.go @@ -387,7 +387,8 @@ func runtimeConfigParams(o LocalRuntimeOptions, constants map[string]string) map "MicroflowConstants": constants, } // Only set ApplicationRootUrl when the app is served behind an external URL; - // on a plain local run the runtime defaults it from the listen address. + // on a plain local run it stays unset (the runtime does not derive one from + // the listen address; see warnWildcardWithoutRootURL). if o.ApplicationRootUrl != "" { params["ApplicationRootUrl"] = o.ApplicationRootUrl } diff --git a/cmd/mxcli/docker/runlocal.go b/cmd/mxcli/docker/runlocal.go index a9eb0207e..22f315f30 100644 --- a/cmd/mxcli/docker/runlocal.go +++ b/cmd/mxcli/docker/runlocal.go @@ -65,10 +65,9 @@ type LocalRunOptions struct { // ListenAddr is the address the app binds to. Empty means 127.0.0.1, which // is loopback-only on purpose. Set it to 0.0.0.0 to let the app be reached // from other machines on the network; pair that with a reachable - // ApplicationRootUrl (--runtime-setting), because the runtime otherwise - // derives its root URL from this address and 0.0.0.0 is not a host a - // browser can ask for. The M2EE admin API is unaffected and stays on - // loopback. + // ApplicationRootUrl (--runtime-setting) if the app builds absolute URLs, + // because the runtime does not derive one (see warnWildcardWithoutRootURL). + // The M2EE admin API is unaffected and stays on loopback. ListenAddr string // AdminPass is the M2EE admin password (default is a fixed local-dev value). AdminPass string @@ -352,12 +351,14 @@ func applicationRootURLFrom(settings *model.ProjectSettings) (rootURL, configNam } // warnWildcardWithoutRootURL warns when the app binds every interface but -// nothing gives the runtime the URL it is reached at. With no -// ApplicationRootUrl the runtime derives one from the listen address, so the -// absolute URLs it generates (OIDC/SAML redirect URIs, deep links, links in -// emails) name 0.0.0.0 or ::, which no browser can open — while the app itself -// answers fine, since the client uses relative URLs. A warning rather than a -// refusal: plenty of apps generate no absolute URL at all. +// nothing gives the runtime the URL it is reached at. The runtime does NOT +// derive one from the bind address: measured on 11.12.1, +// Core.getConfiguration().getApplicationRootUrl() is null after a boot with no +// ApplicationRootUrl, under 127.0.0.1 and 0.0.0.0 alike, and is the passed value +// under --runtime-setting. So anything that builds an absolute URL from it +// (OIDC/SAML redirect URIs, links in emails) has no host another machine can +// use, while the app itself answers fine — the client uses relative URLs. A +// warning rather than a refusal: plenty of apps build no absolute URL at all. func warnWildcardWithoutRootURL(stderr io.Writer, listen string, port int, appRootURL string, settings map[string]any) { if !isWildcardListenAddr(listen) || appRootURL != "" { return @@ -365,8 +366,8 @@ func warnWildcardWithoutRootURL(stderr io.Writer, listen string, port int, appRo if _, ok := settings["ApplicationRootUrl"]; ok { return } - fmt.Fprintf(stderr, "Warning: --listen-addr %s with no application root URL — the runtime derives one from the bind address, "+ - "so absolute URLs it generates (SSO redirects, deep links) will not open from another machine. "+ + fmt.Fprintf(stderr, "Warning: --listen-addr %s with no application root URL — the runtime does not derive one, "+ + "so absolute URLs the app builds from it (SSO redirects, links in emails) have no host another machine can open. "+ "Pass --runtime-setting ApplicationRootUrl=http://:%d/ or set one naming a real host in the configuration.\n", listen, port) } @@ -378,8 +379,7 @@ func warnWildcardWithoutRootURL(stderr io.Writer, listen string, port int, appRo // mean "chosen". Passing that back would be a behaviour change for every // existing project — and an actively wrong one under --app-port, where the // stock value names a port the app is not serving on. A loopback host also adds -// nothing: with no ApplicationRootUrl the runtime derives one from the listen -// address, which is the same thing. So only a real host name — the case this +// nothing another machine could use. So only a real host name — the case this // exists for, giving each app in a solution its own name — is honoured. func customHostRootURL(rootURL string) bool { if rootURL == "" { diff --git a/docs-site/src/tools/run-local.md b/docs-site/src/tools/run-local.md index ef07f05fa..2f6d6a71b 100644 --- a/docs-site/src/tools/run-local.md +++ b/docs-site/src/tools/run-local.md @@ -131,7 +131,7 @@ would make Studio Pro 11.13 fail to open it — a branch with no upstream, or | `--app-port` | 8080 | App HTTP port | | `--admin-port` | 8090 | M2EE admin API port | | `--serve-port` | 6543 | `mxbuild --serve` port | -| `--listen-addr` | `127.0.0.1` | IP address the app binds to (not a host name). `0.0.0.0` binds all IPv4 interfaces, `::` all interfaces, so other machines on the network can open the app; the URLs it is reachable at are printed after boot. Without an application root URL the runtime derives one from this address, so absolute URLs it generates (SSO redirects, deep links) name the wildcard: pass `--runtime-setting ApplicationRootUrl=http://:/` or set a real host in the configuration (a warning says so). The M2EE admin API is unaffected and always binds `127.0.0.1` | +| `--listen-addr` | `127.0.0.1` | IP address the app binds to (not a host name). `0.0.0.0` binds all IPv4 interfaces, `::` all interfaces, so other machines on the network can open the app; the URLs it is reachable at are printed after boot. The runtime does not derive an application root URL from this address, so if the app builds absolute URLs (SSO redirects, links in emails) pass `--runtime-setting ApplicationRootUrl=http://:/` or set a real host in the configuration (a warning says so). The M2EE admin API is unaffected and always binds `127.0.0.1` | | `--mxbuild-path` | resolved for this host | The mxbuild to build with, overriding resolution (Studio Pro on macOS/Windows, the cached CDN download on Linux). `MXCLI_MXBUILD_PATH` sets the same override from the environment; the flag wins | | `--db-host` | 127.0.0.1:5432 | Database `host:port`; bracket IPv6 endpoints (`[::1]:5432`) | | `--db-name` | derived from project | Database name |