Skip to content

feat(devloop): run on various application servers - #25851

Merged
mshabarov merged 38 commits into
mainfrom
feat/devloop-wildfly-tomee-goals
Sep 30, 2026
Merged

mshabarov merged 38 commits into
mainfrom
feat/devloop-wildfly-tomee-goals

Conversation

@tltv

@tltv tltv commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Fixes #25458

Background — how the loop reaches the application. The dev loop hot
swaps by attaching two -javaagent jars to the JVM the application runs
in and redefining classes through Instrumentation. For a server that
runs inside the build's own JVM, that JVM is Maven's, so MAVEN_OPTS
carries them and there is nothing else to arrange.

Until now the loop could only run an application whose server shared the
build's JVM. It now also runs WildFly, TomEE, Payara Server, Payara
Micro, Open Liberty and Apache Tomcat, which each start a server of
their own, with the agents travelling in a plugin parameter that reaches
the forked JVM. Two fixes and an integration-test fixture come with it.

Context. Nothing on Maven's command line and nothing in its
environment reaches a JVM that Maven forked, so each forked container
needs a channel of its own. Four of them expose one as a user property
and Liberty as a property prefix; Cargo exposes none that any command
line can set, which is why it is the one runtime that cannot work
without the loop's build extension.

  • Added WildFly and TomEE as runtimes, with the JVM flags travelling in
    wildfly.javaOpts and tomee-plugin.args, module options folded into
    one token for WildFly, and backslashes doubled for TomEE alone
  • Added Payara Server and Payara Micro, both on the start goal, never
    dev, whose watcher redeploys in competition with every apply
    • Payara Server's payara.javaCommandLineOptions is a List<String>,
      which Maven splits on commas and the mojo then drops any piece
      without an = in it, so comma-bearing flags travel in a JVM
      argument file instead
  • Added Open Liberty, whose channel is one Maven property per flag
    (liberty.jvm.<key>), with looseApplication forced off so that
    Liberty's own application monitor stops competing with every apply
  • Added Apache Tomcat, driven through Codehaus Cargo, in one entry that
    answers for every container Cargo drives
    • Its flags go on the model as the cargo.jvmargs project property,
      because no parameter of its run goal carries a user property, and
      the build extension appends to a value a project set itself rather
      than replacing it
  • Switched the run goal off for the whole reactor and back on for the
    application's module for Cargo, both Payaras and WildFly: unlike
    Jetty's, their mojos do not skip a project that builds no deployment,
    and failed or blocked the build on the reactor root
    • A forked container now names package across the reactor as well,
      so a sibling module reaches WEB-INF/lib as a jar rather than as a
      directory
  • Gave a forked container a startup window of its own, since
    provisioning a server outruns the time an ordinary boot is given
  • Confirmed a resource against the bytes on the classpath before
    counting it as changed, so a rewritten-but-identical file is no longer
    a change
  • Tried both loopback literals when the application dials the daemon,
    after a Payara Micro boot left the application JVM preferring ::1
    while the daemon listened on 127.0.0.1 and the application ran
    unregistered
  • Moved the agent's Instrumentation handle out of the system
    properties, which WildFly's transactions subsystem read as a string
    and died on
  • Added test-devloop-cargo, a third IT fixture running the shared
    dev-loop ITs against a WAR deployed on Tomcat 11, and wired it into
    the CI matrix
  • Adds a fixture per forked server the dev loop drives, each running the
    shared suite from flow-test-devloop-support in dev-bundle mode, and runs
    them in CI slots 9 and 10. Liberty is served under /devloop, so the ITs
    read a context path from the fixture pom.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

 1 623 files  + 72   1 707 suites  +72   2h 8m 58s ⏱️ + 26m 55s
12 598 tests +109  12 526 ✅ +109  72 💤 ±0  0 ❌ ±0 
13 253 runs  +365  13 181 ✅ +365  72 💤 ±0  0 ❌ ±0 

Results for commit ef20217. ± Comparison against base commit 1224eae.

♻️ This comment has been updated with latest results.

