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 @@ -18,4 +18,6 @@ public interface HotReplacementContext {
* @throws Exception
*/
boolean doScan() throws Exception;

void addPreScanStep(Runnable runnable);
}
5 changes: 3 additions & 2 deletions core/devmode/src/main/java/io/quarkus/dev/DevModeMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,13 @@ private static synchronized void doStart() {
//we can potentially throw away this class loader, and reload the app
try {
Thread.currentThread().setContextClassLoader(runtimeCl);
RuntimeRunner runner = RuntimeRunner.builder()
RuntimeRunner.Builder builder = RuntimeRunner.builder()
.setLaunchMode(LaunchMode.DEVELOPMENT)
.setClassLoader(runtimeCl)
.setTarget(classesRoot.toPath())
.setFrameworkClassesPath(wiringDir.toPath())
.setTransformerCache(cacheDir.toPath())
.setTransformerCache(cacheDir.toPath());
RuntimeRunner runner = builder
.build();
runner.run();
closeable = runner;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -49,6 +51,7 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext {
private final Map<String, Long> configFileTimestamps = new ConcurrentHashMap<>();

private static final Logger log = Logger.getLogger(RuntimeUpdatesProcessor.class.getPackage().getName());
private final List<Runnable> preScanSteps = new CopyOnWriteArrayList<>();

public RuntimeUpdatesProcessor(Path classesDir, Path sourcesDir, Path resourcesDir, ClassLoaderCompiler compiler) {
this.classesDir = classesDir;
Expand Down Expand Up @@ -79,6 +82,13 @@ public Throwable getDeploymentProblem() {

public boolean doScan() throws IOException {
final long startNanoseconds = System.nanoTime();
for (Runnable i : preScanSteps) {
try {
i.run();
} catch (Throwable t) {
log.error("Pre Scan step failed", t);
}
}

boolean classChanged = checkForChangedClasses();
boolean configFileChanged = checkForConfigFileChange();
Expand All @@ -91,6 +101,11 @@ public boolean doScan() throws IOException {
return false;
}

@Override
public void addPreScanStep(Runnable runnable) {
preScanSteps.add(runnable);
}

boolean checkForChangedClasses() throws IOException {
final Set<File> changedSourceFiles;

Expand Down
4 changes: 4 additions & 0 deletions devtools/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
Expand Down
182 changes: 182 additions & 0 deletions devtools/common/src/main/java/io/quarkus/remotedev/AgentRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2016, Stuart Douglas, and individual contributors as indicated
* by the @authors tag.
*
* 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.
*/

package io.quarkus.remotedev;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.ClientEndpointConfig;
import javax.websocket.ContainerProvider;

public class AgentRunner extends QuarkusWebsocketProtocol {

private static final String REMOTE_PASSWORD = "quarkus-security-key";

private final Map<String, Long> classChangeTimes = new ConcurrentHashMap<>();
private final Map<String, Long> resourceChangeTimes = new ConcurrentHashMap<>();
//we only care about classes changed after the agent started
private final long agentStart;

private final String web;
private final String srcs;
private final String classes;
private final String uri;
private final String password;

public AgentRunner(String web, String srcs, String classes, String uri, String password) {
this.web = web;
this.srcs = srcs;
this.classes = classes;
this.uri = uri;
this.password = password;
//
this.agentStart = ManagementFactory.getRuntimeMXBean().getStartTime();
}

public void run() {

try {
ContainerProvider.getWebSocketContainer().connectToServer(this,
ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
headers.put(REMOTE_PASSWORD, Collections.singletonList(password));
}
}).build(), new URI(uri));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}

protected Map<String, byte[]> changedSrcs() {
Map<String, byte[]> found = new HashMap<>();
if (srcs != null) {
try {
scanForProgramChanges("", new File(srcs), found);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
return found;
}

protected Map<String, byte[]> changedWebResources() {
Map<String, byte[]> found = new HashMap<>();
if (web != null) {
try {
scanForWebResources("", new File(web), found);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
return found;
}

@Override
protected void logMessage(String message) {
System.out.println(message);
}

@Override
protected void error(Throwable t) {
t.printStackTrace();
System.exit(1);
}

@Override
protected void done() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}

private void scanForWebResources(String currentPath, File root, Map<String, byte[]> found) throws IOException {
File serverCurrent = new File(root, currentPath);
for (String part : serverCurrent.list()) {
String fullPart = (currentPath.isEmpty() ? "" : (currentPath + File.separatorChar)) + part;
File f = new File(serverCurrent, part);
if (f.isDirectory()) {
scanForWebResources(fullPart, root, found);
} else {
File localFile = new File(serverCurrent, part);
Long recordedChange = resourceChangeTimes.get(fullPart);
long lastModified = localFile.lastModified();
if (recordedChange == null) {
recordedChange = agentStart;
}
if (recordedChange < lastModified) {
found.put(fullPart, readFile(localFile));
resourceChangeTimes.put(fullPart, lastModified);
}
}
}
}

private void scanForProgramChanges(String currentPath, File root, Map<String, byte[]> found)
throws IOException {
File serverCurrent = new File(root, currentPath);
for (String part : serverCurrent.list()) {
String fullPart = (currentPath.isEmpty() ? "" : (currentPath + File.separatorChar)) + part;
File f = new File(serverCurrent, part);
if (f.isDirectory()) {
scanForProgramChanges(fullPart, root, found);
} else if (part.contains(".")) {
File localFile = new File(serverCurrent, part);
String fullPartAsClassFile = fullPart.substring(0, fullPart.lastIndexOf('.')) + ".class";

Long recordedChange = classChangeTimes.get(fullPartAsClassFile);
long lastModified = localFile.lastModified();
if (recordedChange == null) {
recordedChange = agentStart;
}
System.out.println("file " + localFile + " " + recordedChange + " " + lastModified);
if (recordedChange < lastModified) {
found.put(fullPart, readFile(localFile));
classChangeTimes.put(fullPartAsClassFile, lastModified);
}
}
}
}

private byte[] readFile(File localFile) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (FileInputStream in = new FileInputStream(localFile)) {
byte[] buf = new byte[1024];
int r;
while ((r = in.read(buf)) > 0) {
out.write(buf, 0, r);
}
}
return out.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2016, Stuart Douglas, and individual contributors as indicated
* by the @authors tag.
*
* 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.
*/

package io.quarkus.remotedev;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;

import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

/**
* Implements the Fakereplace Websocket protocol
*/
public abstract class QuarkusWebsocketProtocol extends Endpoint implements MessageHandler.Whole<byte[]> {
private static final int CLASS_CHANGE_RESPONSE = 2;
private static final int CLASS_CHANGE_REQUEST = 1;
private volatile Session session;

protected abstract Map<String, byte[]> changedSrcs();

protected abstract Map<String, byte[]> changedWebResources();

protected abstract void logMessage(String message);

protected abstract void error(Throwable t);

protected abstract void done();

@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
logMessage("Connected to remote server");
session.addMessageHandler(this);
this.session = session;
}

@Override
public void onClose(Session session, CloseReason closeReason) {
logMessage("Connection closed " + closeReason);
done();
}

@Override
public void onError(Session session, Throwable thr) {
error(thr);
}

@Override
public void onMessage(byte[] bytes) {
switch (bytes[0]) {
case CLASS_CHANGE_REQUEST: {
logMessage("Scanning for changed classes");
sendChangedClasses();
break;
}
default: {
logMessage("Ignoring unknown message type " + bytes[0]);
}
}
}

private void sendChangedClasses() {
final Map<String, byte[]> changedSrcs = changedSrcs();
final Map<String, byte[]> changedResources = changedWebResources();
logMessage("Scan complete changed srcs " + changedSrcs.keySet()
+ " changes resources " + changedResources);
try (OutputStream out = session.getBasicRemote().getSendStream()) {

out.write(CLASS_CHANGE_RESPONSE);
DataOutputStream data = new DataOutputStream(new DeflaterOutputStream(out));
data.writeInt(changedSrcs.size());
for (Map.Entry<String, byte[]> entry : changedSrcs.entrySet()) {
data.writeUTF(entry.getKey());
data.writeInt(entry.getValue().length);
data.write(entry.getValue());
}
data.writeInt(changedResources.size());
for (Map.Entry<String, byte[]> entry : changedResources.entrySet()) {
data.writeUTF(entry.getKey());
data.writeInt(entry.getValue().length);
data.write(entry.getValue());
}
data.close();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
3 changes: 3 additions & 0 deletions devtools/gradle-it/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
apply plugin: 'java'
apply plugin: 'io.quarkus.gradle.plugin'

compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"

buildscript {
repositories {
mavenCentral()
Expand Down
Loading