Skip to content

Commit

Permalink
Merge pull request #46 from goblint/server-recovery
Browse files Browse the repository at this point in the history
Recover Goblint server automatically after crashing
  • Loading branch information
karoliineh authored Jan 31, 2023
2 parents ab231f5 + 3c27969 commit 56ef5f3
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 63 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@
<version>3.1.1.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.methvin/directory-watcher -->
<dependency>
<groupId>io.methvin</groupId>
<artifactId>directory-watcher</artifactId>
<version>0.17.1</version>
</dependency>


<!-- log4j -->
<dependency>
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ private static void addAnalysis() {
GoblintService goblintService = localEndpoint.getServer();

// read Goblint configurations
goblintService.read_config(new Params(new File(goblintServer.getGoblintConf()).getAbsolutePath()));
goblintService.read_config(new Params(new File(goblintServer.getGoblintConf()).getAbsolutePath()))
.whenComplete((res, ex) -> {
String msg = "Goblint was unable to successfully read the new configuration. " + ex.getMessage();
magpieServer.forwardMessageToClient(new MessageParams(MessageType.Warning, msg));
log.error(msg);
});

// add analysis to the MagpieServer
ServerAnalysis serverAnalysis = new GoblintAnalysis(magpieServer, goblintServer, goblintService, gobpieConfiguration);
Expand Down
90 changes: 61 additions & 29 deletions src/main/java/analysis/GoblintAnalysis.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package analysis;

import api.GoblintService;
import api.messages.GoblintFunctionsResult;
import api.messages.GoblintMessagesResult;
import api.messages.Params;
import api.messages.*;
import com.ibm.wala.classLoader.Module;
import goblintserver.GoblintServer;
import gobpie.GobPieConfiguration;
Expand Down Expand Up @@ -101,7 +99,11 @@ public void analyze(Collection<? extends Module> files, AnalysisConsumer consume
server.consume(new ArrayList<>(response), source());
log.info("--------------------- Analysis finished ----------------------");
}).exceptionally(ex -> {
log.info(ex.getMessage());
// TODO: handle closed socket exceptions:
// org.eclipse.lsp4j.jsonrpc.JsonRpcException: java.net.SocketException: Broken pipe; errno=32
// and org.eclipse.lsp4j.jsonrpc.JsonRpcException: org.newsclub.net.unix.SocketClosedException: Not open
log.error("--------------------- Analysis failed ----------------------");
log.error(ex.getMessage());
return null;
});
}
Expand Down Expand Up @@ -150,37 +152,60 @@ private void preAnalyse() {
}
}


/**
* Sends the requests to Goblint server and gets their results.
* Checks if analysis succeeded.
* If analysis succeeds, requests the messages from the Goblint server.
* If showCfg option is turned on, asks for the function names for code lenses.
*
* @return a CompletableFuture of a collection of warning messages and cfg code lenses if request was successful.
* @throws GobPieException in case the analysis was aborted or returned a VerifyError.
*/

private CompletableFuture<Collection<AnalysisResult>> reanalyse() {

return goblintService.analyze(new Params())
.thenCompose(analysisResult -> {
// Make sure that analysis succeeded
if (analysisResult.getStatus().contains("Aborted"))
throw new GobPieException("The running analysis has been aborted.", GobPieExceptionType.GOBLINT_EXCEPTION);
else if (analysisResult.getStatus().contains("VerifyError"))
throw new GobPieException("Analysis returned VerifyError.", GobPieExceptionType.GOBLINT_EXCEPTION);
// Get warning messages
CompletableFuture<List<GoblintMessagesResult>> messagesTask = goblintService.messages();
if (gobpieConfiguration.getshowCfg() != null && gobpieConfiguration.getshowCfg()) {
// Get list of functions
CompletableFuture<List<GoblintFunctionsResult>> functionsTask = goblintService.functions();
return messagesTask.thenCombine(functionsTask, (messages, functions) ->
Stream.concat(
convertMessagesFromJson(messages).stream(),
convertFunctionsFromJson(functions).stream())
.collect(Collectors.toList()));
}
return messagesTask.thenApply(this::convertMessagesFromJson);
});
.thenCompose(this::getComposedAnalysisResults)
.applyToEither(didGoblintCrash(), res -> res);
}

private void didAnalysisNotSucceed(GoblintAnalysisResult analysisResult) {
if (analysisResult.getStatus().contains("Aborted"))
throw new GobPieException("The running analysis has been aborted.", GobPieExceptionType.GOBLINT_EXCEPTION);
else if (analysisResult.getStatus().contains("VerifyError"))
throw new GobPieException("Analysis returned VerifyError.", GobPieExceptionType.GOBLINT_EXCEPTION);
}