@tltv
tltv force-pushed the feat/devloop-jetty-support branch from 979f9b4 to 385c2a7 Compare September 22, 2026 07:27
@tltv
tltv force-pushed the feat/devloop-wildfly-tomee-goals branch from a198f84 to f67ebb1 Compare September 22, 2026 07:48
@vaadin-bot vaadin-bot added +1.0.0 and removed +0.0.1 labels Sep 22, 2026
@github-actions github-actions Bot added +0.0.1 and removed +1.0.0 labels Sep 22, 2026
@tltv tltv changed the title feat(devloop): run on WildFly and TomEE feat(devloop): run on Cargo, WildFly and TomEE Sep 22, 2026
Base automatically changed from feat/devloop-jetty-support to main September 23, 2026 08:28
@tltv
tltv force-pushed the feat/devloop-wildfly-tomee-goals branch 2 times, most recently from 0a9c4dd to 2e45907 Compare September 24, 2026 11:16
@tltv tltv changed the title feat(devloop): run on Cargo, WildFly and TomEE feat(devloop): run on various application servers Sep 24, 2026
@tltv
tltv force-pushed the feat/devloop-wildfly-tomee-goals branch from 3724211 to 2a85db1 Compare September 25, 2026 12:00
@tltv
tltv marked this pull request as ready for review September 25, 2026 12:40
@github-actions

Copy link
Copy Markdown
Contributor

A forked server's JVM flags now travel through a build-extension hop before Maven runs the goal

sequenceDiagram
    participant D as MavenGoalRuntime.invocation()
    participant M as Maven CLI
    participant E as DevLoopBuildExtension
    participant P as wildfly-maven-plugin
    participant S as forked server JVM
    D->>M: runs mvn with wildfly.skip=true, wildfly.javaOpts carrying the agents
    Note over D,M: Jetty still sets MAVEN_OPTS directly, no extension involved
    M->>E: afterProjectsRead(session) (new)
    E->>E: reconfigure() forces skip=false and javaOpts in the app module only (new)
    E->>M: effective model rewritten for that module (new)
    M->>P: runs wildfly:run in the app module alone
    P->>S: starts WildFly, javaOpts handing the agents to this JVM (new hop)
    Note over S: MAVEN_OPTS never reaches this JVM, only the plugin's own parameter does
Loading

Per ServerPlugin.java and DevLoopBuildExtension.java, the loop previously only drove Jetty, whose server shares Maven's own JVM and reads MAVEN_OPTS directly. For WildFly, TomEE, both Payaras, Liberty and Cargo, the server is a forked process Maven's command line and environment cannot reach, so MavenGoalRuntime now hands the agents to DevLoopBuildExtension, a Maven lifecycle participant that rewrites the plugin's effective model - forcing <skip>false</skip> and the flags property - in the application's module alone, before the goal that starts the forked JVM runs.

Diagram Bot draws the mechanism this pull request touches; it does not review the change. Verify it against the diff.

Generated by Diagram Bot for #25851 · agent · 73.8 AIC · ⊞ 9.1K · ◷

Both run goals fork a server JVM, so the agents cannot ride in on
MAVEN_OPTS the way they do for embedded Jetty. ServerPlugin records
where a server takes its JVM flags, and MavenGoalRuntime routes the
agents and the application's own -D settings into that parameter.

What the forked shape forced:

- WildFly groups module options before it builds the server command
  line, so a two-token `--add-opens X` arrives as every flag ahead of
  every value and the JVM refuses to start; they are folded into
  `--add-opens=X`.
- TomEE reads its `args` the way a shell would and drops a backslash as
  an escape, so a Windows path survives only when doubled. WildFly
  splits on whitespace and must not be escaped, so the escaping is per
  plugin.
- WildFly's goal already forks `package` and TomEE's forks nothing, so
  only TomEE is given a phase.
- Provisioning a server on a first start outruns the five minutes a
  boot is given, so the startup window is now a runtime's to declare.

The agent no longer publishes the Instrumentation handle into the
system properties table: a non-String value in a table the rest of the
JVM reads as strings is a trap. WildFly's transactions subsystem merges
those properties when it loads jbossts-properties.xml, took the handle
for a string, and failed its boot with a NullPointerException after the
HTTP listener had already bound. The handle travels by
DevLoopAgent.get() through the system class loader instead, and
DevLoopRedefiner falls back to the old property so that a newer dev
server still works with an older agent jar.
Tomcat has no Maven plugin a Jakarta EE project can use: Apache's
tomcat7-maven-plugin was last released in 2013 and runs a javax.servlet
container. Codehaus Cargo is what the ecosystem uses instead - this
repository's own servlet-container tests included - so the entry is
named after the plugin rather than after Tomcat, and it covers every
other container Cargo drives.

