Hello Jacques, Please give me some time. I will look at your request/email and revert back soon.
Thank you, Ashish On Mon, 27 Jul 2026 at 15:41, Jacques Le Roux via dev <[email protected]> wrote: > Hi, > > Nobody care about licences? > > TIA > > Jacques > > Le 20/07/2026 à 11:51, Jacques Le Roux via dev a écrit : > > Hi Ashish, > > > > I Just had a look at > https://nightlies.apache.org/ofbiz/trunk/rat-output.html > > > > Actually IMO there is more than gradle.lockfile. > > I don't think a LICENSE file there is necessary .It's the only plugin > with it, since there is already one LICENSE file at plugins root. Same for > > framework an applis. > > For README.md there is no Apache license in top. It's not mandatory > since it's in rat-excludes.txt (tools repo). Some README.md have an Apache > > license inside. > > Though I added this linethat in rat-excludes.txt in 2017, I wonder now > if we should not remove this line and add the Apache license inside all > > README.md. > > > > What do you (and people at large) think? > > > > TIA > > > > Jacques > > > > Le 10/07/2026 à 15:46, [email protected] a écrit : > >> This is an automated email from the ASF dual-hosted git repository. > >> > >> ashishvijaywargiya pushed a commit to branch trunk > >> in repositoryhttps://gitbox.apache.org/repos/asf/ofbiz-plugins.git > >> > >> > >> The following commit(s) were added to refs/heads/trunk by this push: > >> new fe1a266d6 Add devreload plugin inside plugins directory (#316) > >> fe1a266d6 is described below > >> > >> commit fe1a266d60425b3b73469f185cb68bc27012495d > >> Author: Ashish Vijaywargiya<[email protected]> > >> AuthorDate: Fri Jul 10 19:16:19 2026 +0530 > >> > >> Add devreload plugin inside plugins directory (#316) > >> devreload plugin will give us option to work in Java > files(events and > >> services) and services.xml files without restarting ofbiz. > >> --- > >> devreload/LICENSE | 201 +++++ > >> devreload/README.md | 56 ++ > >> devreload/build.gradle | 187 +++++ > >> devreload/gradle.lockfile | 4 + > >> devreload/ofbiz-component.xml | 30 + > >> .../apache/ofbiz/devreload/DevReloadContainer.java | 860 > +++++++++++++++++++++ > >> .../org/apache/ofbiz/devreload/HotSwapAgent.java | 109 +++ > >> 7 files changed, 1447 insertions(+) > >> > >> diff --git a/devreload/LICENSE b/devreload/LICENSE > >> new file mode 100644 > >> index 000000000..261eeb9e9 > >> --- /dev/null > >> +++ b/devreload/LICENSE > >> @@ -0,0 +1,201 @@ > >> + Apache License > >> + Version 2.0, January 2004 > >> +http://www.apache.org/licenses/ > >> + > >> + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION > >> + > >> + 1. Definitions. > >> + > >> + "License" shall mean the terms and conditions for use, > reproduction, > >> + and distribution as defined by Sections 1 through 9 of this > document. > >> + > >> + "Licensor" shall mean the copyright owner or entity authorized by > >> + the copyright owner that is granting the License. > >> + > >> + "Legal Entity" shall mean the union of the acting entity and all > >> + other entities that control, are controlled by, or are under > common > >> + control with that entity. For the purposes of this definition, > >> + "control" means (i) the power, direct or indirect, to cause the > >> + direction or management of such entity, whether by contract or > >> + otherwise, or (ii) ownership of fifty percent (50%) or more of > the > >> + outstanding shares, or (iii) beneficial ownership of such entity. > >> + > >> + "You" (or "Your") shall mean an individual or Legal Entity > >> + exercising permissions granted by this License. > >> + > >> + "Source" form shall mean the preferred form for making > modifications, > >> + including but not limited to software source code, documentation > >> + source, and configuration files. > >> + > >> + "Object" form shall mean any form resulting from mechanical > >> + transformation or translation of a Source form, including but > >> + not limited to compiled object code, generated documentation, > >> + and conversions to other media types. > >> + > >> + "Work" shall mean the work of authorship, whether in Source or > >> + Object form, made available under the License, as indicated by a > >> + copyright notice that is included in or attached to the work > >> + (an example is provided in the Appendix below). > >> + > >> + "Derivative Works" shall mean any work, whether in Source or > Object > >> + form, that is based on (or derived from) the Work and for which > the > >> + editorial revisions, annotations, elaborations, or other > modifications > >> + represent, as a whole, an original work of authorship. For the > purposes > >> + of this License, Derivative Works shall not include works that > remain > >> + separable from, or merely link (or bind by name) to the > interfaces of, > >> + the Work and Derivative Works thereof. > >> + > >> + "Contribution" shall mean any work of authorship, including > >> + the original version of the Work and any modifications or > additions > >> + to that Work or Derivative Works thereof, that is intentionally > >> + submitted to Licensor for inclusion in the Work by the copyright > owner > >> + or by an individual or Legal Entity authorized to submit on > behalf of > >> + the copyright owner. For the purposes of this definition, > "submitted" > >> + means any form of electronic, verbal, or written communication > sent > >> + to the Licensor or its representatives, including but not > limited to > >> + communication on electronic mailing lists, source code control > systems, > >> + and issue tracking systems that are managed by, or on behalf of, > the > >> + Licensor for the purpose of discussing and improving the Work, > but > >> + excluding communication that is conspicuously marked or otherwise > >> + designated in writing by the copyright owner as "Not a > Contribution." > >> + > >> + "Contributor" shall mean Licensor and any individual or Legal > Entity > >> + on behalf of whom a Contribution has been received by Licensor > and > >> + subsequently incorporated within the Work. > >> + > >> + 2. Grant of Copyright License. Subject to the terms and conditions > of > >> + this License, each Contributor hereby grants to You a perpetual, > >> + worldwide, non-exclusive, no-charge, royalty-free, irrevocable > >> + copyright license to reproduce, prepare Derivative Works of, > >> + publicly display, publicly perform, sublicense, and distribute > the > >> + Work and such Derivative Works in Source or Object form. > >> + > >> + 3. Grant of Patent License. Subject to the terms and conditions of > >> + this License, each Contributor hereby grants to You a perpetual, > >> + worldwide, non-exclusive, no-charge, royalty-free, irrevocable > >> + (except as stated in this section) patent license to make, have > made, > >> + use, offer to sell, sell, import, and otherwise transfer the > Work, > >> + where such license applies only to those patent claims licensable > >> + by such Contributor that are necessarily infringed by their > >> + Contribution(s) alone or by combination of their Contribution(s) > >> + with the Work to which such Contribution(s) was submitted. If You > >> + institute patent litigation against any entity (including a > >> + cross-claim or counterclaim in a lawsuit) alleging that the Work > >> + or a Contribution incorporated within the Work constitutes direct > >> + or contributory patent infringement, then any patent licenses > >> + granted to You under this License for that Work shall terminate > >> + as of the date such litigation is filed. > >> + > >> + 4. Redistribution. You may reproduce and distribute copies of the > >> + Work or Derivative Works thereof in any medium, with or without > >> + modifications, and in Source or Object form, provided that You > >> + meet the following conditions: > >> + > >> + (a) You must give any other recipients of the Work or > >> + Derivative Works a copy of this License; and > >> + > >> + (b) You must cause any modified files to carry prominent notices > >> + stating that You changed the files; and > >> + > >> + (c) You must retain, in the Source form of any Derivative Works > >> + that You distribute, all copyright, patent, trademark, and > >> + attribution notices from the Source form of the Work, > >> + excluding those notices that do not pertain to any part of > >> + the Derivative Works; and > >> + > >> + (d) If the Work includes a "NOTICE" text file as part of its > >> + distribution, then any Derivative Works that You distribute > must > >> + include a readable copy of the attribution notices contained > >> + within such NOTICE file, excluding those notices that do not > >> + pertain to any part of the Derivative Works, in at least one > >> + of the following places: within a NOTICE text file > distributed > >> + as part of the Derivative Works; within the Source form or > >> + documentation, if provided along with the Derivative Works; > or, > >> + within a display generated by the Derivative Works, if and > >> + wherever such third-party notices normally appear. The > contents > >> + of the NOTICE file are for informational purposes only and > >> + do not modify the License. You may add Your own attribution > >> + notices within Derivative Works that You distribute, > alongside > >> + or as an addendum to the NOTICE text from the Work, provided > >> + that such additional attribution notices cannot be construed > >> + as modifying the License. > >> + > >> + You may add Your own copyright statement to Your modifications > and > >> + may provide additional or different license terms and conditions > >> + for use, reproduction, or distribution of Your modifications, or > >> + for any such Derivative Works as a whole, provided Your use, > >> + reproduction, and distribution of the Work otherwise complies > with > >> + the conditions stated in this License. > >> + > >> + 5. Submission of Contributions. Unless You explicitly state > otherwise, > >> + any Contribution intentionally submitted for inclusion in the > Work > >> + by You to the Licensor shall be under the terms and conditions of > >> + this License, without any additional terms or conditions. > >> + Notwithstanding the above, nothing herein shall supersede or > modify > >> + the terms of any separate license agreement you may have executed > >> + with Licensor regarding such Contributions. > >> + > >> + 6. Trademarks. This License does not grant permission to use the > trade > >> + names, trademarks, service marks, or product names of the > Licensor, > >> + except as required for reasonable and customary use in > describing the > >> + origin of the Work and reproducing the content of the NOTICE > file. > >> + > >> + 7. Disclaimer of Warranty. Unless required by applicable law or > >> + agreed to in writing, Licensor provides the Work (and each > >> + Contributor provides its Contributions) on an "AS IS" BASIS, > >> + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or > >> + implied, including, without limitation, any warranties or > conditions > >> + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A > >> + PARTICULAR PURPOSE. You are solely responsible for determining > the > >> + appropriateness of using or redistributing the Work and assume > any > >> + risks associated with Your exercise of permissions under this > License. > >> + > >> + 8. Limitation of Liability. In no event and under no legal theory, > >> + whether in tort (including negligence), contract, or otherwise, > >> + unless required by applicable law (such as deliberate and grossly > >> + negligent acts) or agreed to in writing, shall any Contributor be > >> + liable to You for damages, including any direct, indirect, > special, > >> + incidental, or consequential damages of any character arising as > a > >> + result of this License or out of the use or inability to use the > >> + Work (including but not limited to damages for loss of goodwill, > >> + work stoppage, computer failure or malfunction, or any and all > >> + other commercial damages or losses), even if such Contributor > >> + has been advised of the possibility of such damages. > >> + > >> + 9. Accepting Warranty or Additional Liability. While redistributing > >> + the Work or Derivative Works thereof, You may choose to offer, > >> + and charge a fee for, acceptance of support, warranty, indemnity, > >> + or other liability obligations and/or rights consistent with this > >> + License. However, in accepting such obligations, You may act only > >> + on Your own behalf and on Your sole responsibility, not on behalf > >> + of any other Contributor, and only if You agree to indemnify, > >> + defend, and hold each Contributor harmless for any liability > >> + incurred by, or claims asserted against, such Contributor by > reason > >> + of your accepting any such warranty or additional liability. > >> + > >> + END OF TERMS AND CONDITIONS > >> + > >> + APPENDIX: How to apply the Apache License to your work. > >> + > >> + To apply the Apache License to your work, attach the following > >> + boilerplate notice, with the fields enclosed by brackets "[]" > >> + replaced with your own identifying information. (Don't include > >> + the brackets!) The text should be enclosed in the appropriate > >> + comment syntax for the file format. We also recommend that a > >> + file or class name and description of purpose be included on the > >> + same "printed page" as the copyright notice for easier > >> + identification within third-party archives. > >> + > >> + Copyright [yyyy] [name of copyright owner] > >> + > >> + Licensed under the Apache License, Version 2.0 (the "License"); > >> + you may not use this file except in compliance with the License. > >> + You may obtain a copy of the License at > >> + > >> +http://www.apache.org/licenses/LICENSE-2.0 > >> + > >> + Unless required by applicable law or agreed to in writing, software > >> + distributed under the License is distributed on an "AS IS" BASIS, > >> + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or > implied. > >> + See the License for the specific language governing permissions and > >> + limitations under the License. > >> diff --git a/devreload/README.md b/devreload/README.md > >> new file mode 100644 > >> index 000000000..246d9495c > >> --- /dev/null > >> +++ b/devreload/README.md > >> @@ -0,0 +1,56 @@ > >> +# devreload > >> + > >> +Development-only hot-reload for OFBiz. Edit a Java service/event > method, a > >> +`services.xml` file, or add a brand-new method, and the change is live > in under a > >> +second — no restart, ever. > >> + > >> +The plugin is entirely self-contained: dropping this directory into a > checkout (or > >> +removing it) has zero effect on the rest of OFBiz either way. > >> + > >> +## Requirements > >> + > >> +This plugin needs a **DCEVM (Dynamic Code Evolution Virtual Machine)** > JVM to run. > >> +Set it up once, before running `./gradlew ofbizDev --no-watch-fs`: > >> + > >> +1. Get a DCEVM-patched JVM. Easiest source: JetBrains Runtime (JBR), > bundled with > >> + IntelliJ IDEA under `<IDE install>/jbr` (`.../Contents/jbr` on > macOS). Standalone > >> + DCEVM builds work too. > >> +2. Point at it — set the `DCEVM_HOME` env var (e.g. in your shell > profile) so every > >> + future run picks it up automatically, or pass > `-PdcevmHome=/path/to/jvm` each time > >> + instead. > >> + > >> +Add this to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) so it's > always set: > >> + > >> +``` > >> +export DCEVM_HOME="/Applications/IntelliJ IDEA.app/Contents/jbr" > >> +``` > >> + > >> +(adjust the path to wherever your DCEVM-patched JVM lives — on macOS > this is IntelliJ's > >> +bundled JetBrains Runtime by default). Reload your shell (or run > `source ~/.zshrc`) and > >> +every future `./gradlew ofbizDev --no-watch-fs` picks it up > automatically, with nothing > >> +else to set. > >> + > >> +## Usage > >> + > >> +``` > >> +./gradlew ofbizDev --no-watch-fs > >> +``` > >> + > >> +This is the only supported command — always run it exactly like this. > It boots OFBiz > >> +and everything hot-swaps live, no restart: method-body edits, > `services.xml` changes, > >> +and structural changes (new/removed methods or fields, changed > signatures) alike. > >> + > >> +`--no-watch-fs` disables Gradle's own file-system watching, which > otherwise competes > >> +with this plugin's `WatchService` for the same macOS per-process > directory-watch > >> +ceiling on a full checkout — without it, some directories can silently > go unwatched. > >> +It's harmless to include even on a smaller/scoped checkout, so it's > part of the one > >> +command to remember. > >> + > >> +If DCEVM isn't set up, it fails immediately — before compiling or > booting anything — > >> +with the same setup steps as above. > >> + > >> +Scope to specific components for a faster startup: > >> + > >> +``` > >> +./gradlew ofbizDev --no-watch-fs -Photreload.components=devreload,party > >> +``` > >> diff --git a/devreload/build.gradle b/devreload/build.gradle > >> new file mode 100644 > >> index 000000000..148fcb902 > >> --- /dev/null > >> +++ b/devreload/build.gradle > >> @@ -0,0 +1,187 @@ > >> +/* > >> + * Licensed to the Apache Software Foundation (ASF) under one > >> + * or more contributor license agreements. See the NOTICE file > >> + * distributed with this work for additional information > >> + * regarding copyright ownership. The ASF licenses this file > >> + * to you under the Apache License, Version 2.0 (the > >> + * "License"); you may not use this file except in compliance > >> + * with the License. You may obtain a copy of the License at > >> + * > >> + *http://www.apache.org/licenses/LICENSE-2.0 > >> + * > >> + * Unless required by applicable law or agreed to in writing, > >> + * software distributed under the License is distributed on an > >> + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY > >> + * KIND, either express or implied. See the License for the > >> + * specific language governing permissions and limitations > >> + * under the License. > >> + */ > >> + > >> +// Optional -Photreload.components=compA,compB, forwarded as > >> +// -Dofbiz.hotreload.components to DevReloadContainer, which then only > watches those > >> +// components' servicedef/source directories instead of every > component in the checkout. > >> +// See DevReloadContainer's `allowedComponents` javadoc for why: > macOS's WatchService has > >> +// a practical per-process ceiling on watched directories well under a > typical ulimit, > >> +// and a checkout the size of OFBiz trunk (~50+ components) can exceed > it. > >> +static List<String> hotreloadComponentArgs(Project project) { > >> + String components = project.findProperty('hotreload.components') > >> + return components ? > ["-Dofbiz.hotreload.components=${components}".toString()] : [] > >> +} > >> + > >> +// Optional -Photreload.watchBuildOutput=true, forwarded as > >> +// -Dofbiz.hotreload.watchBuildOutput to DevReloadContainer. Off by > default: watching > >> +// build/classes/java/main only matters for picking up > externally-produced .class files > >> +// (e.g. `./gradlew -t classes` running in a second terminal) -- > in-process compiles hot-swap > >> +// directly and never need it -- and unlike source/servicedef > watching, that tree isn't > >> +// narrowed by -Photreload.components, so on a full checkout it > roughly doubles the total > >> +// directories watched for no benefit to the common (in-process-only) > workflow. > >> +static List<String> hotreloadWatchBuildOutputArgs(Project project) { > >> + Object watchBuildOutput = > project.findProperty('hotreload.watchBuildOutput') > >> + return watchBuildOutput ? > ["-Dofbiz.hotreload.watchBuildOutput=${watchBuildOutput}".toString()] : [] > >> +} > >> + > >> +// DevReloadContainer's own in-process-compile output directory. > Deliberately separate > >> +// from Gradle's own build/classes/java/main (see DevReloadContainer's > hotReloadOutputDir > >> +// field javadoc for why) and placed ahead of the normal runtime > classpath below, so a > >> +// class compiled here but never yet loaded in the running JVM picks > up the live version > >> +// on its first load too, not just already-loaded ones. Passed down to > the JVM as a system > >> +// property (see commonHotReloadJvmArgs) rather than hardcoded > independently on the Java > >> +// side too, so this path exists as a single source of truth. > >> +static File hotReloadOutputDir(Project project) { > >> + return new File(project.rootDir, 'build/devreload/classes') > >> +} > >> + > >> +// Base JVM args for ofbizDev: the hot-reload activation flags, the > output-directory > >> +// location, and the optional component/watch scoping args. > >> +// -XX:+AllowEnhancedClassRedefinition is added separately by the > caller, once a > >> +// DCEVM-patched JVM has actually been resolved (required -- see > below). > >> +static List<String> commonHotReloadJvmArgs(Project project) { > >> + List<String> args = ['-Dofbiz.hotreload=true', > '-Djdk.attach.allowAttachSelf=true', > >> + > "-Dofbiz.hotreload.outputDir=${hotReloadOutputDir(project).absolutePath}".toString()] > >> + args += hotreloadComponentArgs(project) > >> + args += hotreloadWatchBuildOutputArgs(project) > >> + return args > >> +} > >> + > >> +// Locates a DCEVM-patched `java` executable from an explicit > -PdcevmHome=... or > >> +// DCEVM_HOME env var -- deliberately not auto-detected by guessing > per-OS IDE install > >> +// paths (IntelliJ's JBR bundles DCEVM and is a convenient source of > one, but its > >> +// install location varies too much across OS/Toolbox/version to guess > reliably). One > >> +// explicit way in is easier to reason about, document, and keep > working than several > >> +// silent path guesses that can each go stale independently. > `ofbizDev` below requires > >> +// this to resolve to something -- it fails fast with setup > instructions otherwise. > >> +// > >> +// Two ways to set it once and forget it, so every future plain > `./gradlew ofbizDev` > >> +// just works with no flag: export DCEVM_HOME in a shell profile > (picked up via > >> +// System.getenv() below), or add a `dcevmHome=/path/to/jvm` line to > >> +// `~/.gradle/gradle.properties` (GRADLE_USER_HOME -- picked up via > project.findProperty() > >> +// below, same as -PdcevmHome on the command line, just persisted > instead of retyped). > >> +// The latter needs the Gradle property name `dcevmHome`, not > `DCEVM_HOME` -- gradle.properties > >> +// holds Gradle properties, not env vars, so the two names aren't > interchangeable there. > >> +def resolveDcevmJavaExecutable = { > >> + String home = project.findProperty('dcevmHome') ?: > System.getenv('DCEVM_HOME') > >> + if (!home) { > >> + return null > >> + } > >> + String exeName = > org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'java.exe' : > 'java' > >> + File exe = new File(home, "bin/${exeName}") > >> + if (!exe.exists()) { > >> + // macOS JDK/JBR app bundles nest an extra Contents/Home -- > e.g. IntelliJ's bundled > >> + // JetBrains Runtime lives at "<IDE > install>/Contents/jbr/Contents/Home/bin/java", > >> + // one level deeper than the jbr folder itself (which is what > README/error-message > >> + // guidance points DCEVM_HOME/-PdcevmHome at). Fall back to > that layout before > >> + // giving up, so the documented "point at <IDE > install>/Contents/jbr" guidance > >> + // actually resolves on macOS instead of silently finding > nothing. > >> + exe = new File(home, "Contents/Home/bin/${exeName}") > >> + } > >> + return exe.exists() ? exe.absolutePath : null > >> +} > >> + > >> +// Convenience task: `./gradlew ofbizDev` starts OFBiz with hot-reload > enabled > >> +// (-Dofbiz.hotreload=true), plus the JVM flag DevReloadContainer's > self-attaching > >> +// HotSwapAgent needs (-Djdk.attach.allowAttachSelf=true). Registered > on the root > >> +// project, mirroring the classpath/mainClass wiring of the built-in > `ofbiz` command > >> +// task, so this plugin needs no change to the root build.gradle to be > usable. > >> +// > >> +// A DCEVM-patched JVM is required, not optional: stock-JDK hot-reload > used to be a > >> +// supported fallback, but a stock JVM's redefineClasses can't > hot-swap a new/removed > >> +// method or field anyway (see DevReloadContainer's class-level > javadoc), so silently > >> +// running in that degraded mode just defers the surprise to whenever > someone adds a > >> +// method. Simple by design: if DCEVM_HOME/-PdcevmHome resolves to a > real JVM, this task > >> +// just runs -- no other flag, no mode to pick. If it doesn't resolve, > running `ofbizDev` > >> +// fails immediately with setup instructions, before OFBiz attempts to > boot -- but the > >> +// failure has to be raised from a doFirst task action, not directly > in this > >> +// configuration block: this block runs whenever the project is > evaluated, which in this > >> +// build happens on every `./gradlew <anything>` invocation, not just > when `ofbizDev` > >> +// itself is requested (confirmed empirically -- even `./gradlew help` > realizes this > >> +// task, most likely because the Develocity plugin applied in > settings.gradle touches > >> +// every registered task for build-scan data). Throwing here > unconditionally would break > >> +// every Gradle command whenever DCEVM isn't set up, which is exactly > the "zero effect on > >> +// anyone not using it" guarantee this plugin promises everywhere else > (see README). > >> +// > >> +// JavaExec's `executable` property is pointed at the resolved DCEVM > JVM binary during > >> +// task configuration -- no toolchain is configured anywhere in this > project for that to > >> +// conflict with. One non-obvious wrinkle: `executable` has to be set > directly in the > >> +// task's configuration block, not from a task action (e.g. inside > doFirst). Doing it > >> +// from a task action fails with "Toolchain from executable property > does not match > >> +// toolchain from javaLauncher property" -- by the time a task action > runs, javaLauncher's > >> +// convention has already been finalized for execution, and overriding > executable at that > >> +// point conflicts with it. Setting it during configuration, before > that convention > >> +// finalizes, works cleanly. That's also *why* the DCEVM lookup itself > still has to happen > >> +// eagerly in this block (only the throw is deferred to doFirst): > `executable` can't be > >> +// set any later. > >> +rootProject.tasks.register('ofbizDev', JavaExec) { > >> + group = 'OFBiz Server Commands' > >> + description = 'Start OFBiz with hot-reload (requires a > DCEVM-patched JVM -- see README). Java source, ' + > >> + 'services.xml, and structural changes (new/removed methods > or fields, changed signatures) all ' + > >> + 'reload live with no restart. Point > -PdcevmHome=/path/to/jvm or set DCEVM_HOME to enable; fails ' + > >> + 'fast with setup instructions if neither resolves. > Optionally scope to specific components with ' + > >> + '-Photreload.components=compA,compB, or watch > build/classes/java/main too (for externally-produced ' + > >> + '.class files) with -Photreload.watchBuildOutput=true.' > >> + classpath = rootProject.files(hotReloadOutputDir(project)) + > rootProject.sourceSets.main.runtimeClasspath > >> + mainClass = rootProject.application.mainClass > >> + List<String> jvmArgsList = > rootProject.application.applicationDefaultJvmArgs + > commonHotReloadJvmArgs(project) > >> + String dcevmJava = resolveDcevmJavaExecutable() > >> + if (dcevmJava) { > >> + // Only wired up when actually needed: compiling everything > first just to fail in > >> + // doFirst afterwards would make the "no DCEVM" error slow to > reach on a cold build. > >> + dependsOn rootProject.tasks.named('classes') > >> + jvmArgsList = jvmArgsList + > ['-XX:+AllowEnhancedClassRedefinition'] > >> + logger.lifecycle("ofbizDev: DCEVM-patched JVM resolved at > ${dcevmJava}") > >> + executable = dcevmJava > >> + } else { > >> + // Deferred to a task action (doFirst) instead of thrown > directly here: this > >> + // configuration block runs whenever this project is > evaluated, which in this build > >> + // happens for every `./gradlew <anything>` invocation, not > just when `ofbizDev` > >> + // itself is requested (confirmed empirically -- even > `./gradlew help` realizes this > >> + // task's configuration block, most likely due to the > Develocity plugin applied in > >> + // settings.gradle touching every registered task for > build-scan data). Throwing here > >> + // directly would break every Gradle command whenever DCEVM > isn't set up, violating > >> + // this plugin's "zero effect on anyone not using it" > guarantee (see README). A > >> + // doFirst block, by contrast, only actually runs when > `ofbizDev` is the task Gradle > >> + // is executing. > >> + String attemptedHome = project.findProperty('dcevmHome') ?: > System.getenv('DCEVM_HOME') > >> + String detail = attemptedHome > >> + ? "DCEVM_HOME/-PdcevmHome is set to > '${attemptedHome}', but no 'bin/java' (or macOS " + > >> + "'Contents/Home/bin/java') was found there -- > double-check the path." > >> + : "Neither -PdcevmHome nor the DCEVM_HOME env var is > set." > >> + doFirst { > >> + throw new GradleException( > >> + "ofbizDev requires a DCEVM-patched JVM -- plain > stock-JDK hot-reload is not supported.\n\n" + > >> + "${detail}\n\n" + > >> + "To fix this:\n" + > >> + " 1. Get a DCEVM-patched JVM. The easiest source > is JetBrains Runtime (JBR), which " + > >> + "bundles DCEVM and ships with IntelliJ IDEA under > '<IDE install>/jbr' " + > >> + "('.../Contents/jbr' on macOS). Standalone DCEVM > builds work too.\n" + > >> + " 2. Point at it -- set the DCEVM_HOME env var > once (e.g. in your shell profile, so " + > >> + "every future run picks it up automatically) or > pass -PdcevmHome=/path/to/jvm each " + > >> + "time.\n\n" + > >> + "See plugins/devreload/README.md for the full > setup guide.") > >> + } > >> + } > >> + jvmArgs(jvmArgsList) > >> +} > >> + > >> +dependencyLocking { > >> + lockAllConfigurations() > >> +} > >> diff --git a/devreload/gradle.lockfile b/devreload/gradle.lockfile > >> new file mode 100644 > >> index 000000000..b9b85588a > >> --- /dev/null > >> +++ b/devreload/gradle.lockfile > >> @@ -0,0 +1,4 @@ > >> +# This is a Gradle generated file for dependency locking. > >> +# Manual edits can break the build and are not advised. > >> +# This file is expected to be part of source control. > >> +empty=pluginLibsCompile,pluginLibsCompileOnly,pluginLibsRuntime > >> diff --git a/devreload/ofbiz-component.xml > b/devreload/ofbiz-component.xml > >> new file mode 100644 > >> index 000000000..c9ecab1fb > >> --- /dev/null > >> +++ b/devreload/ofbiz-component.xml > >> @@ -0,0 +1,30 @@ > >> +<?xml version="1.0" encoding="UTF-8"?> > >> +<!-- > >> +Licensed to the Apache Software Foundation (ASF) under one > >> +or more contributor license agreements. See the NOTICE file > >> +distributed with this work for additional information > >> +regarding copyright ownership. The ASF licenses this file > >> +to you under the Apache License, Version 2.0 (the > >> +"License"); you may not use this file except in compliance > >> +with the License. You may obtain a copy of the License at > >> + > >> +http://www.apache.org/licenses/LICENSE-2.0 > >> + > >> +Unless required by applicable law or agreed to in writing, > >> +software distributed under the License is distributed on an > >> +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY > >> +KIND, either express or implied. See the License for the > >> +specific language governing permissions and limitations > >> +under the License. > >> +--> > >> + > >> +<ofbiz-component name="devreload" enabled="true" > >> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > >> + xsi:noNamespaceSchemaLocation=" > https://ofbiz.apache.org/dtds/ofbiz-component.xsd"> > >> + <resource-loader name="main" type="component"/> > >> + > >> + <!-- Development hot-reload container: a no-op unless started with > >> + -Dofbiz.hotreload=true, so it is safe to leave enabled in > every > >> + environment. See README.md for usage. --> > >> + <container name="dev-reload-container" loaders="main" > class="org.apache.ofbiz.devreload.DevReloadContainer"/> > >> +</ofbiz-component> > >> diff --git > a/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java > > >> > b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java > >> new file mode 100644 > >> index 000000000..1f9539cb2 > >> --- /dev/null > >> +++ > b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java > >> @@ -0,0 +1,860 @@ > >> > +/******************************************************************************* > >> + * Licensed to the Apache Software Foundation (ASF) under one > >> + * or more contributor license agreements. See the NOTICE file > >> + * distributed with this work for additional information > >> + * regarding copyright ownership. The ASF licenses this file > >> + * to you under the Apache License, Version 2.0 (the > >> + * "License"); you may not use this file except in compliance > >> + * with the License. You may obtain a copy of the License at > >> + * > >> + *http://www.apache.org/licenses/LICENSE-2.0 > >> + * > >> + * Unless required by applicable law or agreed to in writing, > >> + * software distributed under the License is distributed on an > >> + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY > >> + * KIND, either express or implied. See the License for the > >> + * specific language governing permissions and limitations > >> + * under the License. > >> + > *******************************************************************************/ > >> +package org.apache.ofbiz.devreload; > >> + > >> +import java.io.IOException; > >> +import java.lang.instrument.ClassDefinition; > >> +import java.lang.instrument.Instrumentation; > >> +import java.lang.management.ManagementFactory; > >> +import java.net.URI; > >> +import java.net.URISyntaxException; > >> +import java.net.URL; > >> +import java.nio.file.ClosedWatchServiceException; > >> +import java.nio.file.FileVisitResult; > >> +import java.nio.file.Files; > >> +import java.nio.file.Path; > >> +import java.nio.file.Paths; > >> +import java.nio.file.SimpleFileVisitor; > >> +import java.nio.file.StandardWatchEventKinds; > >> +import java.nio.file.WatchEvent; > >> +import java.nio.file.WatchKey; > >> +import java.nio.file.WatchService; > >> +import java.nio.file.attribute.BasicFileAttributes; > >> +import java.util.ArrayList; > >> +import java.util.Arrays; > >> +import java.util.HashSet; > >> +import java.util.List; > >> +import java.util.Set; > >> +import java.util.concurrent.Executors; > >> +import java.util.concurrent.RejectedExecutionException; > >> +import java.util.concurrent.ScheduledExecutorService; > >> +import java.util.concurrent.ScheduledFuture; > >> +import java.util.concurrent.TimeUnit; > >> +import java.util.function.Consumer; > >> +import java.util.stream.Collectors; > >> +import javax.tools.JavaCompiler; > >> +import javax.tools.StandardJavaFileManager; > >> +import javax.tools.StandardLocation; > >> +import javax.tools.ToolProvider; > >> + > >> +import org.apache.commons.io.FileUtils; > >> +import org.apache.ofbiz.base.component.ComponentConfig; > >> +import org.apache.ofbiz.base.config.GenericConfigException; > >> +import org.apache.ofbiz.base.container.Container; > >> +import org.apache.ofbiz.base.container.ContainerException; > >> +import org.apache.ofbiz.base.start.StartupCommand; > >> +import org.apache.ofbiz.base.util.Debug; > >> +import org.apache.ofbiz.base.util.cache.UtilCache; > >> + > >> +/** > >> + * Development-only container that watches Java sources/classes and > {@code services.xml} > >> + * files and applies changes to a running OFBiz instance without a > restart. > >> + * > >> + * <h2>Activation</h2> > >> + * Add {@code -Dofbiz.hotreload=true > -Djdk.attach.allowAttachSelf=true} to your JVM > >> + * arguments, then start OFBiz normally (or run {@code ./gradlew > ofbizDev}, provided by > >> + * this plugin, which sets both automatically — the only supported > command). That Gradle > >> + * task requires a DCEVM-patched JVM and fails fast with setup > instructions if none is > >> + * resolvable; see "Structural changes" below for why. > >> + * > >> + * <h2>How Java hot-reload works</h2> > >> + * <ol> > >> + * <li>At startup this container self-attaches {@link HotSwapAgent} > to the current JVM > >> + * via the Attach API, obtaining a live {@link Instrumentation} > instance — no > >> + * {@code -javaagent} flag needed.</li> > >> + * <li>A {@link WatchService} thread monitors every component's > {@code src/main/java} > >> + * directory. On save, changed {@code .java} files are compiled > in-process (a JDK, > >> + * not just a JRE, is required) into {@code > build/devreload/classes/} — a directory > >> + * private to this plugin, deliberately <em>not</em> Gradle's own > >> + * {@code build/classes/java/main/}. Writing into Gradle's > managed output would > >> + * leave its incremental-build cache unaware of the change: if a > source file is > >> + * later reverted to content Gradle already has a snapshot for > (e.g. via > >> + * {@code git checkout}) while no hot-reload session is > watching, Gradle's next > >> + * {@code compileJava} would see matching input and skip > recompiling, silently > >> + * leaving the stale hot-swapped {@code .class} file in place. > This plugin's output > >> + * directory is cleared and recreated on every {@link #init}, so > a fresh run never > >> + * inherits a previous session's bytecode, and is placed ahead > of Gradle's own > >> + * output on the classpath (see {@code build.gradle}) so it > always wins when a class > >> + * exists in both places. Running {@code ./gradlew -t classes} > externally in a > >> + * second terminal still works — the same WatchService also > monitors Gradle's own > >> + * output directory directly for externally-produced {@code > .class} files, when > >> + * {@code -Dofbiz.hotreload.watchBuildOutput=true}.</li> > >> + * <li>Changes are debounced for 300 ms so a single compile run is > handled as one batch. > >> + * Each changed class already loaded in the JVM is updated in > place via > >> + * {@link Instrumentation#redefineClasses}, the same mechanism > an IDE debugger uses > >> + * for HotSwap. Because the {@link Class} object's identity > never changes, every > >> + * existing reference to it — including caches inside {@code > JavaEventHandler} and > >> + * {@code StandardJavaEngine} — automatically executes the new > method bodies on the > >> + * next call. No framework code needs to know this plugin > exists.</li> > >> + * <li>Brand-new classes need no special handling at all: they > simply get loaded > >> + * normally, from this plugin's build output directory, the > first time something > >> + * references them.</li> > >> + * </ol> > >> + * > >> + * <h2>Structural changes</h2> > >> + * On a stock JVM, {@code redefineClasses} can only replace method > bodies and static > >> + * initializers of a class that is already loaded — adding/removing > methods or fields, > >> + * changing a method signature, or changing the class hierarchy still > requires a restart. > >> + * This is a hard JVM limitation, not something this plugin can work > around on a stock > >> + * JVM. Running on a DCEVM-patched JVM with {@code > -XX:+AllowEnhancedClassRedefinition} > >> + * lifts that restriction transparently: this class calls the exact > same > >> + * {@code redefineClasses} API either way, so structural changes just > work when that flag > >> + * is present. Because a stock JVM would silently defer that surprise > to whenever someone > >> + * adds a method, {@code ./gradlew ofbizDev}'s build.gradle task > treats a DCEVM-patched > >> + * JVM as required, not optional: it resolves one via {@code > -PdcevmHome}/ > >> + * {@code DCEVM_HOME}, launches with that flag automatically, and > fails fast with setup > >> + * instructions instead of starting OFBiz at all if none is > resolvable. Plain stock-JDK > >> + * operation is not a supported outcome of that command. > >> + * > >> + * <h2>How services.xml changes are handled</h2> > >> + * Every component's {@code servicedef/} directory is also watched; on > change, the > >> + * {@code service.ModelServiceMapByModel} {@link UtilCache} entry is > cleared directly, so > >> + * the new/edited definition is re-read on the next service call. > >> + * > >> + * <h2>Directory watch limits</h2> > >> + * The OS may refuse to watch a directory once a process-wide ceiling > is reached (most > >> + * commonly hit on macOS on a full checkout). This container does not > try to work around > >> + * that itself: a directory whose registration fails is simply left > unwatched, with a > >> + * warning naming it, rather than silently falling back to some slower > alternate > >> + * mechanism. Narrow the set of directories with {@code > -Dofbiz.hotreload.components} > >> + * (or {@code -Photreload.components=compA,compB} with the {@code > ofbizDev} Gradle task) > >> + * to fit under the ceiling — see this component's README for the full > guidance. > >> + * > >> + * <h2>Scope</h2> > >> + * This container is intentionally dev-only. It has no effect when the > system property is > >> + * absent, so it is safe to leave the registration in this component's > >> + * {@code ofbiz-component.xml} for all environments. > >> + */ > >> +public class DevReloadContainer implements Container { > >> + > >> + private static final String MODULE = > DevReloadContainer.class.getName(); > >> + private static final String SERVICE_MODEL_CACHE_NAME = > "service.ModelServiceMapByModel"; > >> + > >> + private String name; > >> + private WatchService watchService; > >> + private Thread watchThread; > >> + private ScheduledExecutorService debounceExecutor; > >> + private Instrumentation instrumentation; > >> + > >> + private final Debouncer<String> classReloadDebouncer = new > Debouncer<>(this::applyReload); > >> + private final Debouncer<Path> xmlReloadDebouncer = new > Debouncer<>(this::applyServiceXmlReload); > >> + private final Debouncer<Path> compileDebouncer = new > Debouncer<>(this::applyCompile); > >> + > >> + // Counts across > registerServicedefDirs()/registerSourceDirs()/registerAll(), so > >> + // start() can emit one aggregated warning instead of leaving > individual failures > >> + // scattered in the log where they're easy to miss. > >> + private int watchDirsAttempted = 0; > >> + private int watchDirsFailed = 0; > >> + > >> + /** Gradle's own compiled-output directory, {@code > build/classes/java/main}. Read-only > >> + * from this class's perspective: only ever watched (when {@code > watchBuildOutput} is > >> + * on) for externally-produced {@code .class} files, never > written to directly. */ > >> + private Path classesDir; > >> + > >> + /** > >> + * This plugin's own compiled-output directory, {@code > build/devreload/classes}, > >> + * deliberately separate from Gradle's {@link #classesDir}. > In-process compiles > >> + * (see {@link #applyCompile}) write here instead of into Gradle's > managed output, so > >> + * Gradle's incremental {@code compileJava} up-to-date check is > never confused by > >> + * writes it didn't make itself. Cleared and recreated fresh on > every {@link #init}, > >> + * and placed ahead of Gradle's output on the runtime classpath > (see > >> + * {@code plugins/devreload/build.gradle}) — so when a class > exists in both > >> + * directories, this one always wins (see {@link > #resolveClassFile}). > >> + */ > >> + private Path hotReloadOutputDir; > >> + > >> + // Populated in start() before the watch thread launches; > read-only after that. > >> + private final Set<Path> servicedefDirs = new HashSet<>(); > >> + private final Set<Path> sourceRootDirs = new HashSet<>(); > >> + > >> + /** > >> + * Component names to watch, from {@code > -Dofbiz.hotreload.components}; {@code null} > >> + * means watch every component. Set this property to a > comma-separated list of > >> + * component names to keep the total number of watched directories > under the OS's > >> + * per-process ceiling on a large checkout. > >> + */ > >> + private Set<String> allowedComponents; > >> + > >> + /** > >> + * Whether {@link #classesDir} ({@code build/classes/java/main}) > itself is watched, from > >> + * {@code -Dofbiz.hotreload.watchBuildOutput}; defaults to {@code > false}. This tree mirrors > >> + * every component's source tree and, unlike {@link > #sourceRootDirs}, is <em>not</em> > >> + * narrowed by {@link #allowedComponents} (compiled output isn't > organized per component), > >> + * so on a full checkout it roughly doubles the total directories > watched. It only exists to > >> + * pick up externally-produced {@code .class} files (e.g. running > {@code ./gradlew -t > >> + * classes} in a second terminal); in-process compiles hot-swap > directly and never need it. > >> + * Off by default so unscoped runs need meaningfully fewer real > watch registrations. > >> + */ > >> + private boolean watchBuildOutput; > >> + > >> + @Override > >> + public void init(List<StartupCommand> ofbizCommands, String name, > String configFile) throws ContainerException { > >> + this.name = name; > >> + > >> + if > (!"true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload"))) { > >> + Debug.logInfo("DevReloadContainer is disabled. Use > -Dofbiz.hotreload=true to enable.", MODULE); > >> + return; > >> + } > >> + > >> + parseHotReloadProperties(); > >> + attachHotSwapAgent(); > >> + > >> + if (!prepareClassesDir()) { > >> + return; > >> + } > >> + if (!prepareHotReloadOutputDir()) { > >> + return; > >> + } > >> + createWatchService(); > >> + registerBuildOutputWatchIfEnabled(); > >> + startDebounceExecutor(); > >> + > >> + Debug.logInfo("DevReloadContainer ready — compiled output at " > + hotReloadOutputDir.toAbsolutePath(), MODULE); > >> + } > >> + > >> + /** > >> + * Reads {@code -Dofbiz.hotreload.components} and {@code > -Dofbiz.hotreload.watchBuildOutput}, > >> + * populating {@link #allowedComponents} and {@link > #watchBuildOutput}. > >> + */ > >> + private void parseHotReloadProperties() { > >> + String componentsProperty = > System.getProperty("ofbiz.hotreload.components"); > >> + if (componentsProperty != null && > !componentsProperty.isBlank()) { > >> + allowedComponents = > Arrays.stream(componentsProperty.split(",")) > >> + .map(String::trim) > >> + .filter(s -> !s.isEmpty()) > >> + .collect(Collectors.toSet()); > >> + Debug.logInfo("Hot-reload: scoped to components " + > allowedComponents > >> + + " (set via -Dofbiz.hotreload.components)", > MODULE); > >> + } > >> + > >> + watchBuildOutput = > "true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload.watchBuildOutput")); > >> + } > >> + > >> + /** > >> + * Self-attaches {@link HotSwapAgent} so Java class redefinition > is available. Failure > >> + * here is non-fatal and never aborts {@link #init}: Java hot-swap > is simply disabled > >> + * while {@code services.xml} auto-reload still works. > >> + */ > >> + private void attachHotSwapAgent() { > >> + try { > >> + instrumentation = HotSwapAgent.install(); > >> + Debug.logInfo("Hot-reload: self-attached HotSwapAgent — > Java class redefinition is available.", MODULE); > >> + if (enhancedRedefinitionRequested()) { > >> + Debug.logInfo("Hot-reload: > -XX:+AllowEnhancedClassRedefinition detected — running on a JVM " > >> + + "(e.g. JetBrains Runtime) that can also > hot-swap structural changes (added/removed " > >> + + "methods or fields, changed signatures), not > just method bodies.", MODULE); > >> + } else { > >> + Debug.logWarning("Hot-reload: structural changes > (added/removed methods or fields, changed " > >> + + "signatures) will require a restart on this > JVM. './gradlew ofbizDev' requires a " > >> + + "DCEVM-patched JVM and refuses to start > without one, so you're most likely seeing " > >> + + "this because hot-reload was activated some > other way on a stock JDK. See " > >> + + "plugins/devreload/README.md to set up > DCEVM_HOME.", MODULE); > >> + } > >> + } catch (Exception e) { > >> + Debug.logWarning("Hot-reload: could not self-attach > HotSwapAgent (" + e.getMessage() > >> + + "). Add -Djdk.attach.allowAttachSelf=true to JVM > args. " > >> + + "Java class changes will require a restart; > services.xml auto-reload still works.", MODULE); > >> + } > >> + } > >> + > >> + /** Sets {@link #classesDir}. Returns {@code false} (after > logging) if it doesn't exist yet. */ > >> + private boolean prepareClassesDir() { > >> + classesDir = Paths.get("build/classes/java/main"); > >> + if (!Files.exists(classesDir)) { > >> + Debug.logWarning("Hot-reload: classes directory not found > at " + classesDir.toAbsolutePath() > >> + + ". Run './gradlew classes' first, then > restart.", MODULE); > >> + return false; > >> + } > >> + return true; > >> + } > >> + > >> + /** > >> + * Sets {@link #hotReloadOutputDir} and clears/recreates it. > Returns {@code false} (after > >> + * logging) if the directory could not be prepared -- without a > writable output directory, > >> + * in-process compilation cannot work at all, so the whole > container is disabled up front > >> + * instead of continuing to a misleading "ready" log. > >> + */ > >> + private boolean prepareHotReloadOutputDir() { > >> + // build.gradle passes this down as > -Dofbiz.hotreload.outputDir, computed from the > >> + // same value it prepends to the classpath, so the path exists > in exactly one place > >> + // rather than being hardcoded independently here too. The > literal default below is > >> + // only a fallback for the (unsupported) case of starting this > container without > >> + // going through the ofbizDev Gradle task. > >> + hotReloadOutputDir = > Paths.get(System.getProperty("ofbiz.hotreload.outputDir", > "build/devreload/classes")); > >> + try { > >> + FileUtils.deleteDirectory(hotReloadOutputDir.toFile()); > >> + Files.createDirectories(hotReloadOutputDir); > >> + return true; > >> + } catch (IOException e) { > >> + Debug.logWarning("Hot-reload: could not prepare this > plugin's own output directory " > >> + + hotReloadOutputDir.toAbsolutePath() + ": " + > e.getMessage(), MODULE); > >> + return false; > >> + } > >> + } > >> + > >> + /** Creates {@link #watchService} on {@link #classesDir}'s > filesystem. */ > >> + private void createWatchService() throws ContainerException { > >> + try { > >> + watchService = > classesDir.getFileSystem().newWatchService(); > >> + } catch (IOException e) { > >> + throw new ContainerException("DevReloadContainer: failed > to initialise WatchService", e); > >> + } > >> + } > >> + > >> + /** > >> + * Registers {@link #classesDir} with the WatchService when {@link > #watchBuildOutput} is > >> + * on, so externally-produced {@code .class} files (e.g. {@code > ./gradlew -t classes} in > >> + * a second terminal) are picked up too. > >> + */ > >> + private void registerBuildOutputWatchIfEnabled() { > >> + if (watchBuildOutput) { > >> + try { > >> + registerAll(classesDir); > >> + } catch (IOException e) { > >> + // registerAll() already logs a warning and skips > individual directories > >> + // that fail to register; reaching here means > something more fundamental > >> + // broke walking the tree at all (e.g. can't even list > classesDir). > >> + Debug.logWarning("Hot-reload: could not fully walk " + > classesDir + ": " + e.getMessage(), MODULE); > >> + } > >> + Debug.logInfo("Hot-reload: watching compiled-output > directory " + classesDir.toAbsolutePath() > >> + + " for externally-produced .class files (set via > -Dofbiz.hotreload.watchBuildOutput=true).", > >> + MODULE); > >> + } else { > >> + Debug.logInfo("Hot-reload: not watching " + > classesDir.toAbsolutePath() + " (this tree isn't " > >> + + "narrowed by -Dofbiz.hotreload.components and > roughly doubles the total directories " > >> + + "watched). In-process edits still hot-swap > normally; running './gradlew -t classes' in a " > >> + + "second terminal will not be picked up unless > you set " > >> + + "-Dofbiz.hotreload.watchBuildOutput=true (or > -Photreload.watchBuildOutput=true with the " > >> + + "ofbizDev Gradle task).", MODULE); > >> + } > >> + } > >> + > >> + /** Starts the daemon executor backing every {@link Debouncer}. */ > >> + private void startDebounceExecutor() { > >> + debounceExecutor = > Executors.newSingleThreadScheduledExecutor(r -> { > >> + Thread t = new Thread(r, "ofbiz-hot-reload-debouncer"); > >> + t.setDaemon(true); > >> + return t; > >> + }); > >> + } > >> + > >> + /** > >> + * Picks whichever of this plugin's own compiled output ({@code > overlay}, under > >> + * {@link #hotReloadOutputDir}) or Gradle's ({@code fallback}, > under {@link #classesDir}) > >> + * is the right one to redefine from. {@code overlay} always wins > when it exists, > >> + * matching its position ahead of {@code fallback} on the runtime > classpath (see > >> + * {@code build.gradle}) — a class compiled here is always the one > a caller would load. > >> + * Returns {@code null} if neither exists. > >> + */ > >> + private Path resolveClassFile(Path overlay, Path fallback) { > >> + if (Files.exists(overlay)) { > >> + return overlay; > >> + } > >> + if (Files.exists(fallback)) { > >> + return fallback; > >> + } > >> + return null; > >> + } > >> + > >> + @Override > >> + public boolean start() throws ContainerException { > >> + if (watchService == null) { > >> + return true; // disabled > >> + } > >> + registerServicedefDirs(); > >> + registerSourceDirs(); > >> + if (watchDirsFailed > 0) { > >> + Debug.logWarning("Hot-reload: " + watchDirsFailed + " of " > + watchDirsAttempted + " directory watch " > >> + + "registrations hit file-descriptor/watch > exhaustion (see warnings above for which ones) " > >> + + "and are NOT being watched — changes there will > not hot-reload until you restart. Scope " > >> + + "hot-reload to just the components you're > working on with " > >> + + "-Dofbiz.hotreload.components=compA,compB (or > -Photreload.components=compA,compB with " > >> + + "the ofbizDev Gradle task) to fit under the OS > watch limit.", MODULE); > >> + } > >> + watchThread = new Thread(this::watchLoop, > "ofbiz-hot-reload-watcher"); > >> + watchThread.setDaemon(true); > >> + watchThread.start(); > >> + Debug.logInfo("DevReloadContainer started. Edit any Java or > services.xml file and changes go live without a restart.", MODULE); > >> + return true; > >> + } > >> + > >> + /** > >> + * Registers every directory that contains a component > service-definition XML file > >> + * (type="model") with the WatchService so that edits to those > files are detected. > >> + * Called once from {@link #start()}, before the watch thread > launches. > >> + */ > >> + private void registerServicedefDirs() { > >> + for (ComponentConfig.ServiceResourceInfo sri : > ComponentConfig.getAllServiceResourceInfos("model")) { > >> + if (allowedComponents != null && > !allowedComponents.contains(sri.getComponentConfig().getComponentName())) { > >> + continue; > >> + } > >> + try { > >> + URL url = sri.createResourceHandler().getURL(); > >> + if (!"file".equals(url.getProtocol())) { > >> + continue; // skip non-filesystem resources > (classpath jars, etc.) > >> + } > >> + Path dir = Paths.get(new > URI(url.toString())).getParent(); > >> + if (dir != null && Files.isDirectory(dir) && > servicedefDirs.add(dir)) { > >> + watchDirsAttempted++; > >> + try { > >> + dir.register(watchService, > >> + StandardWatchEventKinds.ENTRY_CREATE, > >> + StandardWatchEventKinds.ENTRY_MODIFY); > >> + Debug.logInfo("Hot-reload: watching servicedef > directory " + dir, MODULE); > >> + } catch (IOException e) { > >> + warnUnwatched(dir, e); > >> + } > >> + } > >> + } catch (GenericConfigException | URISyntaxException e) { > >> + Debug.logWarning("Hot-reload: could not register > servicedef dir for " > >> + + sri.getLocation() + ": " + e.getMessage(), > MODULE); > >> + } catch (Throwable t) { > >> + // Defensive: a single component's servicedef > registration must not be able > >> + // to abort the loop and leave every subsequent > component's servicedef > >> + // directory unwatched. > >> + Debug.logError(t, "Hot-reload: unexpected error > registering servicedef dir for " > >> + + sri.getLocation(), MODULE); > >> + } > >> + } > >> + } > >> + > >> + /** > >> + * Registers every component's {@code src/main/java} directory > with the WatchService > >> + * so that saving a {@code .java} file triggers in-process > compilation via > >> + * {@link ToolProvider#getSystemJavaCompiler()}. Falls back > gracefully when running > >> + * on a JRE (compiler unavailable) — source watching is simply > skipped. > >> + */ > >> + private void registerSourceDirs() { > >> + if (ToolProvider.getSystemJavaCompiler() == null) { > >> + Debug.logWarning("Hot-reload: javax.tools.JavaCompiler not > available (JRE, not JDK?). " > >> + + "Java source auto-compilation disabled — use > './gradlew -t classes' in a second terminal.", MODULE); > >> + return; > >> + } > >> + for (ComponentConfig cc : ComponentConfig.getAllComponents()) { > >> + if (cc.rootLocation() == null) { > >> + continue; > >> + } > >> + if (allowedComponents != null && > !allowedComponents.contains(cc.getComponentName())) { > >> + continue; > >> + } > >> + Path srcDir = cc.rootLocation().resolve("src/main/java"); > >> + if (Files.isDirectory(srcDir) && > sourceRootDirs.add(srcDir)) { > >> + try { > >> + registerAll(srcDir); > >> + Debug.logInfo("Hot-reload: watching source > directory " + srcDir, MODULE); > >> + } catch (IOException e) { > >> + // registerAll() already logs a warning and skips > individual directories > >> + // that fail to register; reaching here means > something more fundamental > >> + // broke walking the tree at all (e.g. can't list > srcDir). > >> + Debug.logWarning("Hot-reload: could not walk > source dir " + srcDir + ": " + e.getMessage(), MODULE); > >> + } > >> + } > >> + } > >> + if (!sourceRootDirs.isEmpty()) { > >> + Debug.logInfo("Hot-reload: Java source auto-compilation > active — save a .java file and it reloads automatically.", MODULE); > >> + } > >> + } > >> + > >> + @Override > >> + public void stop() throws ContainerException { > >> + if (debounceExecutor != null) { > >> + debounceExecutor.shutdownNow(); > >> + } > >> + if (watchService != null) { > >> + try { > >> + watchService.close(); > >> + } catch (IOException e) { > >> + Debug.logError(e, "DevReloadContainer: error closing > WatchService", MODULE); > >> + } > >> + } > >> + if (watchThread != null) { > >> + watchThread.interrupt(); > >> + } > >> + } > >> + > >> + @Override > >> + public String getName() { > >> + return name; > >> + } > >> + > >> + // > ------------------------------------------------------------------------- > >> + // Watch loop > >> + // > ------------------------------------------------------------------------- > >> + > >> + private void watchLoop() { > >> + while (!Thread.currentThread().isInterrupted()) { > >> + WatchKey key; > >> + try { > >> + key = watchService.take(); > >> + } catch (InterruptedException e) { > >> + Thread.currentThread().interrupt(); > >> + break; > >> + } catch (ClosedWatchServiceException e) { > >> + break; > >> + } > >> + > >> + Path dir = (Path) key.watchable(); > >> + for (WatchEvent<?> event : key.pollEvents()) { > >> + WatchEvent.Kind<?> kind = event.kind(); > >> + if (kind == StandardWatchEventKinds.OVERFLOW) { > >> + continue; > >> + } > >> + > >> + @SuppressWarnings("unchecked") > >> + Path changed = dir.resolve(((WatchEvent<Path>) > event).context()); > >> + > >> + if (kind == StandardWatchEventKinds.ENTRY_CREATE && > Files.isDirectory(changed)) { > >> + // New package directory created during > compilation — register it. > >> + try { > >> + registerAll(changed); > >> + } catch (IOException e) { > >> + Debug.logError(e, "DevReloadContainer: failed > to register new directory: " + changed, MODULE); > >> + } > >> + } else if ((kind == > StandardWatchEventKinds.ENTRY_CREATE || kind == > StandardWatchEventKinds.ENTRY_MODIFY) > >> + && changed.toString().endsWith(".class")) { > >> + // Only react to written/updated class files. > Ignore ENTRY_DELETE so > >> + // that removing a source file (and its .class > output) does not cause > >> + // a redefinition attempt against a now-missing > file. > >> + reloadClassFile(classesDir, changed); > >> + } else if ((kind == > StandardWatchEventKinds.ENTRY_CREATE || kind == > StandardWatchEventKinds.ENTRY_MODIFY) > >> + && changed.toString().endsWith(".xml") > >> + && > servicedefDirs.stream().anyMatch(dir::startsWith)) { > >> + xmlReloadDebouncer.add(changed); > >> + } else if ((kind == > StandardWatchEventKinds.ENTRY_CREATE || kind == > StandardWatchEventKinds.ENTRY_MODIFY) > >> + && changed.toString().endsWith(".java") > >> + && > sourceRootDirs.stream().anyMatch(dir::startsWith)) { > >> + compileDebouncer.add(changed); > >> + } > >> + } > >> + if (!key.reset()) { > >> + Debug.logWarning("Hot-reload: watch key became invalid > (directory deleted?): " > >> + + key.watchable() + ". WatchService will no > longer detect changes in that directory.", MODULE); > >> + } > >> + } > >> + } > >> + > >> + // > ------------------------------------------------------------------------- > >> + // Debounced batching > >> + // > ------------------------------------------------------------------------- > >> + > >> + /** > >> + * Coalesces rapid-fire change notifications into one action, so a > single compile run > >> + * that touches many files (e.g. one with inner/anonymous classes, > or a Gradle build > >> + * writing several {@code .class} files at once) is handled as a > single batch instead > >> + * of one action per file. Shared by all three change pipelines > (class reload, > >> + * {@code services.xml} reload, Java compile) instead of each > hand-rolling its own > >> + * pending-set/cancel/reschedule bookkeeping. > >> + */ > >> + private final class Debouncer<T> { > >> + private final Set<T> pending = new HashSet<>(); > >> + private final Consumer<Set<T>> action; > >> + private ScheduledFuture<?> scheduled; > >> + > >> + Debouncer(Consumer<Set<T>> action) { > >> + this.action = action; > >> + } > >> + > >> + synchronized void add(T item) { > >> + pending.add(item); > >> + if (scheduled != null) { > >> + scheduled.cancel(false); > >> + } > >> + try { > >> + // Wait 300 ms after the last change so a burst of > related changes (e.g. a > >> + // single Gradle compile run writing multiple .class > files) is handled as > >> + // one batch instead of one action per file. > >> + scheduled = debounceExecutor.schedule(this::fire, 300, > TimeUnit.MILLISECONDS); > >> + } catch (RejectedExecutionException e) { > >> + // Container is shutting down; pending changes will > not be applied. > >> + } > >> + } > >> + > >> + private synchronized void fire() { > >> + if (pending.isEmpty()) { > >> + return; > >> + } > >> + Set<T> batch = new HashSet<>(pending); > >> + pending.clear(); > >> + action.accept(batch); > >> + } > >> + } > >> + > >> + /** Resolves {@code changed} to a class name relative to {@code > baseDir} and, if valid, queues it for reload. */ > >> + private void reloadClassFile(Path baseDir, Path changed) { > >> + String className = toClassName(baseDir, changed); > >> + if (className != null) { > >> + classReloadDebouncer.add(className); > >> + } > >> + } > >> + > >> + private void applyReload(Set<String> batch) { > >> + Debug.logInfo("Hot-reload: detected changes in " + batch, > MODULE); > >> + > >> + if (instrumentation == null) { > >> + Debug.logWarning("Hot-reload: HotSwapAgent not attached — > " + batch > >> + + " compiled but not applied to the running JVM. > Restart to pick it up.", MODULE); > >> + return; > >> + } > >> + > >> + List<ClassDefinition> defs = new ArrayList<>(); > >> + for (String className : batch) { > >> + Path relative = Paths.get(className.replace('.', '/') + > ".class"); > >> + Path overlayFile = hotReloadOutputDir.resolve(relative); > >> + Path gradleFile = classesDir.resolve(relative); > >> + Path classFile = resolveClassFile(overlayFile, gradleFile); > >> + if (classFile == null) { > >> + Debug.logWarning("Hot-reload: detected a change for " > + className > >> + + " but could not find its compiled output in > either " > >> + + hotReloadOutputDir.toAbsolutePath() + " or " > + classesDir.toAbsolutePath(), MODULE); > >> + continue; > >> + } > >> + try { > >> + Class<?> loaded = findLoadedClass(className); > >> + if (loaded == null) { > >> + // Never loaded yet in this JVM — nothing to > redefine. It will simply > >> + // load fresh, with the new bytecode, the first > time something > >> + // references it, from whichever directory > resolveClassFile() would > >> + // pick (overlay first on the classpath too, see > build.gradle). > >> + continue; > >> + } > >> + defs.add(new ClassDefinition(loaded, > Files.readAllBytes(classFile))); > >> + } catch (IOException e) { > >> + Debug.logError(e, "Hot-reload: failed to read class > file for " + className, MODULE); > >> + } > >> + } > >> + > >> + if (defs.isEmpty()) { > >> + Debug.logInfo("Hot-reload: nothing already loaded to > redefine for " + batch, MODULE); > >> + return; > >> + } > >> + > >> + try { > >> + instrumentation.redefineClasses(defs.toArray(new > ClassDefinition[0])); > >> + // Clear service definition cache so newly added service > methods are discovered. > >> + // We deliberately do NOT clear webapp.Controller caches > here — controller.xml > >> + // has not changed, only .class files have, and clearing > those caches triggers > >> + // Groovy re-compilation of screen expressions which can > fail unexpectedly. > >> + UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME); > >> + Debug.logInfo("Hot-reload complete for: " + batch, MODULE); > >> + } catch (UnsupportedOperationException e) { > >> + // ./gradlew ofbizDev only ever runs on a DCEVM-patched > JVM (see build.gradle), > >> + // which already lifts the plain-JVM restriction to method > bodies only, so > >> + // add/remove method-or-field and signature changes > normally succeed here. This > >> + // still fires for the narrower set of changes DCEVM > itself can't apply either > >> + // (e.g. a changed class hierarchy) -- the same remaining > limit an IDE debugger's > >> + // HotSwap has even on a capable JVM. > >> + Debug.logWarning("Hot-reload: " + batch + " contains a > structural change (added/removed " > >> + + "method or field, changed signature, changed > hierarchy) that the JVM cannot " > >> + + "hot-swap. Restart OFBiz to pick it up. (" + > e.getMessage() + ")", MODULE); > >> + } catch (Throwable e) { > >> + Debug.logError(e, "Hot-reload failed for " + batch, > MODULE); > >> + } > >> + } > >> + > >> + /** > >> + * Best-effort detection of whether this JVM was launched with > >> + * {@code -XX:+AllowEnhancedClassRedefinition} (e.g. a JetBrains > Runtime), which is > >> + * what allows {@link Instrumentation#redefineClasses} to also > apply structural > >> + * changes instead of just method bodies. Purely informational — > the actual > >> + * capability is exercised (and, if absent, reported) when a > redefinition is > >> + * attempted in {@link #applyReload}. > >> + */ > >> + private static boolean enhancedRedefinitionRequested() { > >> + return > ManagementFactory.getRuntimeMXBean().getInputArguments().stream() > >> + .anyMatch(arg -> > arg.contains("AllowEnhancedClassRedefinition")); > >> + } > >> + > >> + /** Searches classes already loaded in the JVM for one matching > {@code className}. */ > >> + private Class<?> findLoadedClass(String className) { > >> + for (Class<?> c : instrumentation.getAllLoadedClasses()) { > >> + if (c.getName().equals(className)) { > >> + return c; > >> + } > >> + } > >> + return null; > >> + } > >> + > >> + private void applyServiceXmlReload(Set<Path> batch) { > >> + Debug.logInfo("Hot-reload: service XML changed " + batch + " — > clearing service model cache", MODULE); > >> + try { > >> + UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME); > >> + Debug.logInfo("Hot-reload: service model cache cleared; > definitions will be re-read on next service call", MODULE); > >> + } catch (Throwable e) { > >> + Debug.logError(e, "Hot-reload: failed to clear service > model cache", MODULE); > >> + } > >> + } > >> + > >> + private void applyCompile(Set<Path> batch) { > >> + Debug.logInfo("Hot-reload: compiling " + batch, MODULE); > >> + try { > >> + JavaCompiler compiler = > ToolProvider.getSystemJavaCompiler(); > >> + if (compiler == null) { > >> + return; > >> + } > >> + try (StandardJavaFileManager fm = > compiler.getStandardFileManager(null, null, null)) { > >> + fm.setLocation(StandardLocation.CLASS_OUTPUT, > >> + List.of(hotReloadOutputDir.toAbsolutePath().toFile())); > >> + // Reuse the running JVM's classpath — it already > contains all OFBiz jars. > >> + List<String> options = Arrays.asList("-cp", > System.getProperty("java.class.path"), "-proc:none"); > >> + var units = fm.getJavaFileObjectsFromPaths(batch); > >> + boolean ok = compiler.getTask(null, fm, null, options, > null, units).call(); > >> + if (ok) { > >> + Debug.logInfo("Hot-reload: compilation > successful", MODULE); > >> + > >> + // Collect all .class files produced by this > compilation round. > >> + // Each source file can produce multiple .class > files when it contains > >> + // inner or anonymous classes (e.g. Foo$Bar.class, > Foo$1.class). > >> + // All of them must be redefined too — otherwise > the inner class still > >> + // resolves through its stale, previously-loaded > bytecode. > >> + for (Path src : batch) { > >> + Path cf = sourceToClassFile(src); // relative > path for the outer class > >> + if (cf == null) { > >> + continue; > >> + } > >> + String outerName = > cf.getFileName().toString().replace(".class", ""); > >> + Path absOutputDir = > cf.toAbsolutePath().getParent(); > >> + try (var dirStream = Files.list(absOutputDir)) > { > >> + dirStream.filter(absFile -> { > >> + String fn = > absFile.getFileName().toString(); > >> + // Match Foo.class and Foo$Inner.class > / Foo$1.class > >> + return fn.endsWith(".class") > >> + && (fn.equals(outerName + > ".class") > >> + || > fn.startsWith(outerName + "$")); > >> + }).forEach(absFile -> { > >> + // Convert absolute output path back > to a relative path that > >> + // is rooted at CWD (same type as > hotReloadOutputDir) so that > >> + // toClassName(hotReloadOutputDir, > relPath) — which calls > >> + // relativize — does not throw > IllegalArgumentException. > >> + Path rel = hotReloadOutputDir.resolve( > >> + hotReloadOutputDir.toAbsolutePath().relativize(absFile)); > >> + reloadClassFile(hotReloadOutputDir, rel); > >> + }); > >> + } catch (IOException e) { > >> + // Output dir unreadable; fall back to the > outer class only. > >> + reloadClassFile(hotReloadOutputDir, cf); > >> + } > >> + } > >> + > >> + // Re-register class directories so external > compilations (./gradlew classes > >> + // run by a developer in a separate terminal) > still reach the class watcher. > >> + // Only relevant if that watch is enabled in the > first place (watchBuildOutput); > >> + // otherwise there is nothing registered under > classesDir to refresh. > >> + // Catch Exception (not just IOException) because > ClosedWatchServiceException > >> + // extends IllegalStateException, which is a > RuntimeException — it can be > >> + // thrown here if OFBiz is shutting down while a > compile finishes. > >> + if (watchBuildOutput) { > >> + try { > >> + registerAll(classesDir); > >> + } catch (Exception e) { > >> + Debug.logWarning("Hot-reload: could not > re-register class dirs: " + e.getMessage(), MODULE); > >> + } > >> + } > >> + } else { > >> + Debug.logWarning("Hot-reload: compilation failed — > fix the error and save again", MODULE); > >> + } > >> + } > >> + } catch (Throwable e) { > >> + Debug.logError(e, "Hot-reload: compilation error", MODULE); > >> + } > >> + } > >> + > >> + /** > >> + * Maps a {@code .java} source file to the corresponding {@code > .class} output file > >> + * under {@link #hotReloadOutputDir}. Returns {@code null} if the > source file is not > >> + * under any registered source root. > >> + */ > >> + private Path sourceToClassFile(Path sourceFile) { > >> + for (Path srcRoot : sourceRootDirs) { > >> + if (sourceFile.startsWith(srcRoot)) { > >> + Path relative = srcRoot.relativize(sourceFile); > >> + String name = relative.toString(); > >> + if (name.endsWith(".java")) { > >> + String classRelative = name.substring(0, > name.length() - ".java".length()) + ".class"; > >> + return hotReloadOutputDir.resolve(classRelative); > >> + } > >> + } > >> + } > >> + return null; > >> + } > >> + > >> + // > ------------------------------------------------------------------------- > >> + // Helpers > >> + // > ------------------------------------------------------------------------- > >> + > >> + /** > >> + * Recursively registers every directory under {@code start} with > the WatchService. > >> + * A directory whose registration fails (e.g. watch/descriptor > exhaustion) is logged > >> + * and left unwatched via {@link #warnUnwatched} instead of > aborting the whole walk, > >> + * so one overloaded directory never leaves the rest of the tree > unwatched. > >> + */ > >> + private void registerAll(Path start) throws IOException { > >> + Files.walkFileTree(start, new SimpleFileVisitor<Path>() { > >> + @Override > >> + public FileVisitResult preVisitDirectory(Path dir, > BasicFileAttributes attrs) { > >> + watchDirsAttempted++; > >> + try { > >> + dir.register(watchService, > >> + StandardWatchEventKinds.ENTRY_CREATE, > >> + StandardWatchEventKinds.ENTRY_MODIFY); > >> + } catch (IOException e) { > >> + warnUnwatched(dir, e); > >> + } catch (Throwable t) { > >> + // register() itself should only throw > IOException, but nothing here is > >> + // worth crashing the whole startup over. > >> + Debug.logError(t, "Hot-reload: unexpected error > registering watch for " + dir > >> + + " -- this directory will not be > watched.", MODULE); > >> + } > >> + return FileVisitResult.CONTINUE; > >> + } > >> + }); > >> + } > >> + > >> + /** > >> + * Records that {@code dir} could not get a WatchService > registration (most commonly > >> + * the OS's per-process watch ceiling, e.g. macOS's > kqueue-per-directory cost) and > >> + * logs why. The directory is simply left unwatched: changes there > require a restart > >> + * (or {@code -Dofbiz.hotreload.components}/{@code > -Photreload.components} to narrow > >> + * the watched set below the ceiling) rather than falling back to > some slower > >> + * alternate mechanism. > >> + */ > >> + private void warnUnwatched(Path dir, IOException cause) { > >> + watchDirsFailed++; > >> + Debug.logWarning("Hot-reload: could not watch " + dir + " (" + > cause.getMessage() + ") -- changes " > >> + + "there will not be picked up until OFBiz is > restarted. Narrow the watched set with " > >> + + "-Dofbiz.hotreload.components=compA,compB (or > -Photreload.components=compA,compB with the " > >> + + "ofbizDev Gradle task) to fit under the OS watch > limit.", MODULE); > >> + } > >> + > >> + /** > >> + * Converts a {@code .class} file path relative to {@code baseDir} > into a > >> + * binary class name. > >> + * > >> + * <p>Example: {@code com/example/Foo.class} → {@code > com.example.Foo} > >> + */ > >> + private static String toClassName(Path baseDir, Path classFile) { > >> + Path relative = baseDir.relativize(classFile); > >> + String s = relative.toString(); > >> + if (!s.endsWith(".class")) { > >> + return null; > >> + } > >> + // Normalise path separator to '.' and strip the '.class' > suffix > >> + return s.substring(0, s.length() - ".class".length()) > >> + .replace(classFile.getFileSystem().getSeparator(), "."); > >> + } > >> +} > >> diff --git > a/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java > >> b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java > >> new file mode 100644 > >> index 000000000..f3542a5bf > >> --- /dev/null > >> +++ > b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java > >> @@ -0,0 +1,109 @@ > >> > +/******************************************************************************* > >> + * Licensed to the Apache Software Foundation (ASF) under one > >> + * or more contributor license agreements. See the NOTICE file > >> + * distributed with this work for additional information > >> + * regarding copyright ownership. The ASF licenses this file > >> + * to you under the Apache License, Version 2.0 (the > >> + * "License"); you may not use this file except in compliance > >> + * with the License. You may obtain a copy of the License at > >> + * > >> + *http://www.apache.org/licenses/LICENSE-2.0 > >> + * > >> + * Unless required by applicable law or agreed to in writing, > >> + * software distributed under the License is distributed on an > >> + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY > >> + * KIND, either express or implied. See the License for the > >> + * specific language governing permissions and limitations > >> + * under the License. > >> + > *******************************************************************************/ > >> +package org.apache.ofbiz.devreload; > >> + > >> +import java.io.IOException; > >> +import java.io.InputStream; > >> +import java.io.OutputStream; > >> +import java.lang.instrument.Instrumentation; > >> +import java.nio.file.Files; > >> +import java.nio.file.Path; > >> +import java.util.jar.Attributes; > >> +import java.util.jar.JarOutputStream; > >> +import java.util.jar.Manifest; > >> +import java.util.zip.ZipEntry; > >> + > >> +import com.sun.tools.attach.VirtualMachine; > >> + > >> +/** > >> + * Self-attaching Java agent that gives {@link DevReloadContainer} > access to > >> + * {@link Instrumentation#redefineClasses}, the same JVM mechanism an > IDE debugger uses > >> + * for HotSwap — it replaces the bytecode of an already-loaded {@link > Class} object in > >> + * place, so every existing reference to it (including cached {@code > Class} objects in > >> + * framework classes like {@code JavaEventHandler}) picks up the new > method bodies on the > >> + * very next call. > >> + * > >> + * <p>The agent jar is built on the fly from this class's own compiled > bytecode and > >> + * attached to the current process via the JDK Attach API — no {@code > -javaagent} JVM > >> + * flag, and no framework code changes. This class and {@link > DevReloadContainer} are the > >> + * only two moving parts. > >> + * > >> + * <p>Requires the JVM flag {@code -Djdk.attach.allowAttachSelf=true} > (a JDK 9+ safeguard > >> + * against a process attaching to itself). The {@code ofbizDev} Gradle > task provided by > >> + * this plugin sets it automatically. > >> + */ > >> +public final class HotSwapAgent { > >> + > >> + private static volatile Instrumentation instrumentation; > >> + > >> + private HotSwapAgent() { } > >> + > >> + /** Invoked by the JVM once {@link VirtualMachine#loadAgent} loads > this class as an agent. */ > >> + public static void agentmain(String agentArgs, Instrumentation > inst) { > >> + instrumentation = inst; > >> + } > >> + > >> + /** > >> + * Builds a minimal agent jar wrapping this class and > self-attaches it to the current > >> + * JVM, returning the resulting {@link Instrumentation}. Safe to > call more than once; > >> + * later calls return the instance obtained by the first > successful attach. > >> + */ > >> + static synchronized Instrumentation install() throws Exception { > >> + if (instrumentation != null) { > >> + return instrumentation; > >> + } > >> + Path agentJar = buildAgentJar(); > >> + try { > >> + String pid = String.valueOf(ProcessHandle.current().pid()); > >> + VirtualMachine vm = VirtualMachine.attach(pid); > >> + try { > >> + vm.loadAgent(agentJar.toAbsolutePath().toString()); > >> + } finally { > >> + vm.detach(); > >> + } > >> + } finally { > >> + Files.deleteIfExists(agentJar); > >> + } > >> + return instrumentation; > >> + } > >> + > >> + private static Path buildAgentJar() throws IOException { > >> + String resource = HotSwapAgent.class.getName().replace('.', > '/') + ".class"; > >> + Path jar = Files.createTempFile("devreload-hotswap-agent", > ".jar"); > >> + > >> + Manifest manifest = new Manifest(); > >> + Attributes attrs = manifest.getMainAttributes(); > >> + attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); > >> + attrs.put(new Attributes.Name("Agent-Class"), > HotSwapAgent.class.getName()); > >> + attrs.put(new Attributes.Name("Can-Redefine-Classes"), "true"); > >> + attrs.put(new Attributes.Name("Can-Retransform-Classes"), > "true"); > >> + > >> + try (InputStream in = > HotSwapAgent.class.getClassLoader().getResourceAsStream(resource); > >> + OutputStream fos = Files.newOutputStream(jar); > >> + JarOutputStream jos = new JarOutputStream(fos, > manifest)) { > >> + if (in == null) { > >> + throw new IOException("Could not locate compiled class > on classpath: " + resource); > >> + } > >> + jos.putNextEntry(new ZipEntry(resource)); > >> + in.transferTo(jos); > >> + jos.closeEntry(); > >> + } > >> + return jar; > >> + } > >> +} > >> > >