private CompletableFuture<Collection<AnalysisResult>> didGoblintCrash() {
return CompletableFuture.supplyAsync(() -> {
try {
goblintServer.getGoblintRunProcess().getProcess().waitFor();
} catch (InterruptedException ignored) {
}
throw new GobPieException("Goblint has exited.", GobPieExceptionType.GOBLINT_EXCEPTION);
});
}

private CompletableFuture<Collection<AnalysisResult>> convertAndCombineResults(
CompletableFuture<List<GoblintMessagesResult>> messagesCompletableFuture,
CompletableFuture<List<GoblintFunctionsResult>> functionsCompletableFuture) {
return messagesCompletableFuture
.thenCombine(functionsCompletableFuture, (messages, functions) ->
Stream.concat(
convertMessagesFromJson(messages).stream(),
convertFunctionsFromJson(functions).stream()
).collect(Collectors.toList()));
}

private CompletableFuture<Collection<AnalysisResult>> getComposedAnalysisResults(GoblintAnalysisResult analysisResult) {
didAnalysisNotSucceed(analysisResult);
// Get warning messages
CompletableFuture<List<GoblintMessagesResult>> messagesCompletableFuture = goblintService.messages();
if (gobpieConfiguration.getshowCfg() == null || !gobpieConfiguration.getshowCfg()) {
return messagesCompletableFuture.thenApply(this::convertMessagesFromJson);
}
// Get list of functions
CompletableFuture<List<GoblintFunctionsResult>> functionsCompletableFuture = goblintService.functions();
return convertAndCombineResults(messagesCompletableFuture, functionsCompletableFuture);
}


Expand Down Expand Up @@ -229,15 +254,22 @@ public ProcessResult runCommand(File dirPath, String[] command) throws IOExcepti
*/