What the plugin forced:

- No parameter of cargo:run that could carry the loop's JVM flags has a
  user property, so no -D on a command line reaches one. What Cargo does
  read is any Maven project property named cargo.*, so
  DevLoopBuildExtension now also sets project properties for the run.
  One command-line setting per property rather than a separated list:
  the value holds the application's whole JVM command line, and on
  Windows -Dvaadin.devloop.classes= alone contains semicolons.
- cargo.start.jvmargs rather than cargo.jvmargs, because Cargo appends
  both to the container's command line and only the second is one a
  project is likely to have written for itself.
- A goal named on a Maven command line runs on every project in the
  reactor, and the loop names one with -pl :app -am. Jetty's mojo
  supports war packaging alone and skips the rest; Cargo's does not, and
  failed the build on the reactor root before anything had started. The
  goal is switched off reactor-wide by cargo.maven.skip and switched
  back on for the module that declares the plugin by a forced
  <skip>false</skip> - the rule Competing already exists for, used the
  other way round.
- Cargo parses the value with Ant's translateCommandline, where a
  backslash is an ordinary character, so a Windows path needs no
  escaping here - unlike TomEE's.

Readiness reads Cargo's own "<name> started on port [<port>]" rather
than the container's, which is what lets one entry answer for every
container Cargo drives and survives a project that sends the container's
output to a file.

This is the one runtime that cannot work without the build extension, so
a daemon running from an exploded build directory says so rather than
starting a server the agents never reached.

Verified against Tomcat 11.0.26: detected, started, registered, and a
method body hot-swapped in 0.97s with no restart.
staleResources asks whether the classpath copy is current and whether the
daemon has acted on this content, and both were asked of timestamps. A
timestamp says a file was written, not that it changed: tsc rewrites nine
.d.ts files under vaadin-dev-server/src/main/resources on every build with
identical content, and because they are startup-only resources every apply
restarted the application over an edit that was one method body.

The bytes on the classpath are what the running application read, so a yes
from either question is now confirmed against them before it counts.
A third fixture, for the shape the other two do not cover: a container
installed and started as a process of its own and handed a packaged copy of
the WAR, rather than one serving the module's output in place. Laid out
exactly like test-devloop-jetty, so the ITs in test-devloop-support run
against it unchanged through <dependenciesToScan> - 45 tests, with the
DevLoopCargo*IT classes holding what is true only of a fork reading a
deployed WAR.

DevLoopCssIT is excluded and DevLoopCargoResourceIT replaces it: the sibling
module's stylesheet reaches the container inside a jar in the deployed WAR,
so refreshing that module's target/classes - the whole of what an apply can
do for a resource - never reaches what Tomcat serves. The class half of the
same leg is unaffected, and holds for WildFly and TomEE too.

vaadin-dev-server is declared without <optional>, unlike the Jetty fixture's:
maven-war-plugin leaves optional dependencies out of WEB-INF/lib, and the
application would otherwise start, serve, and never register with the daemon.
Both entries run the start goal, never dev, whose watcher rebuilds and
redeploys in competition with every apply.

Payara Server cost one new field. Its flags parameter is a List<String>,
which Maven splits on commas, and the mojo then drops any piece with no
"=" in it - so -DdisabledPlugins=Vaadin,Spring,... would have arrived as
-DdisabledPlugins=Vaadin in silence. commaSplitFlags marks that channel
and the comma-bearing flags travel in a JVM argument file instead.
Micro's exec.args is a plain string and needed no field.

Also fixes the Cargo entry, which set cargo.start.jvmargs. Cargo's
GlassFish family strips cargo.jvmargs from the asadmin client and writes
it into the domain config, while start.jvmargs reaches only asadmin - so
a Payara container would have started with no agents and every apply
would have restarted. cargo.jvmargs is correct for Tomcat too. Since a
project may set it itself, the build extension now appends to a project
property rather than replacing it.
wildfly:run never checks packaging, so it ended the build on the reactor
root asking for a deployment named after a pom. It is now switched off
reactor-wide with wildfly.skip and forced back on for the application's
own module, as Cargo and both Payaras already are.

