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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -673,14 +673,13 @@ public Set<String> syncState(Map<String, String> fileHashes) {
//we have some filters, for files that we don't want to delete
continue;
}
log.info("Scheduled for removal " + file);
if (removedFiles.isEmpty()) {
removedFiles = new ArrayList<>();
}
removedFiles.add(applicationRoot.resolve(file));
}
if (!removedFiles.isEmpty()) {
DevModeMediator.removedFiles.addLast(removedFiles);
DevModeMediator.scheduleDelete(removedFiles);
}
return ret;
} catch (IOException e) {
Expand Down Expand Up @@ -727,12 +726,11 @@ ClassScanResult checkForChangedClasses(QuarkusCompiler compiler,
final List<Path> moduleChangedSourceFilePaths = new ArrayList<>();

for (Path sourcePath : cuf.apply(module).getSourcePaths()) {
final Set<File> changedSourceFiles;
Path start = sourcePath;
if (!Files.exists(start)) {
if (!Files.exists(sourcePath)) {
continue;
}
try (final Stream<Path> sourcesStream = Files.walk(start)) {
final Set<File> changedSourceFiles;
try (final Stream<Path> sourcesStream = Files.walk(sourcePath)) {
changedSourceFiles = sourcesStream
.parallel()
.filter(p -> matchingHandledExtension(p).isPresent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,10 @@ private void copyDependency(Set<ArtifactKey> parentFirstArtifacts, OutputTargetB
return;
}
for (Path resolvedDep : appDep.getResolvedPaths()) {
final String fileName = appDep.getGroupId() + "." + resolvedDep.getFileName();
final boolean isDirectory = Files.isDirectory(resolvedDep);
// we don't use getFileName() for directories, since directories would often be "classes" ending up merging content from multiple dependencies in the same package
final String fileName = isDirectory ? getFileNameForDirectory(appDep)
: appDep.getGroupId() + "." + resolvedDep.getFileName();
final Path targetPath;

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

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

/**
* Returns a JAR file name to be used for a content of a dependency that is in a directory.
*
* @param dep dependency
* @return JAR file name
*/
private static String getFileNameForDirectory(ResolvedDependency dep) {
final StringBuilder sb = new StringBuilder();
sb.append(dep.getGroupId()).append(".").append(dep.getArtifactId()).append("-");
if (!dep.getClassifier().isEmpty()) {
sb.append(dep.getClassifier()).append("-");
}
return sb.append(dep.getVersion()).append(".").append(dep.getType()).toString();
}

private void packageClasses(Path resolvedDep, final Path targetPath, PackageConfig packageConfig) throws IOException {
try (FileSystem runnerZipFs = createNewZip(targetPath, packageConfig)) {
Files.walkFileTree(resolvedDep, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
Expand Down
6 changes: 3 additions & 3 deletions devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -825,9 +825,9 @@ private String handleAutoCompile(List<String> reloadPoms) throws MojoExecutionEx
* @param reloadPoms POM files to be reloaded from disk instead of taken from the reactor
* @return map of parameters for the Quarkus plugin goals
*/
private static Map<String, String> getQuarkusGoalParams(String bootstrapId, List<String> reloadPoms) {
private Map<String, String> getQuarkusGoalParams(String bootstrapId, List<String> reloadPoms) {
final Map<String, String> result = new HashMap<>(4);
result.put(QuarkusBootstrapMojo.MODE_PARAM, LaunchMode.DEVELOPMENT.name());
result.put(QuarkusBootstrapMojo.MODE_PARAM, getLaunchModeClasspath().name());
result.put(QuarkusBootstrapMojo.CLOSE_BOOTSTRAPPED_APP_PARAM, "false");
result.put(QuarkusBootstrapMojo.BOOTSTRAP_ID_PARAM, bootstrapId);
if (reloadPoms != null && !reloadPoms.isEmpty()) {
Expand Down Expand Up @@ -1524,7 +1524,7 @@ private DevModeCommandLine newLauncher(String actualDebugPort, String bootstrapI
// the Maven resolver will be checking for newer snapshots in the remote repository and might end up resolving the artifact from there.
final BootstrapMavenContext mvnCtx = workspaceProvider.createMavenContext(mvnConfig);
appModel = new BootstrapAppModelResolver(new MavenArtifactResolver(mvnCtx))
.setDevMode(true)
.setDevMode(getLaunchModeClasspath().isDevOrTest())
.setTest(LaunchMode.TEST.equals(getLaunchModeClasspath()))
.setCollectReloadableDependencies(!noDeps)
.setLegacyModelResolver(BootstrapAppModelResolver.isLegacyModelResolver(project.getProperties()))
Expand Down
10 changes: 10 additions & 0 deletions devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.ResolutionScope;

import io.quarkus.bootstrap.BootstrapConstants;
import io.quarkus.deployment.dev.DevModeCommandLineBuilder;
import io.quarkus.runtime.LaunchMode;

/**
* The dev mojo, that connects to a remote host.
Expand All @@ -15,4 +17,12 @@ public class RemoteDevMojo extends DevMojo {
protected void modifyDevModeContext(DevModeCommandLineBuilder builder) {
builder.remoteDev(true);
}

@Override
protected LaunchMode getLaunchModeClasspath() {
// For remote-dev we should match the dependency model on the service side, which is a production mutable-jar,
// so we return LaunchMode.NORMAL, but we need to enable workspace discovery to be able to watch for source code changes
project.getProperties().putIfAbsent(BootstrapConstants.QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY, "true");
return LaunchMode.NORMAL;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ private String doConnect(RemoteDevState initialState, Function<Set<String>, Map<
//this file needs to be sent last
//if it is modified it will trigger a reload
//and we need the rest of the app to be present
byte[] lastFile = data.remove(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT);
byte[] lastFile = data.remove(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT);
if (lastFile != null) {
data.put(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT, lastFile);
data.put(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT, lastFile);
}

for (Map.Entry<String, byte[]> entry : data.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ private void handleRequest(HttpServerRequest event) {
.putHeader(QUARKUS_ERROR, "Unknown method " + event.method() + " this is not a valid remote dev request")
.setStatusCode(405).end();
}

}

private void handleDev(HttpServerRequest event) {
Expand All @@ -139,7 +138,6 @@ public Void call() {
hotReplacementContext.setRemoteProblem(problem);
}
synchronized (RemoteSyncHandler.class) {

RemoteSyncHandler.class.notifyAll();
RemoteSyncHandler.class.wait(10000);
if (checkForChanges) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ static Injection forInvokerArgumentLookups(ClassInfo targetBeanClass, MethodInfo
public Injection(AnnotationTarget target, List<InjectionPointInfo> injectionPoints) {
this.target = target;
this.injectionPoints = injectionPoints;
if (injectionPoints.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("Null injection point detected for " + target);
}
}

boolean isMethod() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Deque;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingDeque;

import org.jboss.logging.Logger;

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

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

public static final Deque<List<Path>> removedFiles = new LinkedBlockingDeque<>();
private static final Set<Path> removedFiles = new HashSet<>();

public static void scheduleDelete(Collection<Path> deletedPaths) {
synchronized (removedFiles) {
for (Path deletedPath : deletedPaths) {
if (removedFiles.add(deletedPath)) {
LOGGER.info("Scheduled for removal " + deletedPath);
}
}
}
}

static void doDevMode(Path appRoot) throws IOException, ClassNotFoundException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
Path deploymentClassPath = appRoot.resolve(QuarkusEntryPoint.LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT);
Closeable closeable = doStart(appRoot, deploymentClassPath);
Timer timer = new Timer("Classpath Change Timer", false);
timer.schedule(new ChangeDetector(appRoot, deploymentClassPath, closeable), 1000, 1000);
timer.schedule(new ChangeDetector(appRoot, appRoot.resolve(QuarkusEntryPoint.LIB_DEPLOYMENT_APPMODEL_DAT),
deploymentClassPath, closeable), 1000, 1000);

}

private static Closeable doStart(Path appRoot, Path deploymentClassPath) throws IOException, ClassNotFoundException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Closeable closeable = null;
try (ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(deploymentClassPath))) {
List<String> paths = (List<String>) in.readObject();
Expand All @@ -51,12 +62,11 @@ private static Closeable doStart(Path appRoot, Path deploymentClassPath) throws
throw new RuntimeException(e);
}
}).toArray(URL[]::new));
closeable = new AppProcessCleanup(
return new AppProcessCleanup(
(Closeable) loader.loadClass("io.quarkus.deployment.mutability.DevModeTask")
.getDeclaredMethod("main", Path.class).invoke(null, appRoot),
loader);
}
return closeable;
}

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

private static class ChangeDetector extends TimerTask {

private static long getLastModified(Path appModelDat) throws IOException {
return Files.getLastModifiedTime(appModelDat).toMillis();
}

private final Path appRoot;
private final Path deploymentClassPath;

/**
* If the pom.xml file is changed then this file will be updated
* If a pom.xml file is changed then this file will be updated. So we just check it for changes.
*
* So we just check it for changes.
* We use the {@link QuarkusEntryPoint#LIB_DEPLOYMENT_APPMODEL_DAT} instead of the
* {@link QuarkusEntryPoint#LIB_DEPLOYMENT_DEPLOYMENT_CLASS_PATH_DAT}
* because the application model contains more information about the dependencies.
* A mutable-jar will typically be built as a production application, which may be missing information about reloadable
* dependencies.
* When a mutable-jar is launched in remote-dev mode, the client will update the appmodel.dat with the information about
* the reloadable dependencies.
*
* TODO: is there a potential issue with rsync based implementations not being fully synced? We can just sync this file
* last
* but it gets tricky if we can't control the sync
* last but it gets tricky if we can't control the sync
*/
private final Path deploymentClassPath;

private final Path appModelDat;
private long lastModified;

private Closeable closeable;

public ChangeDetector(Path appRoot, Path deploymentClassPath, Closeable closeable) throws IOException {
public ChangeDetector(Path appRoot, Path appModelDat, Path deploymentClassPath, Closeable closeable)
throws IOException {
this.appRoot = appRoot;
this.deploymentClassPath = deploymentClassPath;
this.closeable = closeable;
lastModified = Files.getLastModifiedTime(deploymentClassPath).toMillis();
this.appModelDat = appModelDat;
lastModified = getLastModified(appModelDat);
}

@Override
public void run() {

try {
long time = Files.getLastModifiedTime(deploymentClassPath).toMillis();
long time = getLastModified(appModelDat);
if (lastModified != time) {
lastModified = time;
if (closeable != null) {
closeable.close();
}
closeable = null;
final List<Path> pathsToDelete = removedFiles.pollFirst();
if (pathsToDelete != null) {
for (Path p : pathsToDelete) {
var sb = new StringBuilder().append("Deleting ").append(p);
if (!Files.deleteIfExists(p)) {
synchronized (removedFiles) {
var removedFilesIterator = removedFiles.iterator();
while (removedFilesIterator.hasNext()) {
final Path removedFile = removedFilesIterator.next();
removedFilesIterator.remove();
var sb = new StringBuilder().append("Deleting ").append(removedFile);
if (!Files.deleteIfExists(removedFile)) {
sb.append(" didn't succeed");
}
LOGGER.info(sb.toString());
Expand All @@ -132,7 +155,5 @@ public void run() {
}

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
public class QuarkusEntryPoint {

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

public static void main(String... args) throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
Expand All @@ -25,9 +26,29 @@
@DisableForNative
public class RemoteDevMojoIT extends RunAndCheckWithAgentMojoTestBase {

@Test
public void testThatTheApplicationIsReloadedMultiModule() throws IOException {
testDir = initProject("projects/multimodule", "projects/multimodule-remote-dev/remote");
agentDir = initProject("projects/multimodule", "projects/multimodule-remote-dev/local");
runAndCheckModule("runner");

final Path remoteLog = testDir.toPath().resolve("runner").resolve("target").resolve("output.log");
assertThat(devModeClient.getHttpResponse("/app/hello")).isEqualTo("hello");

// Edit the "Hello" message.
File source = new File(agentDir, "rest/src/main/java/org/acme/HelloResource.java");
String uuid = UUID.randomUUID().toString();
filter(source, Collections.singletonMap("return \"hello\";", "return \"" + uuid + "\";"));

await()
.pollDelay(1, TimeUnit.SECONDS)
.atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES)
.until(() -> devModeClient.getHttpResponse("/app/hello").contains(uuid));
}

@Test
public void testThatTheApplicationIsReloadedOnJavaChange()
throws MavenInvocationException, IOException, InterruptedException {
throws MavenInvocationException, IOException {
testDir = initProject("projects/classic-remote-dev", "projects/project-classic-run-java-change-remote");
agentDir = initProject("projects/classic-remote-dev", "projects/project-classic-run-java-change-local");
runAndCheck();
Expand Down
Loading
Loading