Skip to content

Commit b1c2c06

Browse files
authored
Merge pull request #48331 from aloubyansky/remote-dev-normal-model
Use production dependency model for remote-dev
2 parents bc0ade2 + 21cb2fe commit b1c2c06

File tree

11 files changed

+145
-62
lines changed

11 files changed

+145
-62
lines changed

core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -673,14 +673,13 @@ public Set<String> syncState(Map<String, String> fileHashes) {
673673
//we have some filters, for files that we don't want to delete
674674
continue;
675675
}
676-
log.info("Scheduled for removal " + file);
677676
if (removedFiles.isEmpty()) {
678677
removedFiles = new ArrayList<>();
679678
}
680679
removedFiles.add(applicationRoot.resolve(file));
681680
}
682681
if (!removedFiles.isEmpty()) {
683-
DevModeMediator.removedFiles.addLast(removedFiles);
682+
DevModeMediator.scheduleDelete(removedFiles);
684683
}
685684
return ret;
686685
} catch (IOException e) {
@@ -727,12 +726,11 @@ ClassScanResult checkForChangedClasses(QuarkusCompiler compiler,
727726
final List<Path> moduleChangedSourceFilePaths = new ArrayList<>();
728727

729728
for (Path sourcePath : cuf.apply(module).getSourcePaths()) {
730-
final Set<File> changedSourceFiles;
731-
Path start = sourcePath;
732-
if (!Files.exists(start)) {
729+
if (!Files.exists(sourcePath)) {
733730
continue;
734731
}
735-
try (final Stream<Path> sourcesStream = Files.walk(start)) {
732+
final Set<File> changedSourceFiles;
733+
try (final Stream<Path> sourcesStream = Files.walk(sourcePath)) {
736734
changedSourceFiles = sourcesStream
737735
.parallel()
738736
.filter(p -> matchingHandledExtension(p).isPresent()

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,10 @@ private void copyDependency(Set<ArtifactKey> parentFirstArtifacts, OutputTargetB
920920
return;
921921
}
922922
for (Path resolvedDep : appDep.getResolvedPaths()) {
923-
final String fileName = appDep.getGroupId() + "." + resolvedDep.getFileName();
923+
final boolean isDirectory = Files.isDirectory(resolvedDep);
924+
// we don't use getFileName() for directories, since directories would often be "classes" ending up merging content from multiple dependencies in the same package
925+
final String fileName = isDirectory ? getFileNameForDirectory(appDep)
926+
: appDep.getGroupId() + "." + resolvedDep.getFileName();
924927
final Path targetPath;
925928

926929
if (allowParentFirst && parentFirstArtifacts.contains(appDep.getKey())) {
@@ -932,7 +935,7 @@ private void copyDependency(Set<ArtifactKey> parentFirstArtifacts, OutputTargetB
932935
}
933936
runtimeArtifacts.computeIfAbsent(appDep.getKey(), (s) -> new ArrayList<>(1)).add(targetPath);
934937

935-
if (Files.isDirectory(resolvedDep)) {
938+
if (isDirectory) {
936939
// This case can happen when we are building a jar from inside the Quarkus repository
937940
// and Quarkus Bootstrap's localProjectDiscovery has been set to true. In such a case
938941
// the non-jar dependencies are the Quarkus dependencies picked up on the file system
@@ -972,6 +975,21 @@ private void copyDependency(Set<ArtifactKey> parentFirstArtifacts, OutputTargetB
972975
}
973976
}
974977

978+
/**
979+
* Returns a JAR file name to be used for a content of a dependency that is in a directory.
980+
*
981+
* @param dep dependency
982+
* @return JAR file name
983+
*/
984+
private static String getFileNameForDirectory(ResolvedDependency dep) {
985+
final StringBuilder sb = new StringBuilder();
986+
sb.append(dep.getGroupId()).append(".").append(dep.getArtifactId()).append("-");
987+
if (!dep.getClassifier().isEmpty()) {
988+
sb.append(dep.getClassifier()).append("-");
989+
}
990+
return sb.append(dep.getVersion()).append(".").append(dep.getType()).toString();
991+
}
992+
975993
private void packageClasses(Path resolvedDep, final Path targetPath, PackageConfig packageConfig) throws IOException {
976994
try (FileSystem runnerZipFs = createNewZip(targetPath, packageConfig)) {
977995
Files.walkFileTree(resolvedDep, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,

devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,9 @@ private String handleAutoCompile(List<String> reloadPoms) throws MojoExecutionEx
825825
* @param reloadPoms POM files to be reloaded from disk instead of taken from the reactor
826826
* @return map of parameters for the Quarkus plugin goals
827827
*/
828-
private static Map<String, String> getQuarkusGoalParams(String bootstrapId, List<String> reloadPoms) {
828+
private Map<String, String> getQuarkusGoalParams(String bootstrapId, List<String> reloadPoms) {
829829
final Map<String, String> result = new HashMap<>(4);
830-
result.put(QuarkusBootstrapMojo.MODE_PARAM, LaunchMode.DEVELOPMENT.name());
830+
result.put(QuarkusBootstrapMojo.MODE_PARAM, getLaunchModeClasspath().name());
831831
result.put(QuarkusBootstrapMojo.CLOSE_BOOTSTRAPPED_APP_PARAM, "false");
832832
result.put(QuarkusBootstrapMojo.BOOTSTRAP_ID_PARAM, bootstrapId);
833833
if (reloadPoms != null && !reloadPoms.isEmpty()) {
@@ -1524,7 +1524,7 @@ private DevModeCommandLine newLauncher(String actualDebugPort, String bootstrapI
15241524
// the Maven resolver will be checking for newer snapshots in the remote repository and might end up resolving the artifact from there.
15251525
final BootstrapMavenContext mvnCtx = workspaceProvider.createMavenContext(mvnConfig);
15261526
appModel = new BootstrapAppModelResolver(new MavenArtifactResolver(mvnCtx))
1527-
.setDevMode(true)
1527+
.setDevMode(getLaunchModeClasspath().isDevOrTest())
15281528
.setTest(LaunchMode.TEST.equals(getLaunchModeClasspath()))
15291529
.setCollectReloadableDependencies(!noDeps)
15301530
.setLegacyModelResolver(BootstrapAppModelResolver.isLegacyModelResolver(project.getProperties()))

devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import org.apache.maven.plugins.annotations.Mojo;
55
import org.apache.maven.plugins.annotations.ResolutionScope;
66

7+
import io.quarkus.bootstrap.BootstrapConstants;
78
import io.quarkus.deployment.dev.DevModeCommandLineBuilder;
9+
import io.quarkus.runtime.LaunchMode;
810

911
/**
1012
* The dev mojo, that connects to a remote host.
@@ -15,4 +17,12 @@ public class RemoteDevMojo extends DevMojo {
1517
protected void modifyDevModeContext(DevModeCommandLineBuilder builder) {
1618
builder.remoteDev(true);
1719
}
20+
21+
@Override
22+
protected LaunchMode getLaunchModeClasspath() {
23+
// For remote-dev we should match the dependency model on the service side, which is a production mutable-jar,
24+
// so we return LaunchMode.NORMAL, but we need to enable workspace discovery to be able to watch for source code changes
25+
project.getProperties().putIfAbsent(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY, "true");
26+
return LaunchMode.NORMAL;
27+
}
1828
}

extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/HttpRemoteDevClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ private String doConnect(RemoteDevState initialState, Function<Set<String>, Map<
159159
//this file needs to be sent last
160160
//if it is modified it will trigger a reload
161161
//and we need the rest of the app to be present
162-
byte[] lastFile = data.remove(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT);
162+
byte[] lastFile = data.remove(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT);
163163
if (lastFile != null) {
164-
data.put(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT, lastFile);
164+
data.put(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT, lastFile);
165165
}
166166

167167
for (Map.Entry<String, byte[]> entry : data.entrySet()) {

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/RemoteSyncHandler.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ private void handleRequest(HttpServerRequest event) {
117117
.putHeader(QUARKUS_ERROR, "Unknown method " + event.method() + " this is not a valid remote dev request")
118118
.setStatusCode(405).end();
119119
}
120-
121120
}
122121

123122
private void handleDev(HttpServerRequest event) {
@@ -139,7 +138,6 @@ public Void call() {
139138
hotReplacementContext.setRemoteProblem(problem);
140139
}
141140
synchronized (RemoteSyncHandler.class) {
142-
143141
RemoteSyncHandler.class.notifyAll();
144142
RemoteSyncHandler.class.wait(10000);
145143
if (checkForChanges) {

independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ static Injection forInvokerArgumentLookups(ClassInfo targetBeanClass, MethodInfo
457457
public Injection(AnnotationTarget target, List<InjectionPointInfo> injectionPoints) {
458458
this.target = target;
459459
this.injectionPoints = injectionPoints;
460+
if (injectionPoints.stream().anyMatch(Objects::isNull)) {
461+
throw new IllegalArgumentException("Null injection point detected for " + target);
462+
}
460463
}
461464

462465
boolean isMethod() {

independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/DevModeMediator.java

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
import java.net.URLClassLoader;
1010
import java.nio.file.Files;
1111
import java.nio.file.Path;
12-
import java.util.Deque;
12+
import java.util.Collection;
13+
import java.util.HashSet;
1314
import java.util.List;
15+
import java.util.Set;
1416
import java.util.Timer;
1517
import java.util.TimerTask;
16-
import java.util.concurrent.LinkedBlockingDeque;
1718

1819
import org.jboss.logging.Logger;
1920

@@ -26,20 +27,30 @@ public class DevModeMediator {
2627

2728
protected static final Logger LOGGER = Logger.getLogger(DevModeMediator.class);
2829

29-
public static final Deque<List<Path>> removedFiles = new LinkedBlockingDeque<>();
30+
private static final Set<Path> removedFiles = new HashSet<>();
31+
32+
public static void scheduleDelete(Collection<Path> deletedPaths) {
33+
synchronized (removedFiles) {
34+
for (Path deletedPath : deletedPaths) {
35+
if (removedFiles.add(deletedPath)) {
36+
LOGGER.info("Scheduled for removal " + deletedPath);
37+
}
38+
}
39+
}
40+
}
3041

3142
static void doDevMode(Path appRoot) throws IOException, ClassNotFoundException, IllegalAccessException,
3243
InvocationTargetException, NoSuchMethodException {
3344
Path deploymentClassPath = appRoot.resolve(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT);
3445
Closeable closeable = doStart(appRoot, deploymentClassPath);
3546
Timer timer = new Timer("Classpath Change Timer", false);
36-
timer.schedule(new ChangeDetector(appRoot, deploymentClassPath, closeable), 1000, 1000);
47+
timer.schedule(new ChangeDetector(appRoot, appRoot.resolve(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT),
48+
deploymentClassPath, closeable), 1000, 1000);
3749

3850
}
3951

4052
private static Closeable doStart(Path appRoot, Path deploymentClassPath) throws IOException, ClassNotFoundException,
4153
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
42-
Closeable closeable = null;
4354
try (ObjectInputStream in = new ObjectInputStream(
4455
Files.newInputStream(deploymentClassPath))) {
4556
List<String> paths = (List<String>) in.readObject();
@@ -51,12 +62,11 @@ private static Closeable doStart(Path appRoot, Path deploymentClassPath) throws
5162
throw new RuntimeException(e);
5263
}
5364
}).toArray(URL[]::new));
54-
closeable = new AppProcessCleanup(
65+
return new AppProcessCleanup(
5566
(Closeable) loader.loadClass("io.quarkus.deployment.mutability.DevModeTask")
5667
.getDeclaredMethod("main", Path.class).invoke(null, appRoot),
5768
loader);
5869
}
59-
return closeable;
6070
}
6171

6272
private static class AppProcessCleanup implements Closeable {
@@ -77,45 +87,58 @@ public void close() throws IOException {
7787
}
7888

7989
private static class ChangeDetector extends TimerTask {
90+
91+
private static long getLastModified(Path appModelDat) throws IOException {
92+
return Files.getLastModifiedTime(appModelDat).toMillis();
93+
}
94+
8095
private final Path appRoot;
96+
private final Path deploymentClassPath;
97+
8198
/**
82-
* If the pom.xml file is changed then this file will be updated
99+
* If a pom.xml file is changed then this file will be updated. So we just check it for changes.
83100
*
84-
* So we just check it for changes.
101+
* We use the {@link QuarkusEntryPoint#LIB_DEPLOYMENT_APPMODEL_DAT} instead of the
102+
* {@link QuarkusEntryPoint#LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT}
103+
* because the application model contains more information about the dependencies.
104+
* A mutable-jar will typically be built as a production application, which may be missing information about reloadable
105+
* dependencies.
106+
* When a mutable-jar is launched in remote-dev mode, the client will update the appmodel.dat with the information about
107+
* the reloadable dependencies.
85108
*
86109
* TODO: is there a potential issue with rsync based implementations not being fully synced? We can just sync this file
87-
* last
88-
* but it gets tricky if we can't control the sync
110+
* last but it gets tricky if we can't control the sync
89111
*/
90-
private final Path deploymentClassPath;
91-
112+
private final Path appModelDat;
92113
private long lastModified;
93114

94115
private Closeable closeable;
95116

96-
public ChangeDetector(Path appRoot, Path deploymentClassPath, Closeable closeable) throws IOException {
117+
public ChangeDetector(Path appRoot, Path appModelDat, Path deploymentClassPath, Closeable closeable)
118+
throws IOException {
97119
this.appRoot = appRoot;
98120
this.deploymentClassPath = deploymentClassPath;
99121
this.closeable = closeable;
100-
lastModified = Files.getLastModifiedTime(deploymentClassPath).toMillis();
122+
this.appModelDat = appModelDat;
123+
lastModified = getLastModified(appModelDat);
101124
}
102125

103126
@Override
104127
public void run() {
105-
106128
try {
107-
long time = Files.getLastModifiedTime(deploymentClassPath).toMillis();
129+
long time = getLastModified(appModelDat);
108130
if (lastModified != time) {
109131
lastModified = time;
110132
if (closeable != null) {
111133
closeable.close();
112134
}
113-
closeable = null;
114-
final List<Path> pathsToDelete = removedFiles.pollFirst();
115-
if (pathsToDelete != null) {
116-
for (Path p : pathsToDelete) {
117-
var sb = new StringBuilder().append("Deleting ").append(p);
118-
if (!Files.deleteIfExists(p)) {
135+
synchronized (removedFiles) {
136+
var removedFilesIterator = removedFiles.iterator();
137+
while (removedFilesIterator.hasNext()) {
138+
final Path removedFile = removedFilesIterator.next();
139+
removedFilesIterator.remove();
140+
var sb = new StringBuilder().append("Deleting ").append(removedFile);
141+
if (!Files.deleteIfExists(removedFile)) {
119142
sb.append(" didn't succeed");
120143
}
121144
LOGGER.info(sb.toString());
@@ -132,7 +155,5 @@ public void run() {
132155
}
133156

134157
}
135-
136158
}
137-
138159
}

independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/QuarkusEntryPoint.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
public class QuarkusEntryPoint {
2525

2626
public static final String QUARKUS_APPLICATION_DAT = "quarkus/quarkus-application.dat";
27+
public static final String LIB_DEPLOYMENT_APPMODEL_DAT = "lib/deployment/appmodel.dat";
2728
public static final String LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT = "lib/deployment/deployment-class-path.dat";
2829

2930
public static void main(String... args) throws Throwable {

integration-tests/maven/src/test/java/io/quarkus/maven/it/RemoteDevMojoIT.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.File;
77
import java.io.IOException;
88
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Path;
910
import java.util.Arrays;
1011
import java.util.Collections;
1112
import java.util.UUID;
@@ -25,9 +26,29 @@
2526
@DisableForNative
2627
public class RemoteDevMojoIT extends RunAndCheckWithAgentMojoTestBase {
2728

29+
@Test
30+
public void testThatTheApplicationIsReloadedMultiModule() throws IOException {
31+
testDir = initProject("projects/multimodule", "projects/multimodule-remote-dev/remote");
32+
agentDir = initProject("projects/multimodule", "projects/multimodule-remote-dev/local");
33+
runAndCheckModule("runner");
34+
35+
final Path remoteLog = testDir.toPath().resolve("runner").resolve("target").resolve("output.log");
36+
assertThat(devModeClient.getHttpResponse("/app/hello")).isEqualTo("hello");
37+
38+
// Edit the "Hello" message.
39+
File source = new File(agentDir, "rest/src/main/java/org/acme/HelloResource.java");
40+
String uuid = UUID.randomUUID().toString();
41+
filter(source, Collections.singletonMap("return \"hello\";", "return \"" + uuid + "\";"));
42+
43+
await()
44+
.pollDelay(1, TimeUnit.SECONDS)
45+
.atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES)
46+
.until(() -> devModeClient.getHttpResponse("/app/hello").contains(uuid));
47+
}
48+
2849
@Test
2950
public void testThatTheApplicationIsReloadedOnJavaChange()
30-
throws MavenInvocationException, IOException, InterruptedException {
51+
throws MavenInvocationException, IOException {
3152
testDir = initProject("projects/classic-remote-dev", "projects/project-classic-run-java-change-remote");
3253
agentDir = initProject("projects/classic-remote-dev", "projects/project-classic-run-java-change-local");
3354
runAndCheck();

0 commit comments

Comments
 (0)