Its packaging fork covers that module alone, so a sibling stopped at
compile and reached WEB-INF/lib as a directory rather than a jar. A
forked container now names package across the reactor; this also covers
Liberty.
The classpath copy cannot answer whether the running application has
seen a resource: an IDE that copies resources on save makes the copy
current while the app still holds the old content, so the restart never
happened. Remember a digest per resource instead.
tltv and others added 2 commits September 28, 2026 12:19
Sonar (java:S864) flagged the ternary mixing a condition with string
concatenation. An early return says the same without relying on
operator precedence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…25957)

Adds a fixture per forked server the dev loop drives, each running the
shared suite from flow-test-devloop-support in dev-bundle mode, and runs
them in CI slots 9 and 10. Liberty is served under /devloop, so the ITs
read a context path from the fixture pom.
tltv and others added 3 commits September 29, 2026 10:45
The settle window ended a start on the deployment WildFly booted from its
persisted configuration when replacing it outlasted the window, and the
replacement line could be read before the replaced deployment's close
reached the daemon. Only the launch's own deployment now ends the start,
after a short window for that close to arrive.
mshabarov
mshabarov previously approved these changes Sep 29, 2026
@mshabarov

Copy link
Copy Markdown
Contributor

@tltv I pushed a fix for these found minor problems, please let me know if you thing these are redundant:

Problems

  1. WildFly first boots the deployment saved from the last run, then replaces it with the build's own. If the replacement took longer than the 15s settle window, the start reported "running" on the copy that was about to be undeployed.
  2. The "replaced deployment" log line could be read before the daemon noticed the old deployment's connection closing. The start then returned with no live registration behind it.

Fix

  • On runtimes that replace a deployment after boot (only WildFly for now), only the build's own deployment line ends the start. The serving line and the settle window no longer do. If that line never appears, the start times out with a clear message.
  • After that line, the start waits a short extra window (1s, vaadin.dev.deploySettleMillis) so the old connection's close can arrive. If the registration drops during it, the start waits for the new one.
  • The decision logic now lives in AppProcess.StartWait, with unit tests for both cases.

@tltv
tltv added this pull request to the merge queue Sep 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 29, 2026
@tltv
tltv added this pull request to the merge queue Sep 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 29, 2026
The CLI output was read to the end before waiting on the process, so a
command that hung, or a process it started that kept the pipe open,
blocked the test until the CI step timed out, with no output. Read the
output on its own thread, fail after 5 minutes with what the CLI said,
and kill what the command started.
When a dev loop IT hangs on a start or an apply, the reason is in
target/devloop/app.log and daemon.log, which the report left out.
@sonarqubecloud

Copy link
Copy Markdown

@mshabarov
mshabarov added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 39c4c52 Sep 30, 2026
49 checks passed
@mshabarov
mshabarov deleted the feat/devloop-wildfly-tomee-goals branch September 30, 2026 07:24
vaadin-bot added a commit to vaadin/docs that referenced this pull request Sep 30, 2026
Add WildFly, TomEE, Payara Server, Payara Micro, Open Liberty, and
Cargo (Apache Tomcat) to the Dev Loop CLI's supported runtimes table,
the `-Dvaadin.dev.runtime` override values, and the differences a
forked server process brings versus an embedded one.

Documents vaadin/flow#25851 (`ef202175510f770337e555bc007e8ce439cc91ad`).
@github-actions

Copy link
Copy Markdown
Contributor

Pull request created: #6213

Generated by Documentation Bot · agent · 113.2 AIC · ⌖ 6.85 AIC · ⊞ 11.7K

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Bot: Draft documentation pull request for this change: vaadin/docs#6213

Files updated:

  • articles/flow/configuration/live-reload/dev-loop-cli.adoc

It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.

Generated by Documentation Bot for #25851 · agent · 113.2 AIC · ⌖ 6.85 AIC · ⊞ 11.7K · ◷

vaadin-bot added a commit that referenced this pull request Sep 30, 2026
…#26066)