public FileAlterationObserver createGoblintConfObserver() {

FileFilter fileFilter = file -> file.getName().equals(gobpieConfiguration.getGoblintConf());

FileAlterationObserver observer = new FileAlterationObserver(System.getProperty("user.dir"), fileFilter);
observer.addListener(new FileAlterationListenerAdaptor() {
@Override
public void onFileChange(File file) {
goblintService.reset_config();
goblintService.read_config(new Params(new File(goblintServer.getGoblintConf()).getAbsolutePath()));
if (!goblintServer.getGoblintRunProcess().getProcess().isAlive()) {
magpieServer.exit();
} else {
goblintService.reset_config();
goblintService.read_config(new Params(new File(goblintServer.getGoblintConf()).getAbsolutePath()))
.whenComplete((res, ex) -> {
String msg = "Goblint was unable to successfully read the new configuration. " + ex.getMessage();
magpieServer.forwardMessageToClient(new MessageParams(MessageType.Warning, msg));
log.error(msg);
});
}
}
});

Expand Down
30 changes: 12 additions & 18 deletions src/main/java/api/GoblintService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,39 @@

public interface GoblintService {

// Examples of requests used in this project:
// {"jsonrpc":"2.0","id":0,"method":"read_config","params":{"fname":"goblint.json"}}
// {"jsonrpc":"2.0","id":0,"method":"analyze","params":{}}
// {"jsonrpc":"2.0","id":0,"method":"messages"}
// {"jsonrpc":"2.0","id":0,"method":"functions"}
// {"jsonrpc":"2.0","id":0,"method":"cfg", "params":{"fname":"main"}}
// {"jsonrpc":"2.0","id":0,"method":"node_state","params":{"nid":"fun2783"}}

// Examples of responses for the requests:
// method: "analyze" response:
// {"id":0,"jsonrpc":"2.0","result":{"status":["Success"]}}
// method: "messages" response:
// {"id":0,"jsonrpc":"2.0","result":[{"tags":[{"Category":["Race"]}], ... }]}
// method: "functions" response:
// {"id":0,"jsonrpc":"2.0","result":[{"funName":"qsort","location":{"file":"/home/ ... }]}
// method: "cfg" response:
// {"id":0,"jsonrpc":"2.0","result":{"cfg":"digraph cfg {\n\tnode [id=\"\\N\", ... }}

@JsonRequest
CompletableFuture<JsonObject> ping();

// request: {"jsonrpc":"2.0","id":0,"method":"analyze","params":{}}
// response: {"id":0,"jsonrpc":"2.0","result":{"status":["Success"]}}
@JsonRequest
CompletableFuture<GoblintAnalysisResult> analyze(Params params);

// request: {"jsonrpc":"2.0","id":0,"method":"messages"}
// response: {"id":0,"jsonrpc":"2.0","result":[{"tags":[{"Category":["Race"]}], ... }]}
@JsonRequest
CompletableFuture<List<GoblintMessagesResult>> messages();

// request: {"jsonrpc":"2.0","id":0,"method":"functions"}
// response: {"id":0,"jsonrpc":"2.0","result":[{"funName":"qsort","location":{"file":"/home/ ... }]}
@JsonRequest
CompletableFuture<List<GoblintFunctionsResult>> functions();

// request: {"jsonrpc":"2.0","id":0,"method":"cfg", "params":{"fname":"main"}}
// response: {"id":0,"jsonrpc":"2.0","result":{"cfg":"digraph cfg {\n\tnode [id=\"\\N\", ... }}
@JsonRequest
CompletableFuture<GoblintCFGResult> cfg(Params params);

// request: {"jsonrpc":"2.0","id":0,"method":"node_state","params":{"nid":"fun2783"}}
@JsonRequest
CompletableFuture<List<JsonObject>> node_state(GobPieHttpHandler.NodeParams params);

@JsonRequest
CompletableFuture<Void> reset_config();

// request: {"jsonrpc":"2.0","id":0,"method":"read_config","params":{"fname":"goblint.json"}}
// response: {"id":0,"jsonrpc":"2.0","result":null}
// {"id":0,"jsonrpc":"2.0","error":{"code":-32603,"message":"Json_encoding: Unexpected object field .."}}
@JsonRequest
CompletableFuture<Void> read_config(Params params);

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/api/GoblintServiceLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected RemoteEndpoint createRemoteEndpoint(MessageJsonHandler messageJsonHand

public void connectSocketStreams() {
try {
AFUNIXSocket socket = AFUNIXSocket.newInstance();
AFUNIXSocket socket = AFUNIXSocket.newInstance(); // TODO: close after
socket.connect(AFUNIXSocketAddress.of(new File(goblintSocketName)));
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/api/util/DirectoryWatchingUtility.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package api.util;

import io.methvin.watcher.DirectoryWatcher;

import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;

public class DirectoryWatchingUtility {

private final DirectoryWatcher watcher;

public DirectoryWatchingUtility(Path directoryToWatch) throws IOException {
this.watcher = DirectoryWatcher.builder()
.path(directoryToWatch) // or use paths(directoriesToWatch)
.listener(event -> {
switch (event.eventType()) {
case CREATE:
/* file created */
stopWatching();
return;
case MODIFY:
/* file modified */
break;
case DELETE:
/* file deleted */
break;
}
})
// .fileHashing(false) // defaults to true
// .logger(logger) // defaults to LoggerFactory.getLogger(DirectoryWatcher.class)
// .watchService(watchService) // defaults based on OS to either JVM WatchService or the JNA macOS WatchService
.build();
}

public void stopWatching() throws IOException {
watcher.close();
}

public CompletableFuture<Void> watch() {
// you can also use watcher.watch() to block the current thread
return watcher.watchAsync();
}
}
27 changes: 13 additions & 14 deletions src/main/java/goblintserver/GoblintServer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package goblintserver;

import api.util.DirectoryWatchingUtility;
import gobpie.GobPieException;
import magpiebridge.core.MagpieServer;
import org.apache.logging.log4j.LogManager;
Expand All @@ -15,6 +16,7 @@
import java.io.IOException;
import java.nio.file.*;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;

import static gobpie.GobPieExceptionType.GOBLINT_EXCEPTION;
Expand Down Expand Up @@ -86,30 +88,25 @@ public void startGoblintServer() {
try {
// run command to start goblint
log.info("Goblint run with command: " + String.join(" ", goblintRunCommand));

goblintRunProcess = runCommand(new File(System.getProperty("user.dir")), goblintRunCommand);

// wait until Goblint socket is created before continuing
if (!new File(goblintSocket).exists()) {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get(System.getProperty("user.dir"));
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
if ((event.context()).equals(Paths.get(goblintSocket))) {
log.info("Goblint server started.");
return;
}
}
key.reset();
}
watch();
}
} catch (IOException | InvalidExitValueException | InterruptedException | TimeoutException e) {
throw new GobPieException("Running Goblint failed.", e, GOBLINT_EXCEPTION);
}
}

private void watch() throws IOException, InterruptedException {
Path path = Paths.get(System.getProperty("user.dir"));
DirectoryWatchingUtility socketWatchingUtility = new DirectoryWatchingUtility(path);
socketWatchingUtility.watch().thenAccept(res -> {
log.info("Goblint server started.");
});
}


/**
* Method for running a command.
Expand All @@ -131,6 +128,8 @@ public void afterStop(Process process) {
magpieServer.forwardMessageToClient(new MessageParams(MessageType.Error, "Goblint server exited due to an error. Please check the output terminal of GobPie extension for more information."));
log.error("Goblint server exited due to an error (code: " + process.exitValue() + "). Please fix the issue reported above and restart the extension.");
}
magpieServer.cleanUp();
// TODO: throw an exception? where (and how) can it be caught to be handled though?
}
};

Expand Down

0 comments on commit 56ef5f3

Please sign in to comment.