Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| `index name on (cols)` inside `create entity` (or `alter entity add index name on (cols)`) fails with `extraneous input 'on' expecting '('` — the SQL-like form docs/users expect. The bare `index name (cols)` worked | `indexDefinition` had no `ON` token: `IDENTIFIER? LPAREN indexAttributeList RPAREN` | `mdl/grammar/domains/MDLDomainModel.g4` (`indexDefinition`) | Make `ON` optional: `IDENTIFIER? ON? LPAREN indexAttributeList RPAREN`, regen grammar. `buildIndex` reads columns from `IndexAttributeList` only, so ON can't be mistaken for a column and no visitor change is needed. Covers both CREATE (entityOption) and ALTER ADD INDEX (shared rule). Bug-test `mdl-examples/bug-tests/f4-entity-index-on.mdl`. Findings #4 |
| `retrieve … where [Seq = $Game/MoveSeq + 1]` fails with a bare `mismatched input '+' expecting ']'` — no hint that Mendix XPath can't compute values (this is a Mendix limitation, not an mxcli bug) | Mendix XPath constraints take a literal/token/variable/path on the value side, never an arithmetic expression; the parse error named the token but not the cause | `mdl/visitor/visitor.go` (`enhanceErrorMessage`, `looksLikeXPathArithmetic`/`xpathArithmeticRe`) | Do NOT add grammar support (mxbuild would still reject the XPath). Add an error hint keyed on `mismatched input '<+|*|div|mod>' expecting ']'` (`expecting ']'` only occurs inside a `[…]` constraint) explaining the limitation and the workaround: compute into a variable first, then compare. Also documented in `xpath-constraints.md`. Bug-test `mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl`. Findings #8 |
| Design properties are written free-form: a `ColorPicker`/`ToggleButtonGroup` value serializes as a plain option (wrong `$Type` for Studio Pro's Appearance tab), and a typo'd key/value (they're case-sensitive) passes `mxcli check`. Also `show design properties <widget>` reports "No design properties found for widget type container" for a valid widget | Root bug: `resolveDesignPropsKey` upper-cased the MDL keyword but the lookup map is **lowercase-keyed**, so `container`→`DivContainer` never resolved — leaving `resolveDesignPropertyValueType` dead code and the theme registry unused on the write/validate paths | `mdl/executor/theme_reader.go` (`resolveDesignPropsKey` case fix) + `mdl/executor/cmd_pages_builder_v3.go` (`astDesignPropToValue` takes theme props) + `mdl/executor/validate_design_properties.go` (new, MDL-WIDGET11/12) wired from `cmd/mxcli/cmd_check.go` + `cmd/mxcli/lsp_diagnostics.go` (cached `themeRegistry`) | Fix `resolveDesignPropsKey` to lower-case the lookup. On write, resolve each flat value's type from the registry (`ColorPicker`/`ToggleButtonGroup`→custom). On check (`-p` only, when themesource defines properties), walk page/snippet/alter-page widget trees and warn: **MDL-WIDGET11** unknown key (case-sensitivity hint / valid-key list), **MDL-WIDGET12** invalid value (lists allowed values). Warnings, not errors — a newer theme may add keys/values (forward-compat, per `page-styling-support.md:402`). Skip compound (registry doesn't model sub-props) and widgets with no type-specific metadata (pluggable). Bug-test `mdl-examples/bug-tests/typed-design-properties.mdl` |
| `mxcli run --local`: when a page action throws, the browser shows the generic Mendix error dialog and there is nothing to correlate it against — the runtime's own stdout/stderr (server stack trace, microflow `LOG` output) is swallowed, so a server-side bug can't be told apart from a client one | The runtime JVM was spawned with `cmd.Stdout=log; cmd.Stderr=log` where `log` is an in-memory `syncBuffer` surfaced only on a *startup* failure; during normal operation it goes nowhere on disk | `cmd/mxcli/docker/localboot.go` (`spawnAndConfigure`, `openRuntimeLog`, `LocalRuntime.logFile`, `LocalRuntimeOptions.RuntimeLogPath`) + `cmd/mxcli/docker/runlocal.go` (default `<projectDir>/.mxcli/runtime.log`) + `cmd/mxcli/cmd_run.go` (`--runtime-log`) | Tee the JVM's stdout+stderr to `<projectDir>/.mxcli/runtime.log` via `io.MultiWriter(log, file)` (the in-memory buffer still backs startup-error reporting). Append across restarts with a `=== runtime start … ===` marker; close the handle on Stop/reopen. Default on; `--runtime-log <path>` relocates, `-` disables. Print the path at boot. Test `TestOpenRuntimeLog`. Findings #25 |

---

Expand Down
11 changes: 11 additions & 0 deletions .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Launch `run --local` as the **sole** command in its invocation (don't chain a tr
| `--screenshot` | off | Playwright PNG after boot + each change |
| `--screenshot-path` / `--screenshot-url` | `.mxcli/run-local.png` / app root | Screenshot output / page (URL or `/path`) |
| `--screenshot-user` / `--screenshot-password` | — | Log in once, reuse session (pages behind login) |
| `--runtime-log` | `.mxcli/runtime.log` | Tee the Mendix runtime's stdout+stderr to a file for debugging (`-` disables) |
| `--app-port` / `--admin-port` / `--serve-port` | 8080 / 8090 / 6543 | Ports |
| `--db-host` / `--db-name` / `--db-user` / `--db-password` | 127.0.0.1:5432 / derived / mendix / mendix | Database |

Expand Down Expand Up @@ -149,6 +150,16 @@ mxcli run --local -p app.mpr --watch --screenshot
reuse the session, so pages behind login render authenticated. Best-effort: an
anonymous app with no login form proceeds unauthenticated.

## Debugging a server-side error

When a page action throws, the browser shows the generic Mendix error dialog with no
detail. The runtime's own log — server stack traces and your microflow `LOG ERROR`/
`LOG INFO` output — is tee'd to `<projectDir>/.mxcli/runtime.log` (the path is printed
at boot). `tail -f .mxcli/runtime.log` while you reproduce the action to see the stack
and correlate it. The file is appended across restarts (each boot writes a
`=== runtime start … ===` marker). Override the path with `--runtime-log <path>`, or
pass `--runtime-log -` to disable the file.

## External browser preview (`--hub`)

`--hub <url>` exposes the running app in a **browser at a public URL** without the app
Expand Down
8 changes: 8 additions & 0 deletions cmd/mxcli/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ mxcli tunnel-hub, without leaving this machine: a chisel client reverse-tunnels
the local app out over 443, and the runtime boots with ApplicationRootUrl set to
the hub URL so the app works under that origin. --hub implies --local.

The Mendix runtime's own stdout/stderr (server stack traces, microflow LOG
output) is tee'd to <projectDir>/.mxcli/runtime.log so a server-side error is
debuggable — the path is printed at boot. Override with --runtime-log <path>,
or "-" to disable.

Examples:
mxcli run --local -p app.mpr
mxcli run --local -p app.mpr --watch
Expand Down Expand Up @@ -90,6 +95,7 @@ Examples:
screenshotURLs, _ := cmd.Flags().GetStringArray("screenshot-url")
screenshotUser, _ := cmd.Flags().GetString("screenshot-user")
screenshotPassword, _ := cmd.Flags().GetString("screenshot-password")
runtimeLog, _ := cmd.Flags().GetString("runtime-log")

opts := docker.LocalRunOptions{
ProjectPath: projectPath,
Expand All @@ -111,6 +117,7 @@ Examples:
ScreenshotURLs: screenshotURLs,
ScreenshotUser: screenshotUser,
ScreenshotPassword: screenshotPassword,
RuntimeLogPath: runtimeLog,
DB: docker.DBConfig{
Host: dbHost,
Name: dbName,
Expand Down Expand Up @@ -152,5 +159,6 @@ func init() {
runCmd.Flags().StringArray("screenshot-url", nil, "Page to screenshot: a full URL or a path relative to the app root, e.g. /p/customers (default the app root). Repeat for a multi-page set.")
runCmd.Flags().String("screenshot-user", "", "Log in with this user before screenshotting (for pages behind login)")
runCmd.Flags().String("screenshot-password", "", "Password for --screenshot-user")
runCmd.Flags().String("runtime-log", "", "Tee the Mendix runtime's stdout+stderr to this file for debugging (default <projectDir>/.mxcli/runtime.log; \"-\" to disable)")
rootCmd.AddCommand(runCmd)
}
57 changes: 50 additions & 7 deletions cmd/mxcli/docker/localboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,22 @@ type LocalRuntimeOptions struct {
// Stdout/Stderr receive progress messages (default os.Stdout/os.Stderr).
Stdout io.Writer
Stderr io.Writer
// RuntimeLogPath, when set, tees the runtime JVM's stdout+stderr to this file
// (appended across restarts) so the warm loop is debuggable — server-side
// stack traces and microflow LOG output land somewhere a developer can read.
// Empty disables the file tee (the in-memory buffer for startup errors is
// unaffected). Findings #25.
RuntimeLogPath string
}

// LocalRuntime is a booted standalone runtime process plus its admin connection.
type LocalRuntime struct {
opts LocalRuntimeOptions
cmd *exec.Cmd
log *syncBuffer
m2ee M2EEOptions
ctrl *RuntimeController
opts LocalRuntimeOptions
cmd *exec.Cmd
log *syncBuffer
logFile *os.File // open when RuntimeLogPath is set; runtime stdout/stderr tee
m2ee M2EEOptions
ctrl *RuntimeController
}

func (o *LocalRuntimeOptions) applyDefaults() {
Expand Down Expand Up @@ -265,9 +272,22 @@ func (rt *LocalRuntime) spawnAndConfigure() error {
cmd.Env = localRuntimeEnv(rt.opts)
PrepareMxCommand(cmd) // FreeType LD_PRELOAD workaround, layered on cmd.Env
setProcessGroup(cmd) // reap any JVM child on Stop so the port is freed
// The in-memory buffer always captures output for startup-failure reporting.
// When RuntimeLogPath is set, also tee stdout+stderr to that file so the
// runtime's own log (stack traces, microflow LOG output) is readable while
// the warm loop runs — otherwise it is swallowed (findings #25).
log := &syncBuffer{}
cmd.Stdout = log
cmd.Stderr = log
var out io.Writer = log
if rt.opts.RuntimeLogPath != "" {
if f, err := rt.openRuntimeLog(); err == nil {
rt.logFile = f
out = io.MultiWriter(log, f)
} else {
fmt.Fprintf(rt.opts.Stdout, " (could not open runtime log %s: %v)\n", rt.opts.RuntimeLogPath, err)
}
}
cmd.Stdout = out
cmd.Stderr = out
if err := cmd.Start(); err != nil {
return fmt.Errorf("launching runtime JVM: %w", err)
}
Expand All @@ -292,6 +312,25 @@ func (rt *LocalRuntime) spawnAndConfigure() error {
return nil
}

// openRuntimeLog opens (creating the parent dir) the runtime log for appending
// and writes a start marker. A prior handle (from an earlier spawn/restart) is
// closed first so the file is reused across restarts rather than leaked.
func (rt *LocalRuntime) openRuntimeLog() (*os.File, error) {
if rt.logFile != nil {
_ = rt.logFile.Close()
rt.logFile = nil
}
if err := os.MkdirAll(filepath.Dir(rt.opts.RuntimeLogPath), 0o755); err != nil {
return nil, err
}
f, err := os.OpenFile(rt.opts.RuntimeLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, err
}
fmt.Fprintf(f, "\n=== runtime start %s ===\n", time.Now().Format(time.RFC3339))
return f, nil
}

// waitAdminReady polls runtime_status until the admin API responds or times out.
func (rt *LocalRuntime) waitAdminReady(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
Expand Down Expand Up @@ -364,5 +403,9 @@ func (rt *LocalRuntime) stopProcess() error {
<-done
}
rt.cmd = nil
if rt.logFile != nil {
_ = rt.logFile.Close()
rt.logFile = nil
}
return nil
}
50 changes: 50 additions & 0 deletions cmd/mxcli/docker/localboot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package docker
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -200,3 +201,52 @@ func TestStartLocalRuntime_Validation(t *testing.T) {
t.Error("expected error when the launcher jar is absent")
}
}

// TestOpenRuntimeLog verifies the runtime-log tee creates the parent dir, appends
// across restarts (reusing the file, closing the prior handle), and writes a
// start marker each spawn. Findings #25.
func TestOpenRuntimeLog(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "nested", ".mxcli", "runtime.log")
rt := &LocalRuntime{opts: LocalRuntimeOptions{RuntimeLogPath: logPath}}

// First spawn: opens, writes marker, then some "runtime" output.
f1, err := rt.openRuntimeLog()
if err != nil {
t.Fatalf("openRuntimeLog: %v", err)
}
rt.logFile = f1
if _, err := f1.WriteString("first-run line\n"); err != nil {
t.Fatalf("write: %v", err)
}

// Second spawn (a restart): must reuse the same file (append) and close f1.
f2, err := rt.openRuntimeLog()
if err != nil {
t.Fatalf("openRuntimeLog restart: %v", err)
}
rt.logFile = f2
if _, err := f2.WriteString("second-run line\n"); err != nil {
t.Fatalf("write 2: %v", err)
}
_ = f2.Close()

data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read log: %v", err)
}
s := string(data)
for _, want := range []string{"first-run line", "second-run line", "=== runtime start"} {
if !strings.Contains(s, want) {
t.Errorf("runtime log missing %q; got:\n%s", want, s)
}
}
// Two start markers (one per spawn) → append, not truncate.
if n := strings.Count(s, "=== runtime start"); n != 2 {
t.Errorf("expected 2 start markers (append across restarts), got %d", n)
}
// Writing to the first (now-closed) handle must fail — it was closed on reopen.
if _, err := f1.WriteString("x"); err == nil {
t.Error("expected write to the closed prior handle to fail")
}
}
23 changes: 20 additions & 3 deletions cmd/mxcli/docker/runlocal.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ type LocalRunOptions struct {
// screenshotStorage is the resolved Playwright storage-state file (from login);
// internal, set during boot.
screenshotStorage string
Stdout io.Writer
Stderr io.Writer
// RuntimeLogPath tees the Mendix runtime's stdout+stderr to a file so the
// warm loop is debuggable (server stack traces, microflow LOG output).
// Default <projectDir>/.mxcli/runtime.log; set to "-" to disable. Findings #25.
RuntimeLogPath string
Stdout io.Writer
Stderr io.Writer
}

// defaultLocalAdminPass is the admin password for a local dev runtime. The admin
Expand Down Expand Up @@ -133,6 +137,9 @@ func (o *LocalRunOptions) applyDefaults() {
if o.ScreenshotPath == "" {
o.ScreenshotPath = filepath.Join(filepath.Dir(o.ProjectPath), ".mxcli", "run-local.png")
}
if o.RuntimeLogPath == "" {
o.RuntimeLogPath = filepath.Join(filepath.Dir(o.ProjectPath), ".mxcli", "runtime.log")
}
if o.Stdout == nil {
o.Stdout = os.Stdout
}
Expand Down Expand Up @@ -458,7 +465,13 @@ func RunLocal(opts LocalRunOptions) error {
appRootURL = hubReg.URL
}

// 6. Boot the runtime against the fresh deployment.
// 6. Boot the runtime against the fresh deployment. Tee the runtime's own
// stdout/stderr to a log file so server-side errors are debuggable ("-"
// disables). (findings #25)
runtimeLog := opts.RuntimeLogPath
if runtimeLog == "-" {
runtimeLog = ""
}
rt, err := StartLocalRuntime(LocalRuntimeOptions{
DeployDir: opts.DeployDir,
InstallPath: installPath,
Expand All @@ -468,6 +481,7 @@ func RunLocal(opts LocalRunOptions) error {
AdminPass: opts.AdminPass,
ApplicationRootUrl: appRootURL,
DB: opts.DB,
RuntimeLogPath: runtimeLog,
Stdout: w,
Stderr: stderr,
})
Expand All @@ -477,6 +491,9 @@ func RunLocal(opts LocalRunOptions) error {
defer rt.Stop()

fmt.Fprintf(w, "\nApp is running at %s\n", rt.AppURL())
if runtimeLog != "" {
fmt.Fprintf(w, "Runtime log: %s\n", runtimeLog)
}

// 6a. With --hub, open a reverse tunnel so the app is reachable in a browser at
// its public URL, and heartbeat so it shows as available in the hub overview.
Expand Down
10 changes: 10 additions & 0 deletions docs-site/src/tools/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ so structural changes need a restart; behavioural changes do not.
| `--screenshot-path` | `<projectDir>/.mxcli/run-local.png` | Screenshot output PNG |
| `--screenshot-url` | app root | Page to shoot: full URL, or a path relative to the app root (e.g. `/p/customers`). Repeat for a multi-page set. |
| `--screenshot-user` / `--screenshot-password` | — | Log in once (Mendix form auth) and reuse the session, so pages behind login render authenticated |
| `--runtime-log` | `<projectDir>/.mxcli/runtime.log` | Tee the Mendix runtime's stdout+stderr (server stack traces, microflow `LOG` output) to a file; `-` disables |

## Debugging a server-side error

When a page action throws, the browser shows the generic Mendix error dialog with no
detail. The runtime's own log is tee'd to `<projectDir>/.mxcli/runtime.log` (the path is
printed at boot) — `tail -f .mxcli/runtime.log` while you reproduce the action to see the
server stack trace and your microflow `LOG` output. The file is appended across restarts,
each marked with `=== runtime start … ===`. Use `--runtime-log <path>` to relocate it or
`--runtime-log -` to turn it off.

## External browser preview (`--hub`)

Expand Down
Loading