This PR cherry-picks changes from the original PR #25851 to branch 25.3.
---
#### Original PR description
> Fixes #25458
> 
> **Background — how the loop reaches the application.** The dev loop
hot
> swaps by attaching two `-javaagent` jars to the JVM the application
runs
> in and redefining classes through `Instrumentation`. For a server that
> runs inside the build's own JVM, that JVM is Maven's, so `MAVEN_OPTS`
> carries them and there is nothing else to arrange.
> 
> Until now the loop could only run an application whose server shared
the
> build's JVM. It now also runs WildFly, TomEE, Payara Server, Payara
> Micro, Open Liberty and Apache Tomcat, which each start a server of
> their own, with the agents travelling in a plugin parameter that
reaches
> the forked JVM. Two fixes and an integration-test fixture come with
it.
> 
> **Context.** Nothing on Maven's command line and nothing in its
> environment reaches a JVM that Maven forked, so each forked container
> needs a channel of its own. Four of them expose one as a user property
> and Liberty as a property prefix; Cargo exposes none that any command
> line can set, which is why it is the one runtime that cannot work
> without the loop's build extension.
> 
> - Added WildFly and TomEE as runtimes, with the JVM flags travelling
in
> `wildfly.javaOpts` and `tomee-plugin.args`, module options folded into
>   one token for WildFly, and backslashes doubled for TomEE alone
> - Added Payara Server and Payara Micro, both on the `start` goal,
never
>   `dev`, whose watcher redeploys in competition with every apply
> - Payara Server's `payara.javaCommandLineOptions` is a `List<String>`,
>     which Maven splits on commas and the mojo then drops any piece
>     without an `=` in it, so comma-bearing flags travel in a JVM
>     argument file instead
> - Added Open Liberty, whose channel is one Maven property per flag
>   (`liberty.jvm.<key>`), with `looseApplication` forced off so that
>   Liberty's own application monitor stops competing with every apply
> - Added Apache Tomcat, driven through Codehaus Cargo, in one entry
that
>   answers for every container Cargo drives
>   - Its flags go on the model as the `cargo.jvmargs` project property,
>     because no parameter of its run goal carries a user property, and
>     the build extension appends to a value a project set itself rather
>     than replacing it
> - Switched the run goal off for the whole reactor and back on for the
>   application's module for Cargo, both Payaras and WildFly: unlike
> Jetty's, their mojos do not skip a project that builds no deployment,
>   and failed or blocked the build on the reactor root
>   - A forked container now names `package` across the reactor as well,
> so a sibling module reaches `WEB-INF/lib` as a jar rather than as a
>     directory
> - Gave a forked container a startup window of its own, since
>   provisioning a server outruns the time an ordinary boot is given
> - Confirmed a resource against the bytes on the classpath before
> counting it as changed, so a rewritten-but-identical file is no longer
>   a change
> - Tried both loopback literals when the application dials the daemon,
>   after a Payara Micro boot left the application JVM preferring `::1`
>   while the daemon listened on `127.0.0.1` and the application ran
>   unregistered
> - Moved the agent's `Instrumentation` handle out of the system
>   properties, which WildFly's transactions subsystem read as a string
>   and died on
> - Added `test-devloop-cargo`, a third IT fixture running the shared
>   dev-loop ITs against a WAR deployed on Tomcat 11, and wired it into
>   the CI matrix
> - Adds a fixture per forked server the dev loop drives, each running
the
> shared suite from flow-test-devloop-support in dev-bundle mode, and
runs
> them in CI slots 9 and 10. Liberty is served under /devloop, so the
ITs
> read a context path from the fixture pom.

---------

Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: mikhail <mikhail@vaadin.com>
Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
Co-authored-by: Marco Collovati <marco@vaadin.com>
mshabarov pushed a commit to vaadin/docs that referenced this pull request Oct 2, 2026
Documentation for vaadin/flow#25851.

**Change categories:** NEW_FEATURE, BEHAVIOR_CHANGE

| File | Change |
|------|--------|
| `articles/flow/configuration/live-reload/dev-loop-cli.adoc` | Added
`wildfly`, `tomee`, `payara`, `payara-micro`, `liberty`, and `cargo` to
the "Launch With Various Containers" runtime table and the
`-Dvaadin.dev.runtime` override list; replaced the stale "more runtimes
are planned" note; added bullets explaining that these runtimes fork a
server process of their own (longer startup window) and that a
not-yet-loaded class keeps its pre-edit bytes until the next restart. |

This pull request covers the reader-facing surface only. The rest of the
source pull request is Maven-plugin wiring, integration-test fixtures,
and CI changes internal to the `flow-devloop-daemon` module, none of
which a documentation reader needs.

Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for non-Spring application servers

3 participants