From 5fe48f3dbf86b0b19a823b55345f7866062e208b Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 16:39:24 +0530 Subject: [PATCH 1/3] update --- .../org/togetherjava/jshellapi/Config.java | 28 +- .../jshellapi/dto/JShellEvalAbortion.java | 5 +- .../dto/JShellEvalAbortionCause.java | 10 +- .../jshellapi/dto/JShellResult.java | 13 +- .../jshellapi/dto/JShellSnippetResult.java | 7 +- .../jshellapi/exceptions/DockerException.java | 9 +- .../jshellapi/rest/JShellController.java | 88 ++-- .../jshellapi/service/DockerService.java | 161 +++---- .../jshellapi/service/JShellService.java | 140 +++--- .../service/JShellSessionService.java | 146 ++++--- .../jshellapi/service/SessionInfo.java | 43 +- .../service/StartupScriptsService.java | 18 +- .../togetherjava/jshellapi/service/Utils.java | 10 +- .../togetherjava/jshell/wrapper/Config.java | 3 +- .../jshell/wrapper/EvalResult.java | 4 +- .../jshell/wrapper/JShellEvalAbortion.java | 5 +- .../wrapper/JShellEvalAbortionCause.java | 6 +- .../jshell/wrapper/JShellEvalStop.java | 4 +- .../jshell/wrapper/JShellWrapper.java | 153 ++++--- .../jshell/wrapper/StringOutputStream.java | 12 +- .../jshell/wrapper/TimeoutWatcher.java | 18 +- .../java/JShellWrapperStartupScriptTest.java | 24 +- .../src/test/java/JShellWrapperTest.java | 204 +++++---- .../src/test/java/StringOutputStreamTest.java | 14 +- build.gradle | 16 +- spotless.xml | 404 ++++++++++++++++++ 26 files changed, 1039 insertions(+), 506 deletions(-) create mode 100644 spotless.xml diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java index 3e2558b..37669ec 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java @@ -5,18 +5,19 @@ @ConfigurationProperties("jshellapi") public record Config( - long regularSessionTimeoutSeconds, - long oneTimeSessionTimeoutSeconds, - long evalTimeoutSeconds, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - long maxAliveSessions, - int dockerMaxRamMegaBytes, - double dockerCPUsUsage, - @Nullable String dockerCPUSetCPUs, - long schedulerSessionKillScanRateSeconds, - long dockerResponseTimeout, - long dockerConnectionTimeout) { + long regularSessionTimeoutSeconds, + long oneTimeSessionTimeoutSeconds, + long evalTimeoutSeconds, + long evalTimeoutValidationLeeway, + int sysOutCharLimit, + long maxAliveSessions, + int dockerMaxRamMegaBytes, + double dockerCPUsUsage, + @Nullable String dockerCPUSetCPUs, + long schedulerSessionKillScanRateSeconds, + long dockerResponseTimeout, + long dockerConnectionTimeout +) { public Config { if (regularSessionTimeoutSeconds <= 0) throw new IllegalArgumentException("Invalid value " + regularSessionTimeoutSeconds); @@ -38,7 +39,8 @@ public record Config( throw new IllegalArgumentException("Invalid value " + dockerCPUSetCPUs); if (schedulerSessionKillScanRateSeconds <= 0) throw new IllegalArgumentException( - "Invalid value " + schedulerSessionKillScanRateSeconds); + "Invalid value " + schedulerSessionKillScanRateSeconds + ); if (dockerResponseTimeout <= 0) throw new IllegalArgumentException("Invalid value " + dockerResponseTimeout); if (dockerConnectionTimeout <= 0) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java index 3a6f93b..fca4693 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java @@ -1,4 +1,7 @@ package org.togetherjava.jshellapi.dto; public record JShellEvalAbortion( - String sourceCause, String remainingSource, JShellEvalAbortionCause cause) {} + String sourceCause, + String remainingSource, + JShellEvalAbortionCause cause +) {} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java index 2925ae5..511619e 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java @@ -1,10 +1,10 @@ package org.togetherjava.jshellapi.dto; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; -import java.util.List; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) public sealed interface JShellEvalAbortionCause { @@ -12,8 +12,10 @@ public sealed interface JShellEvalAbortionCause { record TimeoutAbortionCause() implements JShellEvalAbortionCause {} @JsonTypeName("UNCAUGHT_EXCEPTION") - record UnhandledExceptionAbortionCause(String exceptionClass, String exceptionMessage) - implements JShellEvalAbortionCause {} + record UnhandledExceptionAbortionCause( + String exceptionClass, + String exceptionMessage + ) implements JShellEvalAbortionCause {} @JsonTypeName("COMPILE_TIME_ERROR") record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause {} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java index 1fac3d7..3c81f99 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java @@ -1,14 +1,15 @@ package org.togetherjava.jshellapi.dto; -import org.springframework.lang.Nullable; - import java.util.List; +import org.springframework.lang.Nullable; + public record JShellResult( - List snippetsResults, - @Nullable JShellEvalAbortion abortion, - boolean stdoutOverflow, - String stdout) { + List snippetsResults, + @Nullable JShellEvalAbortion abortion, + boolean stdoutOverflow, + String stdout +) { public JShellResult { snippetsResults = List.copyOf(snippetsResults); } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java index 13c0d3a..107d43c 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java @@ -3,4 +3,9 @@ import org.springframework.lang.Nullable; public record JShellSnippetResult( - SnippetStatus status, SnippetType type, int id, String source, @Nullable String result) {} + SnippetStatus status, + SnippetType type, + int id, + String source, + @Nullable String result +) {} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java index 3c7e093..30fa2d6 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java @@ -20,10 +20,11 @@ public DockerException(Throwable cause) { } public DockerException( - String message, - Throwable cause, - boolean enableSuppression, - boolean writableStackTrace) { + String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace + ) { super(message, cause, enableSuppression, writableStackTrace); } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 63d2011..6dc539f 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -1,5 +1,7 @@ package org.togetherjava.jshellapi.rest; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -12,8 +14,6 @@ import org.togetherjava.jshellapi.service.StartupScriptId; import org.togetherjava.jshellapi.service.StartupScriptsService; -import java.util.List; - @RequestMapping("jshell") @RestController public class JShellController { @@ -22,63 +22,72 @@ public class JShellController { @PostMapping("/eval/{id}") public JShellResult eval( - @PathVariable String id, - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) - throws DockerException { + @PathVariable String id, + @RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code + ) throws DockerException { validateId(id); return service.session(id, startupScriptId) - .eval(code) - .orElseThrow( - () -> - new ResponseStatusException( - HttpStatus.CONFLICT, "An operation is already running")); + .eval(code) + .orElseThrow( + () -> new ResponseStatusException( + HttpStatus.CONFLICT, + "An operation is already running" + ) + ); } @PostMapping("/eval") public JShellResultWithId eval( - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) - throws DockerException { + @RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code + ) throws DockerException { JShellService jShellService = service.session(startupScriptId); return new JShellResultWithId( - jShellService.id(), - jShellService - .eval(code) - .orElseThrow( - () -> - new ResponseStatusException( - HttpStatus.CONFLICT, - "An operation is already running"))); + jShellService.id(), + jShellService + .eval(code) + .orElseThrow( + () -> new ResponseStatusException( + HttpStatus.CONFLICT, + "An operation is already running" + ) + ) + ); } @PostMapping("/single-eval") public JShellResult singleEval( - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) - throws DockerException { + @RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code + ) throws DockerException { JShellService jShellService = service.oneTimeSession(startupScriptId); return jShellService - .eval(code) - .orElseThrow( - () -> - new ResponseStatusException( - HttpStatus.CONFLICT, "An operation is already running")); + .eval(code) + .orElseThrow( + () -> new ResponseStatusException( + HttpStatus.CONFLICT, + "An operation is already running" + ) + ); } @GetMapping("/snippets/{id}") public List snippets( - @PathVariable String id, @RequestParam(required = false) boolean includeStartupScript) - throws DockerException { + @PathVariable String id, + @RequestParam(required = false) boolean includeStartupScript + ) throws DockerException { validateId(id); if (!service.hasSession(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); return service.session(id, null) - .snippets(includeStartupScript) - .orElseThrow( - () -> - new ResponseStatusException( - HttpStatus.CONFLICT, "An operation is already running")); + .snippets(includeStartupScript) + .orElseThrow( + () -> new ResponseStatusException( + HttpStatus.CONFLICT, + "An operation is already running" + ) + ); } @DeleteMapping("/{id}") @@ -107,8 +116,9 @@ public void setStartupScriptsService(StartupScriptsService startupScriptsService private static void validateId(String id) throws ResponseStatusException { if (!id.matches("[a-zA-Z0-9][a-zA-Z0-9_.-]+")) { throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, - "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+"); + HttpStatus.BAD_REQUEST, + "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+" + ); } } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java index e7fb651..44c6950 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java @@ -1,5 +1,11 @@ package org.togetherjava.jshellapi.service; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.TimeUnit; + import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.PullImageResultCallback; @@ -7,7 +13,6 @@ import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; @@ -15,12 +20,6 @@ import org.springframework.stereotype.Service; import org.togetherjava.jshellapi.Config; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.TimeUnit; - @Service public class DockerService implements DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger(DockerService.class); @@ -31,24 +30,24 @@ public class DockerService implements DisposableBean { public DockerService(Config config) { DefaultDockerClientConfig clientConfig = - DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DefaultDockerClientConfig.createDefaultConfigBuilder().build(); ApacheDockerHttpClient httpClient = - new ApacheDockerHttpClient.Builder() - .dockerHost(clientConfig.getDockerHost()) - .sslConfig(clientConfig.getSSLConfig()) - .responseTimeout(Duration.ofSeconds(config.dockerResponseTimeout())) - .connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout())) - .build(); + new ApacheDockerHttpClient.Builder() + .dockerHost(clientConfig.getDockerHost()) + .sslConfig(clientConfig.getSSLConfig()) + .responseTimeout(Duration.ofSeconds(config.dockerResponseTimeout())) + .connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout())) + .build(); this.client = DockerClientImpl.getInstance(clientConfig, httpClient); cleanupLeftovers(WORKER_UNIQUE_ID); } private void cleanupLeftovers(UUID currentId) { - for (Container container : - client.listContainersCmd().withLabelFilter(Set.of(WORKER_LABEL)).exec()) { + for (Container container : client.listContainersCmd().withLabelFilter(Set.of(WORKER_LABEL)) + .exec()) { String containerHumanName = - container.getId() + " " + Arrays.toString(container.getNames()); + container.getId() + " " + Arrays.toString(container.getNames()); LOGGER.info("Found worker container '{}'", containerHumanName); if (!container.getLabels().get(WORKER_LABEL).equals(currentId.toString())) { LOGGER.info("Killing container '{}'", containerHumanName); @@ -58,82 +57,88 @@ private void cleanupLeftovers(UUID currentId) { } public String spawnContainer( - long maxMemoryMegs, - long cpus, - @Nullable String cpuSetCpus, - String name, - Duration evalTimeout, - long sysoutLimit) - throws InterruptedException { + long maxMemoryMegs, + long cpus, + @Nullable String cpuSetCpus, + String name, + Duration evalTimeout, + long sysoutLimit + ) throws InterruptedException { String imageName = "togetherjava.org:5001/togetherjava/jshellwrapper"; boolean presentLocally = - client.listImagesCmd().withFilter("reference", List.of(imageName)).exec().stream() - .flatMap(it -> Arrays.stream(it.getRepoTags())) - .anyMatch(it -> it.endsWith(":master")); + client.listImagesCmd().withFilter("reference", List.of(imageName)).exec().stream() + .flatMap(it -> Arrays.stream(it.getRepoTags())) + .anyMatch(it -> it.endsWith(":master")); if (!presentLocally) { client.pullImageCmd(imageName) - .withTag("master") - .exec(new PullImageResultCallback()) - .awaitCompletion(5, TimeUnit.MINUTES); + .withTag("master") + .exec(new PullImageResultCallback()) + .awaitCompletion(5, TimeUnit.MINUTES); } return client.createContainerCmd(imageName + ":master") - .withHostConfig( - HostConfig.newHostConfig() - .withAutoRemove(true) - .withInit(true) - .withCapDrop(Capability.ALL) - .withNetworkMode("none") - .withPidsLimit(2000L) - .withReadonlyRootfs(true) - .withMemory(maxMemoryMegs * 1024 * 1024) - .withCpuCount(cpus) - .withCpusetCpus(cpuSetCpus)) - .withStdinOpen(true) - .withAttachStdin(true) - .withAttachStderr(true) - .withAttachStdout(true) - .withEnv( - "evalTimeoutSeconds=" + evalTimeout.toSeconds(), - "sysOutCharLimit=" + sysoutLimit) - .withLabels(Map.of(WORKER_LABEL, WORKER_UNIQUE_ID.toString())) - .withName(name) - .exec() - .getId(); + .withHostConfig( + HostConfig.newHostConfig() + .withAutoRemove(true) + .withInit(true) + .withCapDrop(Capability.ALL) + .withNetworkMode("none") + .withPidsLimit(2000L) + .withReadonlyRootfs(true) + .withMemory(maxMemoryMegs * 1024 * 1024) + .withCpuCount(cpus) + .withCpusetCpus(cpuSetCpus) + ) + .withStdinOpen(true) + .withAttachStdin(true) + .withAttachStderr(true) + .withAttachStdout(true) + .withEnv( + "evalTimeoutSeconds=" + evalTimeout.toSeconds(), + "sysOutCharLimit=" + sysoutLimit + ) + .withLabels(Map.of(WORKER_LABEL, WORKER_UNIQUE_ID.toString())) + .withName(name) + .exec() + .getId(); } - public InputStream startAndAttachToContainer(String containerId, InputStream stdin) - throws IOException { + public InputStream startAndAttachToContainer( + String containerId, + InputStream stdin + ) throws IOException { PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); client.attachContainerCmd(containerId) - .withLogs(true) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withStdIn(stdin) - .exec( - new ResultCallback.Adapter<>() { - @Override - public void onNext(Frame object) { - try { - String payloadString = - new String(object.getPayload(), StandardCharsets.UTF_8); - if (object.getStreamType() == StreamType.STDOUT) { - pipeOut.write(object.getPayload()); - } else { - LOGGER.warn( - "Received STDERR from container {}: {}", - containerId, - payloadString); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } + .withLogs(true) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withStdIn(stdin) + .exec( + new ResultCallback.Adapter<>() { + @Override + public void onNext(Frame object) { + try { + String payloadString = + new String(object.getPayload(), StandardCharsets.UTF_8); + if (object.getStreamType() == StreamType.STDOUT) { + pipeOut.write(object.getPayload()); + } else { + LOGGER.warn( + "Received STDERR from container {}: {}", + containerId, + payloadString + ); } - }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + ); client.startContainerCmd(containerId).exec(); return pipeIn; diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java index e712f55..a569458 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java @@ -1,12 +1,5 @@ package org.togetherjava.jshellapi.service; -import org.apache.tomcat.util.http.fileupload.util.Closeable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; -import org.togetherjava.jshellapi.dto.*; -import org.togetherjava.jshellapi.exceptions.DockerException; - import java.io.*; import java.time.Duration; import java.time.Instant; @@ -14,6 +7,13 @@ import java.util.List; import java.util.Optional; +import org.apache.tomcat.util.http.fileupload.util.Closeable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; +import org.togetherjava.jshellapi.dto.*; +import org.togetherjava.jshellapi.exceptions.DockerException; + public class JShellService implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(JShellService.class); private final JShellSessionService sessionService; @@ -31,19 +31,20 @@ public class JShellService implements Closeable { private final int startupScriptSize; public JShellService( - DockerService dockerService, - JShellSessionService sessionService, - String id, - long timeout, - boolean renewable, - long evalTimeout, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - int maxMemory, - double cpus, - @Nullable String cpuSetCpus, - String startupScript) - throws DockerException { + DockerService dockerService, + JShellSessionService sessionService, + String id, + long timeout, + boolean renewable, + long evalTimeout, + long evalTimeoutValidationLeeway, + int sysOutCharLimit, + int maxMemory, + double cpus, + @Nullable String cpuSetCpus, + String startupScript + ) + throws DockerException { this.dockerService = dockerService; this.sessionService = sessionService; this.id = id; @@ -58,19 +59,21 @@ public JShellService( } try { String containerId = - dockerService.spawnContainer( - maxMemory, - (long) Math.ceil(cpus), - cpuSetCpus, - containerName(), - Duration.ofSeconds(evalTimeout), - sysOutCharLimit); + dockerService.spawnContainer( + maxMemory, + (long) Math.ceil(cpus), + cpuSetCpus, + containerName(), + Duration.ofSeconds(evalTimeout), + sysOutCharLimit + ); PipedInputStream containerInput = new PipedInputStream(); this.writer = - new BufferedWriter( - new OutputStreamWriter(new PipedOutputStream(containerInput))); + new BufferedWriter( + new OutputStreamWriter(new PipedOutputStream(containerInput)) + ); InputStream containerOutput = - dockerService.startAndAttachToContainer(containerId, containerInput); + dockerService.startAndAttachToContainer(containerId, containerInput); reader = new BufferedReader(new InputStreamReader(containerOutput)); writer.write(sanitize(startupScript)); writer.newLine(); @@ -122,15 +125,17 @@ private JShellResult readResult() throws IOException, NumberFormatException, Doc List snippetResults = new ArrayList<>(); for (int i = 0; i < snippetsCount; i++) { SnippetStatus status = - Utils.nameOrElseThrow( - SnippetStatus.class, - reader.readLine(), - name -> new DockerException(name + " isn't an enum constant")); + Utils.nameOrElseThrow( + SnippetStatus.class, + reader.readLine(), + name -> new DockerException(name + " isn't an enum constant") + ); SnippetType type = - Utils.nameOrElseThrow( - SnippetType.class, - reader.readLine(), - name -> new DockerException(name + " isn't an enum constant")); + Utils.nameOrElseThrow( + SnippetType.class, + reader.readLine(), + name -> new DockerException(name + " isn't an enum constant") + ); int snippetId = Integer.parseInt(reader.readLine()); String source = cleanCode(reader.readLine()); String result = reader.readLine().transform(r -> r.equals("NONE") ? null : r); @@ -140,27 +145,30 @@ private JShellResult readResult() throws IOException, NumberFormatException, Doc String rawAbortionCause = reader.readLine(); if (!rawAbortionCause.isEmpty()) { JShellEvalAbortionCause abortionCause = - switch (rawAbortionCause) { - case "TIMEOUT" -> new JShellEvalAbortionCause.TimeoutAbortionCause(); - case "UNCAUGHT_EXCEPTION" -> { - String[] split = reader.readLine().split(":"); - yield new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - split[0], split[1]); - } - case "COMPILE_TIME_ERROR" -> { - int errorCount = Integer.parseInt(reader.readLine()); - List errors = new ArrayList<>(); - for (int i = 0; i < errorCount; i++) { - errors.add(desanitize(reader.readLine())); - } - yield new JShellEvalAbortionCause.CompileTimeErrorAbortionCause(errors); + switch (rawAbortionCause) { + case "TIMEOUT" -> new JShellEvalAbortionCause.TimeoutAbortionCause(); + case "UNCAUGHT_EXCEPTION" -> { + String[] split = reader.readLine().split(":"); + yield new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( + split[0], + split[1] + ); + } + case "COMPILE_TIME_ERROR" -> { + int errorCount = Integer.parseInt(reader.readLine()); + List errors = new ArrayList<>(); + for (int i = 0; i < errorCount; i++) { + errors.add(desanitize(reader.readLine())); } - case "SYNTAX_ERROR" -> - new JShellEvalAbortionCause.SyntaxErrorAbortionCause(); - default -> - throw new DockerException( - "Abortion cause " + rawAbortionCause + " doesn't exist"); - }; + yield new JShellEvalAbortionCause.CompileTimeErrorAbortionCause(errors); + } + case "SYNTAX_ERROR" -> + new JShellEvalAbortionCause.SyntaxErrorAbortionCause(); + default -> + throw new DockerException( + "Abortion cause " + rawAbortionCause + " doesn't exist" + ); + }; String causeSource = cleanCode(reader.readLine()); String remainingSource = cleanCode(reader.readLine()); abortion = new JShellEvalAbortion(causeSource, remainingSource, abortionCause); @@ -190,9 +198,10 @@ public Optional> snippets(boolean includeStartupScript) throws Dock snippets.add(cleanCode(snippet)); } return Optional.of( - includeStartupScript - ? snippets - : snippets.subList(startupScriptSize, snippets.size())); + includeStartupScript + ? snippets + : snippets.subList(startupScriptSize, snippets.size()) + ); } catch (IOException ex) { LOGGER.warn("Unexpected error.", ex); close(); @@ -208,9 +217,9 @@ public String containerName() { public boolean isInvalidEvalTimeout() { return doingOperation - && lastTimeoutUpdate - .plusSeconds(evalTimeout + evalTimeoutValidationLeeway) - .isBefore(Instant.now()); + && lastTimeoutUpdate + .plusSeconds(evalTimeout + evalTimeoutValidationLeeway) + .isBefore(Instant.now()); } public boolean shouldDie() { @@ -280,7 +289,8 @@ private void checkContainerOK() throws DockerException { close(); } finally { throw new DockerException( - "Container of session " + id + " returned invalid info : " + OK); + "Container of session " + id + " returned invalid info : " + OK + ); } } } catch (IOException ex) { diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java index 5aacbb8..45f7a9c 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java @@ -1,5 +1,10 @@ package org.togetherjava.jshellapi.service; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -10,11 +15,6 @@ import org.togetherjava.jshellapi.Config; import org.togetherjava.jshellapi.exceptions.DockerException; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - @Service public class JShellSessionService { private static final Logger LOGGER = LoggerFactory.getLogger(JShellSessionService.class); @@ -27,25 +27,26 @@ public class JShellSessionService { private void initScheduler() { scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate( - () -> { + () -> { + jshellSessions.keySet().stream() + .filter(id -> jshellSessions.get(id).isClosed()) + .forEach(this::notifyDeath); + List toDie = jshellSessions.keySet().stream() - .filter(id -> jshellSessions.get(id).isClosed()) - .forEach(this::notifyDeath); - List toDie = - jshellSessions.keySet().stream() - .filter(id -> jshellSessions.get(id).shouldDie()) - .toList(); - for (String id : toDie) { - try { - deleteSession(id); - } catch (DockerException ex) { - LOGGER.error("Unexpected error when deleting session.", ex); - } + .filter(id -> jshellSessions.get(id).shouldDie()) + .toList(); + for (String id : toDie) { + try { + deleteSession(id); + } catch (DockerException ex) { + LOGGER.error("Unexpected error when deleting session.", ex); } - }, - config.schedulerSessionKillScanRateSeconds(), - config.schedulerSessionKillScanRateSeconds(), - TimeUnit.SECONDS); + } + }, + config.schedulerSessionKillScanRateSeconds(), + config.schedulerSessionKillScanRateSeconds(), + TimeUnit.SECONDS + ); } void notifyDeath(String id) { @@ -61,8 +62,10 @@ public boolean hasSession(String id) { return jshellSessions.containsKey(id); } - public JShellService session(String id, @Nullable StartupScriptId startupScriptId) - throws DockerException { + public JShellService session( + String id, + @Nullable StartupScriptId startupScriptId + ) throws DockerException { if (!hasSession(id)) { return createSession(new SessionInfo(id, true, startupScriptId, false, config)); } @@ -71,15 +74,28 @@ public JShellService session(String id, @Nullable StartupScriptId startupScriptI public JShellService session(@Nullable StartupScriptId startupScriptId) throws DockerException { return createSession( - new SessionInfo( - UUID.randomUUID().toString(), false, startupScriptId, false, config)); + new SessionInfo( + UUID.randomUUID().toString(), + false, + startupScriptId, + false, + config + ) + ); } - public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId) - throws DockerException { + public JShellService oneTimeSession( + @Nullable StartupScriptId startupScriptId + ) throws DockerException { return createSession( - new SessionInfo( - UUID.randomUUID().toString(), false, startupScriptId, true, config)); + new SessionInfo( + UUID.randomUUID().toString(), + false, + startupScriptId, + true, + config + ) + ); } public void deleteSession(String id) throws DockerException { @@ -88,55 +104,63 @@ public void deleteSession(String id) throws DockerException { scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS); } - private synchronized JShellService createSession(SessionInfo sessionInfo) - throws DockerException { - if (hasSession( + private synchronized JShellService createSession( + SessionInfo sessionInfo + ) throws DockerException { + if ( + hasSession( sessionInfo - .id())) { // Just in case race condition happens just before createSession + .id() + ) + ) { // Just in case race condition happens just before createSession return jshellSessions.get(sessionInfo.id()); } if (jshellSessions.size() >= config.maxAliveSessions()) { throw new ResponseStatusException( - HttpStatus.TOO_MANY_REQUESTS, "Too many sessions, try again later :(."); + HttpStatus.TOO_MANY_REQUESTS, + "Too many sessions, try again later :(." + ); } LOGGER.info("Creating session : {}.", sessionInfo); JShellService service = - new JShellService( - dockerService, - this, - sessionInfo.id(), - sessionInfo.sessionTimeout(), - sessionInfo.renewable(), - sessionInfo.evalTimeout(), - sessionInfo.evalTimeoutValidationLeeway(), - sessionInfo.sysOutCharLimit(), - config.dockerMaxRamMegaBytes(), - config.dockerCPUsUsage(), - config.dockerCPUSetCPUs(), - startupScriptsService.get(sessionInfo.startupScriptId())); + new JShellService( + dockerService, + this, + sessionInfo.id(), + sessionInfo.sessionTimeout(), + sessionInfo.renewable(), + sessionInfo.evalTimeout(), + sessionInfo.evalTimeoutValidationLeeway(), + sessionInfo.sysOutCharLimit(), + config.dockerMaxRamMegaBytes(), + config.dockerCPUsUsage(), + config.dockerCPUSetCPUs(), + startupScriptsService.get(sessionInfo.startupScriptId()) + ); jshellSessions.put(sessionInfo.id(), service); return service; } /** - * Schedule the validation of the session timeout. In case the code runs for too long, checks if - * the wrapper correctly followed the eval timeout and canceled it, if it didn't, forcefully - * close the session. + * Schedule the validation of the session timeout. In case the code runs for too + * long, checks if the wrapper correctly followed the eval timeout and canceled + * it, if it didn't, forcefully close the session. * - * @param id the id of the session + * @param id the id of the session * @param timeSeconds the time to schedule */ public void scheduleEvalTimeoutValidation(String id, long timeSeconds) { scheduler.schedule( - () -> { - JShellService service = jshellSessions.get(id); - if (service == null) return; - if (service.isInvalidEvalTimeout()) { - service.close(); - } - }, - timeSeconds, - TimeUnit.SECONDS); + () -> { + JShellService service = jshellSessions.get(id); + if (service == null) return; + if (service.isInvalidEvalTimeout()) { + service.close(); + } + }, + timeSeconds, + TimeUnit.SECONDS + ); } @Autowired diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java index 631cd87..cbee1da 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java @@ -4,28 +4,31 @@ import org.togetherjava.jshellapi.Config; public record SessionInfo( + String id, + long sessionTimeout, + boolean renewable, + long evalTimeout, + long evalTimeoutValidationLeeway, + int sysOutCharLimit, + @Nullable StartupScriptId startupScriptId +) { + public SessionInfo( String id, - long sessionTimeout, boolean renewable, - long evalTimeout, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - @Nullable StartupScriptId startupScriptId) { - public SessionInfo( - String id, - boolean renewable, - StartupScriptId startupScriptId, - boolean isOneTime, - Config config) { + StartupScriptId startupScriptId, + boolean isOneTime, + Config config + ) { this( - id, - isOneTime - ? config.oneTimeSessionTimeoutSeconds() - : config.regularSessionTimeoutSeconds(), - renewable, - config.evalTimeoutSeconds(), - config.evalTimeoutValidationLeeway(), - config.sysOutCharLimit(), - startupScriptId); + id, + isOneTime + ? config.oneTimeSessionTimeoutSeconds() + : config.regularSessionTimeoutSeconds(), + renewable, + config.evalTimeoutSeconds(), + config.evalTimeoutValidationLeeway(), + config.sysOutCharLimit(), + startupScriptId + ); } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java index fd05211..2420cc3 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java @@ -1,8 +1,5 @@ package org.togetherjava.jshellapi.service; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Service; - import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -11,6 +8,9 @@ import java.util.Map; import java.util.Objects; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + @Service public class StartupScriptsService { @@ -19,11 +19,15 @@ public class StartupScriptsService { private StartupScriptsService() { scripts = new EnumMap<>(StartupScriptId.class); for (StartupScriptId id : StartupScriptId.values()) { - try (InputStream scriptStream = + try ( + InputStream scriptStream = Objects.requireNonNull( - StartupScriptsService.class.getResourceAsStream( - "/jshell_startup/" + id + ".jsh"), - "Couldn't load script " + id)) { + StartupScriptsService.class.getResourceAsStream( + "/jshell_startup/" + id + ".jsh" + ), + "Couldn't load script " + id + ) + ) { String script = new String(scriptStream.readAllBytes(), StandardCharsets.UTF_8); script = cleanEndLines(script); scripts.put(id, script); diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java index d19457e..6642470 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java @@ -8,7 +8,10 @@ public class Utils { public static , X extends Exception> E nameOrElseThrow( - Class c, String name, Function exceptionFunction) throws X { + Class c, + String name, + Function exceptionFunction + ) throws X { return name(c, name).orElseThrow(() -> exceptionFunction.apply(name)); } @@ -17,7 +20,10 @@ public static > Optional name(Class c, String name) { } public static , K> Optional key( - Class c, Function keyMapper, K name) { + Class c, + Function keyMapper, + K name + ) { return predicate(c, e -> keyMapper.apply(e).equals(name)).findAny(); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java index 194ffe3..2906d09 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java @@ -12,7 +12,8 @@ public static Config load() { public Config { if (evalTimeoutSeconds <= 0) throw new IllegalArgumentException( - "Invalid evalTimeoutSeconds : " + evalTimeoutSeconds); + "Invalid evalTimeoutSeconds : " + evalTimeoutSeconds + ); if (sysOutCharLimit <= 0) throw new IllegalArgumentException("Invalid sysOutCharLimit : " + sysOutCharLimit); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java index e45e58a..6417c3d 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java @@ -1,7 +1,7 @@ package org.togetherjava.jshell.wrapper; -import jdk.jshell.SnippetEvent; - import java.util.List; +import jdk.jshell.SnippetEvent; + public record EvalResult(List events, JShellEvalAbortion abortion) {} diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java index 5daddd6..03c43a4 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java @@ -1,4 +1,7 @@ package org.togetherjava.jshell.wrapper; public record JShellEvalAbortion( - String sourceCause, String remainingSource, JShellEvalAbortionCause cause) {} + String sourceCause, + String remainingSource, + JShellEvalAbortionCause cause +) {} diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java index 0d3b8d3..674a500 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java @@ -6,8 +6,10 @@ public sealed interface JShellEvalAbortionCause { record TimeoutAbortionCause() implements JShellEvalAbortionCause {} - record UnhandledExceptionAbortionCause(String exceptionClass, String exceptionMessage) - implements JShellEvalAbortionCause {} + record UnhandledExceptionAbortionCause( + String exceptionClass, + String exceptionMessage + ) implements JShellEvalAbortionCause {} record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause {} diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java index 9c5e7e3..e07a874 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java @@ -1,9 +1,9 @@ package org.togetherjava.jshell.wrapper; -import jdk.jshell.JShell; - import java.util.concurrent.atomic.AtomicBoolean; +import jdk.jshell.JShell; + public record JShellEvalStop(JShell shell, AtomicBoolean hasStopped) implements Runnable { @Override public void run() { diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java index 90dda35..33bd49a 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java @@ -1,7 +1,5 @@ package org.togetherjava.jshell.wrapper; -import jdk.jshell.*; - import java.io.InputStream; import java.io.PrintStream; import java.util.*; @@ -9,11 +7,13 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import jdk.jshell.*; + /** - * Enter eval to evaluate code, snippets to see snippets, exit to stop. How to use : at startup, one - * line need to be sent, startup script, first enter the command, for example eval or snippets, then - * any needed argument. Then "OK" should immediately be sent back, then after some time, the rest of - * the data. + * Enter eval to evaluate code, snippets to see snippets, exit to stop. How to + * use : at startup, one line need to be sent, startup script, first enter the + * command, for example eval or snippets, then any needed argument. Then "OK" + * should immediately be sent back, then after some time, the rest of the data. */ public class JShellWrapper { @@ -50,21 +50,26 @@ private void verifyStartupEval(EvalResult result) { // TODO Replace with switch if (abortion.cause() instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { throw new RuntimeException("Timeout exceeded."); - } else if (abortion.cause() - instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause) { - throw new RuntimeException( - "Following startup script resulted in an exception : " - + sanitize(abortion.sourceCause()), - event.exception()); - } else if (abortion.cause() - instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause) { - throw new RuntimeException( - "Following startup script was REJECTED : " + sanitize(abortion.sourceCause())); - } else if (abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { + } else if ( + abortion.cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause + ) { throw new RuntimeException( - "Following startup script has a syntax error : " - + sanitize(abortion.sourceCause())); - } else throw new AssertionError(); + "Following startup script resulted in an exception : " + + sanitize(abortion.sourceCause()), + event.exception() + ); + } else + if (abortion.cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause) { + throw new RuntimeException( + "Following startup script was REJECTED : " + sanitize(abortion.sourceCause()) + ); + } else + if (abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { + throw new RuntimeException( + "Following startup script has a syntax error : " + + sanitize(abortion.sourceCause()) + ); + } else throw new AssertionError(); } private void ok(PrintStream processOut) { @@ -79,24 +84,31 @@ private EvalResult eval(JShell shell, String code, AtomicBoolean hasStopped) { var completion = shell.sourceCodeAnalysis().analyzeCompletion(clean(code)); if (!completion.completeness().isComplete()) { abortion = - new JShellEvalAbortion( - code, "", new JShellEvalAbortionCause.SyntaxErrorAbortionCause()); + new JShellEvalAbortion( + code, + "", + new JShellEvalAbortionCause.SyntaxErrorAbortionCause() + ); break; } List evalEvents = shell.eval(completion.source()); JShellEvalAbortionCause abortionCause = handleEvents(shell, evalEvents, resultEvents); if (abortionCause != null) { abortion = - new JShellEvalAbortion( - completion.source(), completion.remaining(), abortionCause); + new JShellEvalAbortion( + completion.source(), + completion.remaining(), + abortionCause + ); break; } if (hasStopped.get()) { abortion = - new JShellEvalAbortion( - completion.source(), - completion.remaining(), - new JShellEvalAbortionCause.TimeoutAbortionCause()); + new JShellEvalAbortion( + completion.source(), + completion.remaining(), + new JShellEvalAbortionCause.TimeoutAbortionCause() + ); break; } code = completion.remaining(); @@ -105,7 +117,10 @@ private EvalResult eval(JShell shell, String code, AtomicBoolean hasStopped) { } private JShellEvalAbortionCause handleEvents( - JShell shell, List evalEvents, List resultEvents) { + JShell shell, + List evalEvents, + List resultEvents + ) { for (SnippetEvent event : evalEvents) { if (event.causeSnippet() == null) { // Only keep snippet creation events resultEvents.add(event); @@ -118,24 +133,32 @@ private JShellEvalAbortionCause handleEvents( } private JShellEvalAbortionCause.UnhandledExceptionAbortionCause createExceptionCause( - SnippetEvent event) { + SnippetEvent event + ) { if (event.exception() == null) { return null; } else if (event.exception() instanceof EvalException evalException) { return new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - evalException.getExceptionClassName(), evalException.getMessage()); + evalException.getExceptionClassName(), + evalException.getMessage() + ); } else { return new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - event.exception().getClass().getName(), event.exception().getMessage()); + event.exception().getClass().getName(), + event.exception().getMessage() + ); } } private JShellEvalAbortionCause.CompileTimeErrorAbortionCause createCompileErrorCause( - JShell shell, SnippetEvent event) { + JShell shell, + SnippetEvent event + ) { return new JShellEvalAbortionCause.CompileTimeErrorAbortionCause( - shell.diagnostics(event.snippet()) - .map(d -> sanitize(d.getMessage(Locale.ENGLISH))) - .toList()); + shell.diagnostics(event.snippet()) + .map(d -> sanitize(d.getMessage(Locale.ENGLISH))) + .toList() + ); } /** @@ -157,20 +180,23 @@ private JShellEvalAbortionCause.CompileTimeErrorAbortionCause createCompileError * */ private void eval( - Scanner processIn, - PrintStream processOut, - Config config, - JShell shell, - StringOutputStream jshellOut) { + Scanner processIn, + PrintStream processOut, + Config config, + JShell shell, + StringOutputStream jshellOut + ) { AtomicBoolean hasStopped = new AtomicBoolean(); TimeoutWatcher watcher = - new TimeoutWatcher( - config.evalTimeoutSeconds(), new JShellEvalStop(shell, hasStopped)); + new TimeoutWatcher( + config.evalTimeoutSeconds(), + new JShellEvalStop(shell, hasStopped) + ); int lineCount = Integer.parseInt(processIn.nextLine()); String code = - IntStream.range(0, lineCount) - .mapToObj(i -> processIn.nextLine()) - .collect(Collectors.joining("\n")); + IntStream.range(0, lineCount) + .mapToObj(i -> processIn.nextLine()) + .collect(Collectors.joining("\n")); ok(processOut); watcher.start(); @@ -195,17 +221,21 @@ private List writeEvalResult(EvalResult result, StringOutputStream jshel // TODO replace with switch if (abortion.cause() instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { outBuffer.add("TIMEOUT"); - } else if (abortion.cause() - instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c) { + } else if ( + abortion + .cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c + ) { outBuffer.add("UNCAUGHT_EXCEPTION"); outBuffer.add(getExceptionFromCause(c)); - } else if (abortion.cause() - instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c) { + } else if ( + abortion.cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c + ) { outBuffer.add("COMPILE_TIME_ERROR"); outBuffer.add(String.valueOf(c.errors().size())); outBuffer.addAll(c.errors()); - } else if (abortion.cause() - instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause c) { + } else if ( + abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause c + ) { outBuffer.add("SYNTAX_ERROR"); } else throw new AssertionError(); outBuffer.add(sanitize(abortion.sourceCause())); @@ -232,11 +262,11 @@ private List writeEvalResult(EvalResult result, StringOutputStream jshel */ private void writeEvalSnippetEvent(List outBuffer, SnippetEvent event) { String status = - switch (event.status()) { - case VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED -> - event.status().name(); - default -> throw new RuntimeException("Invalid status"); - }; + switch (event.status()) { + case VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED -> + event.status().name(); + default -> throw new RuntimeException("Invalid status"); + }; outBuffer.add(status); if (event.previousStatus() == Snippet.Status.NONEXISTENT) { outBuffer.add("ADDITION"); @@ -249,7 +279,8 @@ private void writeEvalSnippetEvent(List outBuffer, SnippetEvent event) { } private String getExceptionFromCause( - JShellEvalAbortionCause.UnhandledExceptionAbortionCause cause) { + JShellEvalAbortionCause.UnhandledExceptionAbortionCause cause + ) { return sanitize(cause.exceptionClass() + ":" + cause.exceptionMessage()); } @@ -268,9 +299,9 @@ private String getExceptionFromCause( private void snippets(PrintStream processOut, JShell shell) { ok(processOut); shell.snippets() - .map(Snippet::source) - .map(JShellWrapper::sanitize) - .forEach(processOut::println); + .map(Snippet::source) + .map(JShellWrapper::sanitize) + .forEach(processOut::println); processOut.println(); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java index 5ca0fb4..42449dd 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java @@ -56,15 +56,17 @@ public void write(byte[] b, int off, int len) { public Result readAll() { if (index > bytes.length) throw new IllegalStateException(); // Should never happen String s = - new String( - index == bytes.length ? bytes : Arrays.copyOf(bytes, index), - StandardCharsets.UTF_8); + new String( + index == bytes.length ? bytes : Arrays.copyOf(bytes, index), + StandardCharsets.UTF_8 + ); index = 0; if (byteOverflow) { byteOverflow = false; return new Result( - s.charAt(s.length() - 1) == UNKNOWN_CHAR ? s.substring(0, s.length() - 1) : s, - true); + s.charAt(s.length() - 1) == UNKNOWN_CHAR ? s.substring(0, s.length() - 1) : s, + true + ); } if (s.length() > maxSize) return new Result(s.substring(0, maxSize), true); return new Result(s, false); diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java index 3f08d8e..1e5ac65 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java @@ -6,15 +6,15 @@ public class TimeoutWatcher { public TimeoutWatcher(int timeoutSeconds, Runnable timeoutAction) { Runnable runnable = - () -> { - try { - Thread.sleep(timeoutSeconds * 1000L); - } catch (InterruptedException e) { // Stopped - return; - } - timeout = true; - timeoutAction.run(); - }; + () -> { + try { + Thread.sleep(timeoutSeconds * 1000L); + } catch (InterruptedException e) { // Stopped + return; + } + timeout = true; + timeoutAction.run(); + }; thread = new Thread(runnable); thread.setName("Timeout Watcher"); } diff --git a/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java b/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java index 4825e8c..27061e3 100644 --- a/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java +++ b/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java @@ -1,28 +1,29 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.PrintStream; + import org.junit.jupiter.api.Test; import org.togetherjava.jshell.wrapper.Config; import org.togetherjava.jshell.wrapper.JShellWrapper; -import java.io.PrintStream; - class JShellWrapperStartupScriptTest { @Test void testDoubleSnippets() { Config config = new Config(5, 1024); StringInputStream inputStream = - new StringInputStream( - """ - import java.util.*; void println(Object o) { System.out.println(o); } - eval - 1 - println(List.of("a", "b", "c")) - exit"""); + new StringInputStream( + """ + import java.util.*; void println(Object o) { System.out.println(o); } + eval + 1 + println(List.of("a", "b", "c")) + exit""" + ); UnboundStringOutputStream outputStream = new UnboundStringOutputStream(); JShellWrapper jshell = new JShellWrapper(); jshell.run(config, inputStream, new PrintStream(outputStream)); assertEquals( - """ + """ OK 2 OK @@ -37,6 +38,7 @@ void testDoubleSnippets() { [a, b, c]\\n OK """, - outputStream.readAll()); + outputStream.readAll() + ); } } diff --git a/JShellWrapper/src/test/java/JShellWrapperTest.java b/JShellWrapper/src/test/java/JShellWrapperTest.java index 86fe8e8..784aaf3 100644 --- a/JShellWrapper/src/test/java/JShellWrapperTest.java +++ b/JShellWrapper/src/test/java/JShellWrapperTest.java @@ -1,13 +1,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.InputStream; +import java.io.PrintStream; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.togetherjava.jshell.wrapper.Config; import org.togetherjava.jshell.wrapper.JShellWrapper; -import java.io.InputStream; -import java.io.PrintStream; - class JShellWrapperTest { static Config config; static JShellWrapper jshell; @@ -29,11 +29,11 @@ void evalTest(String input, String expectedOutput) { @Test void testHelloWorld() { evalTest( - """ - eval - 1 - System.out.println("Hello world!")""", - """ + """ + eval + 1 + System.out.println("Hello world!")""", + """ OK 0 OK @@ -45,20 +45,21 @@ void testHelloWorld() { false - Hello world!\\n"""); + Hello world!\\n""" + ); } @Test void testMultilinesInput() { evalTest( - """ - eval - 4 - for(int i = 0; i < 10; i++) { - System.out.print(i); - } - System.out.println();""", - """ + """ + eval + 4 + for(int i = 0; i < 10; i++) { + System.out.print(i); + } + System.out.println();""", + """ OK 0 OK @@ -75,17 +76,18 @@ void testMultilinesInput() { false - 0123456789\\n"""); + 0123456789\\n""" + ); } @Test void testStdoutOverflow() { evalTest( - """ - eval - 1 - for(int i = 0; i < 1024; i++) System.out.print(0)""", - """ + """ + eval + 1 + for(int i = 0; i < 1024; i++) System.out.print(0)""", + """ OK 0 OK @@ -98,13 +100,14 @@ void testStdoutOverflow() { false %s""" - .formatted("0".repeat(1024))); + .formatted("0".repeat(1024)) + ); evalTest( - """ - eval - 1 - for(int i = 0; i <= 1024; i++) System.out.print(0)""", - """ + """ + eval + 1 + for(int i = 0; i <= 1024; i++) System.out.print(0)""", + """ OK 0 OK @@ -117,18 +120,19 @@ void testStdoutOverflow() { true %s""" - .formatted("0".repeat(1024))); + .formatted("0".repeat(1024)) + ); } @Test void testModificationAndMultiplesSnippets() { evalTest( - """ - eval - 2 - int i = 0; - int i = 2;""", - """ + """ + eval + 2 + int i = 0; + int i = 2;""", + """ OK 0 OK @@ -145,17 +149,18 @@ void testModificationAndMultiplesSnippets() { 2 false - """); + """ + ); } @Test void testUseId() { evalTest( - """ - eval - 1 - System.out.println("Hello world!")""", - """ + """ + eval + 1 + System.out.println("Hello world!")""", + """ OK 0 OK @@ -167,17 +172,18 @@ void testUseId() { false - Hello world!\\n"""); + Hello world!\\n""" + ); } @Test void testTimeout() { evalTest( - """ - eval - 1 - while(true);""", - """ + """ + eval + 1 + while(true);""", + """ OK 0 OK @@ -191,17 +197,18 @@ void testTimeout() { while(true); false - """); + """ + ); } @Test void testUncaughtException() { // TODO other kind of exception, not in EvalException evalTest( - """ - eval - 1 - throw new RuntimeException("Some message : fail")""", - """ + """ + eval + 1 + throw new RuntimeException("Some message : fail")""", + """ OK 0 OK @@ -216,17 +223,18 @@ void testUncaughtException() { // TODO other kind of exception, not in EvalExcep throw new RuntimeException("Some message : fail"); false - """); + """ + ); } @Test void testRejected() { evalTest( - """ - eval - 1 - print""", - """ + """ + eval + 1 + print""", + """ OK 0 OK @@ -242,18 +250,19 @@ void testRejected() { print false - """); + """ + ); } @Test void testSyntaxError() { // DEFINITELY_INCOMPLETE evalTest( - """ - eval - 1 - print(""", - """ + """ + eval + 1 + print(""", + """ OK 0 OK @@ -262,14 +271,15 @@ void testSyntaxError() { print( false - """); + """ + ); // CONSIDERED_INCOMPLETE evalTest( - """ - eval - 1 - while(true)""", - """ + """ + eval + 1 + while(true)""", + """ OK 0 OK @@ -278,13 +288,14 @@ void testSyntaxError() { while(true) false - """); - evalTest( - """ - eval - 1 - for(int i = 0; i < 10; i++)""", """ + ); + evalTest( + """ + eval + 1 + for(int i = 0; i < 10; i++)""", + """ OK 0 OK @@ -293,19 +304,20 @@ void testSyntaxError() { for(int i = 0; i < 10; i++) false - """); + """ + ); } @Test void testRejectedAndMultiples() { evalTest( - """ - eval - 3 - int i = 0; - print; - System.out.println(i);""", - """ + """ + eval + 3 + int i = 0; + print; + System.out.println(i);""", + """ OK 0 OK @@ -326,19 +338,20 @@ void testRejectedAndMultiples() { \\nprint; \\nSystem.out.println(i); false - """); + """ + ); } @Test void testMultilinesAndHardcodedNewLineInString() { evalTest( - """ - eval - 3 - { - System.out.println("\\n"); - }""", - """ + """ + eval + 3 + { + System.out.println("\\n"); + }""", + """ OK 0 OK @@ -350,6 +363,7 @@ void testMultilinesAndHardcodedNewLineInString() { false - \\n\\n"""); + \\n\\n""" + ); } } diff --git a/JShellWrapper/src/test/java/StringOutputStreamTest.java b/JShellWrapper/src/test/java/StringOutputStreamTest.java index f106986..eab7987 100644 --- a/JShellWrapper/src/test/java/StringOutputStreamTest.java +++ b/JShellWrapper/src/test/java/StringOutputStreamTest.java @@ -1,12 +1,12 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.charset.StandardCharsets; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.togetherjava.jshell.wrapper.StringOutputStream; -import java.nio.charset.StandardCharsets; - class StringOutputStreamTest { static final String E_ACUTE = "\u00E9"; static final String SMILEY = "\uD83D\uDE0A"; @@ -84,14 +84,16 @@ void testWriteOverload() { final String eAcuteX5AndSmileyX5 = E_ACUTE.repeat(5) + SMILEY.repeat(5); stream.write( - eAcuteX5AndSmileyX5.getBytes(StandardCharsets.UTF_8), 2 * 3, (2 * 2) + (4 * 4)); + eAcuteX5AndSmileyX5.getBytes(StandardCharsets.UTF_8), 2 * 3, (2 * 2) + (4 * 4) + ); assertResult(false, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll()); final String eAcuteX5AndSmileyX5AndA = E_ACUTE.repeat(5) + SMILEY.repeat(5) + 'a'; stream.write( - eAcuteX5AndSmileyX5AndA.getBytes(StandardCharsets.UTF_8), - 2 * 3, - (2 * 2) + (4 * 4) + 1); + eAcuteX5AndSmileyX5AndA.getBytes(StandardCharsets.UTF_8), + 2 * 3, + (2 * 2) + (4 * 4) + 1 + ); assertResult(true, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll()); } diff --git a/build.gradle b/build.gradle index c38eabb..d5ec115 100644 --- a/build.gradle +++ b/build.gradle @@ -16,16 +16,12 @@ subprojects { spotless { java { - importOrder() - googleJavaFormat() - .aosp() - .reflowLongStrings() - .formatJavadoc(true) - .reorderImports(true) - .groupArtifact('com.google.googlejavaformat:google-java-format') + target('**/java/**/*.java') trimTrailingWhitespace() - indentWithSpaces() - endWithNewline() + removeUnusedImports() + // '\\#' = static imports, '' = everything else + importOrder('\\#', 'java', 'javax', 'com.javadiscord', '') + eclipse().configFile("${rootProject.rootDir}/spotless.xml") } } @@ -68,4 +64,4 @@ subprojects { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' } -} \ No newline at end of file +} diff --git a/spotless.xml b/spotless.xml new file mode 100644 index 0000000..8af22c7 --- /dev/null +++ b/spotless.xml @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f8de6a564304c07018b5e93db5a01fbe17dd4c04 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 16:42:00 +0530 Subject: [PATCH 2/3] rewrite if --- .../jshellapi/service/JShellSessionService.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java index 45f7a9c..b3f54ec 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java @@ -107,12 +107,8 @@ public void deleteSession(String id) throws DockerException { private synchronized JShellService createSession( SessionInfo sessionInfo ) throws DockerException { - if ( - hasSession( - sessionInfo - .id() - ) - ) { // Just in case race condition happens just before createSession + // Just in case race condition happens just before createSession + if (hasSession(sessionInfo.id())) { return jshellSessions.get(sessionInfo.id()); } if (jshellSessions.size() >= config.maxAliveSessions()) { From aa677d18aaa8783d3a6ccc393ac4767af753eb9d Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Fri, 24 May 2024 05:01:47 +0530 Subject: [PATCH 3/3] tj bot spotless config --- .../org/togetherjava/jshellapi/Config.java | 22 +- .../jshellapi/dto/JShellEvalAbortion.java | 8 +- .../dto/JShellEvalAbortionCause.java | 20 +- .../jshellapi/dto/JShellResult.java | 12 +- .../jshellapi/dto/JShellResultWithId.java | 3 +- .../jshellapi/dto/JShellSnippetResult.java | 10 +- .../jshellapi/exceptions/DockerException.java | 8 +- .../jshellapi/rest/JShellController.java | 79 +- .../jshellapi/service/DockerService.java | 116 ++- .../jshellapi/service/JShellService.java | 132 ++-- .../service/JShellSessionService.java | 142 ++-- .../jshellapi/service/SessionInfo.java | 36 +- .../service/StartupScriptsService.java | 18 +- .../togetherjava/jshellapi/service/Utils.java | 14 +- .../togetherjava/jshell/wrapper/Config.java | 3 +- .../jshell/wrapper/EvalResult.java | 7 +- .../jshell/wrapper/JShellEvalAbortion.java | 8 +- .../wrapper/JShellEvalAbortionCause.java | 16 +- .../jshell/wrapper/JShellEvalStop.java | 4 +- .../jshell/wrapper/JShellWrapper.java | 156 ++-- .../jshell/wrapper/StringOutputStream.java | 21 +- .../jshell/wrapper/TimeoutWatcher.java | 19 +- .../java/JShellWrapperStartupScriptTest.java | 31 +- .../src/test/java/JShellWrapperTest.java | 139 ++-- .../src/test/java/StringInputStream.java | 3 +- .../src/test/java/StringOutputStreamTest.java | 21 +- build.gradle | 13 +- spotless.xml | 678 ++++++++---------- 28 files changed, 709 insertions(+), 1030 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java index 37669ec..4c337e9 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java @@ -4,20 +4,11 @@ import org.springframework.lang.Nullable; @ConfigurationProperties("jshellapi") -public record Config( - long regularSessionTimeoutSeconds, - long oneTimeSessionTimeoutSeconds, - long evalTimeoutSeconds, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - long maxAliveSessions, - int dockerMaxRamMegaBytes, - double dockerCPUsUsage, - @Nullable String dockerCPUSetCPUs, - long schedulerSessionKillScanRateSeconds, - long dockerResponseTimeout, - long dockerConnectionTimeout -) { +public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeoutSeconds, + long evalTimeoutSeconds, long evalTimeoutValidationLeeway, int sysOutCharLimit, + long maxAliveSessions, int dockerMaxRamMegaBytes, double dockerCPUsUsage, + @Nullable String dockerCPUSetCPUs, long schedulerSessionKillScanRateSeconds, + long dockerResponseTimeout, long dockerConnectionTimeout) { public Config { if (regularSessionTimeoutSeconds <= 0) throw new IllegalArgumentException("Invalid value " + regularSessionTimeoutSeconds); @@ -39,8 +30,7 @@ public record Config( throw new IllegalArgumentException("Invalid value " + dockerCPUSetCPUs); if (schedulerSessionKillScanRateSeconds <= 0) throw new IllegalArgumentException( - "Invalid value " + schedulerSessionKillScanRateSeconds - ); + "Invalid value " + schedulerSessionKillScanRateSeconds); if (dockerResponseTimeout <= 0) throw new IllegalArgumentException("Invalid value " + dockerResponseTimeout); if (dockerConnectionTimeout <= 0) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java index fca4693..ba04fbd 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortion.java @@ -1,7 +1,5 @@ package org.togetherjava.jshellapi.dto; -public record JShellEvalAbortion( - String sourceCause, - String remainingSource, - JShellEvalAbortionCause cause -) {} +public record JShellEvalAbortion(String sourceCause, String remainingSource, + JShellEvalAbortionCause cause) { +} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java index 511619e..0fed77b 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellEvalAbortionCause.java @@ -1,25 +1,27 @@ package org.togetherjava.jshellapi.dto; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import java.util.List; + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) public sealed interface JShellEvalAbortionCause { @JsonTypeName("TIMEOUT") - record TimeoutAbortionCause() implements JShellEvalAbortionCause {} + record TimeoutAbortionCause() implements JShellEvalAbortionCause { + } @JsonTypeName("UNCAUGHT_EXCEPTION") - record UnhandledExceptionAbortionCause( - String exceptionClass, - String exceptionMessage - ) implements JShellEvalAbortionCause {} + record UnhandledExceptionAbortionCause(String exceptionClass, + String exceptionMessage) implements JShellEvalAbortionCause { + } @JsonTypeName("COMPILE_TIME_ERROR") - record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause {} + record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause { + } @JsonTypeName("SYNTAX_ERROR") - record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause {} + record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause { + } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java index 3c81f99..6ab2bc6 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java @@ -1,15 +1,11 @@ package org.togetherjava.jshellapi.dto; -import java.util.List; - import org.springframework.lang.Nullable; -public record JShellResult( - List snippetsResults, - @Nullable JShellEvalAbortion abortion, - boolean stdoutOverflow, - String stdout -) { +import java.util.List; + +public record JShellResult(List snippetsResults, + @Nullable JShellEvalAbortion abortion, boolean stdoutOverflow, String stdout) { public JShellResult { snippetsResults = List.copyOf(snippetsResults); } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResultWithId.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResultWithId.java index e0e4d6f..be9c7a3 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResultWithId.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResultWithId.java @@ -1,3 +1,4 @@ package org.togetherjava.jshellapi.dto; -public record JShellResultWithId(String id, JShellResult result) {} +public record JShellResultWithId(String id, JShellResult result) { +} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java index 107d43c..3409ccb 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellSnippetResult.java @@ -2,10 +2,6 @@ import org.springframework.lang.Nullable; -public record JShellSnippetResult( - SnippetStatus status, - SnippetType type, - int id, - String source, - @Nullable String result -) {} +public record JShellSnippetResult(SnippetStatus status, SnippetType type, int id, String source, + @Nullable String result) { +} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java index 30fa2d6..331ff36 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/exceptions/DockerException.java @@ -19,12 +19,8 @@ public DockerException(Throwable cause) { super(cause); } - public DockerException( - String message, - Throwable cause, - boolean enableSuppression, - boolean writableStackTrace - ) { + public DockerException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 6dc539f..2c60570 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -1,11 +1,10 @@ package org.togetherjava.jshellapi.rest; -import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; + import org.togetherjava.jshellapi.dto.JShellResult; import org.togetherjava.jshellapi.dto.JShellResultWithId; import org.togetherjava.jshellapi.exceptions.DockerException; @@ -14,6 +13,8 @@ import org.togetherjava.jshellapi.service.StartupScriptId; import org.togetherjava.jshellapi.service.StartupScriptsService; +import java.util.List; + @RequestMapping("jshell") @RestController public class JShellController { @@ -21,73 +22,45 @@ public class JShellController { private StartupScriptsService startupScriptsService; @PostMapping("/eval/{id}") - public JShellResult eval( - @PathVariable String id, - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code - ) throws DockerException { + public JShellResult eval(@PathVariable String id, + @RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code) throws DockerException { validateId(id); return service.session(id, startupScriptId) .eval(code) - .orElseThrow( - () -> new ResponseStatusException( - HttpStatus.CONFLICT, - "An operation is already running" - ) - ); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, + "An operation is already running")); } @PostMapping("/eval") - public JShellResultWithId eval( - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code - ) throws DockerException { + public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code) throws DockerException { JShellService jShellService = service.session(startupScriptId); - return new JShellResultWithId( - jShellService.id(), - jShellService - .eval(code) - .orElseThrow( - () -> new ResponseStatusException( - HttpStatus.CONFLICT, - "An operation is already running" - ) - ) - ); + return new JShellResultWithId(jShellService.id(), + jShellService.eval(code) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, + "An operation is already running"))); } @PostMapping("/single-eval") - public JShellResult singleEval( - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code - ) throws DockerException { + public JShellResult singleEval(@RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code) throws DockerException { JShellService jShellService = service.oneTimeSession(startupScriptId); - return jShellService - .eval(code) - .orElseThrow( - () -> new ResponseStatusException( - HttpStatus.CONFLICT, - "An operation is already running" - ) - ); + return jShellService.eval(code) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, + "An operation is already running")); } @GetMapping("/snippets/{id}") - public List snippets( - @PathVariable String id, - @RequestParam(required = false) boolean includeStartupScript - ) throws DockerException { + public List snippets(@PathVariable String id, + @RequestParam(required = false) boolean includeStartupScript) throws DockerException { validateId(id); if (!service.hasSession(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); return service.session(id, null) .snippets(includeStartupScript) - .orElseThrow( - () -> new ResponseStatusException( - HttpStatus.CONFLICT, - "An operation is already running" - ) - ); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, + "An operation is already running")); } @DeleteMapping("/{id}") @@ -115,10 +88,8 @@ public void setStartupScriptsService(StartupScriptsService startupScriptsService private static void validateId(String id) throws ResponseStatusException { if (!id.matches("[a-zA-Z0-9][a-zA-Z0-9_.-]+")) { - throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, - "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+" - ); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+"); } } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java index 44c6950..c5abcbb 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java @@ -1,11 +1,5 @@ package org.togetherjava.jshellapi.service; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.TimeUnit; - import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.PullImageResultCallback; @@ -18,8 +12,15 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; + import org.togetherjava.jshellapi.Config; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.TimeUnit; + @Service public class DockerService implements DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger(DockerService.class); @@ -30,24 +31,24 @@ public class DockerService implements DisposableBean { public DockerService(Config config) { DefaultDockerClientConfig clientConfig = - DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DefaultDockerClientConfig.createDefaultConfigBuilder().build(); ApacheDockerHttpClient httpClient = - new ApacheDockerHttpClient.Builder() - .dockerHost(clientConfig.getDockerHost()) - .sslConfig(clientConfig.getSSLConfig()) - .responseTimeout(Duration.ofSeconds(config.dockerResponseTimeout())) - .connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout())) - .build(); + new ApacheDockerHttpClient.Builder().dockerHost(clientConfig.getDockerHost()) + .sslConfig(clientConfig.getSSLConfig()) + .responseTimeout(Duration.ofSeconds(config.dockerResponseTimeout())) + .connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout())) + .build(); this.client = DockerClientImpl.getInstance(clientConfig, httpClient); cleanupLeftovers(WORKER_UNIQUE_ID); } private void cleanupLeftovers(UUID currentId) { - for (Container container : client.listContainersCmd().withLabelFilter(Set.of(WORKER_LABEL)) + for (Container container : client.listContainersCmd() + .withLabelFilter(Set.of(WORKER_LABEL)) .exec()) { String containerHumanName = - container.getId() + " " + Arrays.toString(container.getNames()); + container.getId() + " " + Arrays.toString(container.getNames()); LOGGER.info("Found worker container '{}'", containerHumanName); if (!container.getLabels().get(WORKER_LABEL).equals(currentId.toString())) { LOGGER.info("Killing container '{}'", containerHumanName); @@ -56,19 +57,15 @@ private void cleanupLeftovers(UUID currentId) { } } - public String spawnContainer( - long maxMemoryMegs, - long cpus, - @Nullable String cpuSetCpus, - String name, - Duration evalTimeout, - long sysoutLimit - ) throws InterruptedException { + public String spawnContainer(long maxMemoryMegs, long cpus, @Nullable String cpuSetCpus, + String name, Duration evalTimeout, long sysoutLimit) throws InterruptedException { String imageName = "togetherjava.org:5001/togetherjava/jshellwrapper"; - boolean presentLocally = - client.listImagesCmd().withFilter("reference", List.of(imageName)).exec().stream() - .flatMap(it -> Arrays.stream(it.getRepoTags())) - .anyMatch(it -> it.endsWith(":master")); + boolean presentLocally = client.listImagesCmd() + .withFilter("reference", List.of(imageName)) + .exec() + .stream() + .flatMap(it -> Arrays.stream(it.getRepoTags())) + .anyMatch(it -> it.endsWith(":master")); if (!presentLocally) { client.pullImageCmd(imageName) @@ -78,36 +75,30 @@ public String spawnContainer( } return client.createContainerCmd(imageName + ":master") - .withHostConfig( - HostConfig.newHostConfig() - .withAutoRemove(true) - .withInit(true) - .withCapDrop(Capability.ALL) - .withNetworkMode("none") - .withPidsLimit(2000L) - .withReadonlyRootfs(true) - .withMemory(maxMemoryMegs * 1024 * 1024) - .withCpuCount(cpus) - .withCpusetCpus(cpuSetCpus) - ) + .withHostConfig(HostConfig.newHostConfig() + .withAutoRemove(true) + .withInit(true) + .withCapDrop(Capability.ALL) + .withNetworkMode("none") + .withPidsLimit(2000L) + .withReadonlyRootfs(true) + .withMemory(maxMemoryMegs * 1024 * 1024) + .withCpuCount(cpus) + .withCpusetCpus(cpuSetCpus)) .withStdinOpen(true) .withAttachStdin(true) .withAttachStderr(true) .withAttachStdout(true) - .withEnv( - "evalTimeoutSeconds=" + evalTimeout.toSeconds(), - "sysOutCharLimit=" + sysoutLimit - ) + .withEnv("evalTimeoutSeconds=" + evalTimeout.toSeconds(), + "sysOutCharLimit=" + sysoutLimit) .withLabels(Map.of(WORKER_LABEL, WORKER_UNIQUE_ID.toString())) .withName(name) .exec() .getId(); } - public InputStream startAndAttachToContainer( - String containerId, - InputStream stdin - ) throws IOException { + public InputStream startAndAttachToContainer(String containerId, InputStream stdin) + throws IOException { PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); @@ -117,28 +108,23 @@ public InputStream startAndAttachToContainer( .withStdOut(true) .withStdErr(true) .withStdIn(stdin) - .exec( - new ResultCallback.Adapter<>() { - @Override - public void onNext(Frame object) { - try { - String payloadString = + .exec(new ResultCallback.Adapter<>() { + @Override + public void onNext(Frame object) { + try { + String payloadString = new String(object.getPayload(), StandardCharsets.UTF_8); - if (object.getStreamType() == StreamType.STDOUT) { - pipeOut.write(object.getPayload()); - } else { - LOGGER.warn( - "Received STDERR from container {}: {}", - containerId, - payloadString - ); - } - } catch (IOException e) { - throw new UncheckedIOException(e); + if (object.getStreamType() == StreamType.STDOUT) { + pipeOut.write(object.getPayload()); + } else { + LOGGER.warn("Received STDERR from container {}: {}", containerId, + payloadString); } + } catch (IOException e) { + throw new UncheckedIOException(e); } } - ); + }); client.startContainerCmd(containerId).exec(); return pipeIn; diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java index a569458..30631a9 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java @@ -1,19 +1,20 @@ package org.togetherjava.jshellapi.service; -import java.io.*; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - import org.apache.tomcat.util.http.fileupload.util.Closeable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.lang.Nullable; + import org.togetherjava.jshellapi.dto.*; import org.togetherjava.jshellapi.exceptions.DockerException; +import java.io.*; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + public class JShellService implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(JShellService.class); private final JShellSessionService sessionService; @@ -30,21 +31,10 @@ public class JShellService implements Closeable { private final DockerService dockerService; private final int startupScriptSize; - public JShellService( - DockerService dockerService, - JShellSessionService sessionService, - String id, - long timeout, - boolean renewable, - long evalTimeout, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - int maxMemory, - double cpus, - @Nullable String cpuSetCpus, - String startupScript - ) - throws DockerException { + public JShellService(DockerService dockerService, JShellSessionService sessionService, + String id, long timeout, boolean renewable, long evalTimeout, + long evalTimeoutValidationLeeway, int sysOutCharLimit, int maxMemory, double cpus, + @Nullable String cpuSetCpus, String startupScript) throws DockerException { this.dockerService = dockerService; this.sessionService = sessionService; this.id = id; @@ -58,22 +48,13 @@ public JShellService( throw new DockerException("The session isn't completely destroyed, try again later."); } try { - String containerId = - dockerService.spawnContainer( - maxMemory, - (long) Math.ceil(cpus), - cpuSetCpus, - containerName(), - Duration.ofSeconds(evalTimeout), - sysOutCharLimit - ); + String containerId = dockerService.spawnContainer(maxMemory, (long) Math.ceil(cpus), + cpuSetCpus, containerName(), Duration.ofSeconds(evalTimeout), sysOutCharLimit); PipedInputStream containerInput = new PipedInputStream(); - this.writer = - new BufferedWriter( - new OutputStreamWriter(new PipedOutputStream(containerInput)) - ); + this.writer = new BufferedWriter( + new OutputStreamWriter(new PipedOutputStream(containerInput))); InputStream containerOutput = - dockerService.startAndAttachToContainer(containerId, containerInput); + dockerService.startAndAttachToContainer(containerId, containerInput); reader = new BufferedReader(new InputStreamReader(containerOutput)); writer.write(sanitize(startupScript)); writer.newLine(); @@ -99,7 +80,8 @@ public Optional eval(String code) throws DockerException { } updateLastTimeout(); sessionService.scheduleEvalTimeoutValidation(id, evalTimeout + evalTimeoutValidationLeeway); - if (!code.endsWith("\n")) code += '\n'; + if (!code.endsWith("\n")) + code += '\n'; try { writer.write("eval"); writer.newLine(); @@ -124,18 +106,10 @@ private JShellResult readResult() throws IOException, NumberFormatException, Doc final int snippetsCount = Integer.parseInt(reader.readLine()); List snippetResults = new ArrayList<>(); for (int i = 0; i < snippetsCount; i++) { - SnippetStatus status = - Utils.nameOrElseThrow( - SnippetStatus.class, - reader.readLine(), - name -> new DockerException(name + " isn't an enum constant") - ); - SnippetType type = - Utils.nameOrElseThrow( - SnippetType.class, - reader.readLine(), - name -> new DockerException(name + " isn't an enum constant") - ); + SnippetStatus status = Utils.nameOrElseThrow(SnippetStatus.class, reader.readLine(), + name -> new DockerException(name + " isn't an enum constant")); + SnippetType type = Utils.nameOrElseThrow(SnippetType.class, reader.readLine(), + name -> new DockerException(name + " isn't an enum constant")); int snippetId = Integer.parseInt(reader.readLine()); String source = cleanCode(reader.readLine()); String result = reader.readLine().transform(r -> r.equals("NONE") ? null : r); @@ -144,31 +118,25 @@ private JShellResult readResult() throws IOException, NumberFormatException, Doc JShellEvalAbortion abortion = null; String rawAbortionCause = reader.readLine(); if (!rawAbortionCause.isEmpty()) { - JShellEvalAbortionCause abortionCause = - switch (rawAbortionCause) { - case "TIMEOUT" -> new JShellEvalAbortionCause.TimeoutAbortionCause(); - case "UNCAUGHT_EXCEPTION" -> { - String[] split = reader.readLine().split(":"); - yield new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - split[0], - split[1] - ); - } - case "COMPILE_TIME_ERROR" -> { - int errorCount = Integer.parseInt(reader.readLine()); - List errors = new ArrayList<>(); - for (int i = 0; i < errorCount; i++) { - errors.add(desanitize(reader.readLine())); - } - yield new JShellEvalAbortionCause.CompileTimeErrorAbortionCause(errors); + JShellEvalAbortionCause abortionCause = switch (rawAbortionCause) { + case "TIMEOUT" -> new JShellEvalAbortionCause.TimeoutAbortionCause(); + case "UNCAUGHT_EXCEPTION" -> { + String[] split = reader.readLine().split(":"); + yield new JShellEvalAbortionCause.UnhandledExceptionAbortionCause(split[0], + split[1]); + } + case "COMPILE_TIME_ERROR" -> { + int errorCount = Integer.parseInt(reader.readLine()); + List errors = new ArrayList<>(); + for (int i = 0; i < errorCount; i++) { + errors.add(desanitize(reader.readLine())); } - case "SYNTAX_ERROR" -> - new JShellEvalAbortionCause.SyntaxErrorAbortionCause(); - default -> - throw new DockerException( - "Abortion cause " + rawAbortionCause + " doesn't exist" - ); - }; + yield new JShellEvalAbortionCause.CompileTimeErrorAbortionCause(errors); + } + case "SYNTAX_ERROR" -> new JShellEvalAbortionCause.SyntaxErrorAbortionCause(); + default -> throw new DockerException( + "Abortion cause " + rawAbortionCause + " doesn't exist"); + }; String causeSource = cleanCode(reader.readLine()); String remainingSource = cleanCode(reader.readLine()); abortion = new JShellEvalAbortion(causeSource, remainingSource, abortionCause); @@ -197,11 +165,8 @@ public Optional> snippets(boolean includeStartupScript) throws Dock while (!(snippet = reader.readLine()).isEmpty()) { snippets.add(cleanCode(snippet)); } - return Optional.of( - includeStartupScript - ? snippets - : snippets.subList(startupScriptSize, snippets.size()) - ); + return Optional.of(includeStartupScript ? snippets + : snippets.subList(startupScriptSize, snippets.size())); } catch (IOException ex) { LOGGER.warn("Unexpected error.", ex); close(); @@ -217,9 +182,8 @@ public String containerName() { public boolean isInvalidEvalTimeout() { return doingOperation - && lastTimeoutUpdate - .plusSeconds(evalTimeout + evalTimeoutValidationLeeway) - .isBefore(Instant.now()); + && lastTimeoutUpdate.plusSeconds(evalTimeout + evalTimeoutValidationLeeway) + .isBefore(Instant.now()); } public boolean shouldDie() { @@ -289,8 +253,7 @@ private void checkContainerOK() throws DockerException { close(); } finally { throw new DockerException( - "Container of session " + id + " returned invalid info : " + OK - ); + "Container of session " + id + " returned invalid info : " + OK); } } } catch (IOException ex) { @@ -300,7 +263,8 @@ private void checkContainerOK() throws DockerException { } private synchronized boolean tryStartOperation() { - if (doingOperation) return false; + if (doingOperation) + return false; doingOperation = true; return true; } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java index b3f54ec..c6411ef 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java @@ -1,10 +1,5 @@ package org.togetherjava.jshellapi.service; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -12,9 +7,15 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; + import org.togetherjava.jshellapi.Config; import org.togetherjava.jshellapi.exceptions.DockerException; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + @Service public class JShellSessionService { private static final Logger LOGGER = LoggerFactory.getLogger(JShellSessionService.class); @@ -26,32 +27,30 @@ public class JShellSessionService { private void initScheduler() { scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.scheduleAtFixedRate( - () -> { - jshellSessions.keySet().stream() - .filter(id -> jshellSessions.get(id).isClosed()) - .forEach(this::notifyDeath); - List toDie = - jshellSessions.keySet().stream() - .filter(id -> jshellSessions.get(id).shouldDie()) - .toList(); - for (String id : toDie) { - try { - deleteSession(id); - } catch (DockerException ex) { - LOGGER.error("Unexpected error when deleting session.", ex); - } + scheduler.scheduleAtFixedRate(() -> { + jshellSessions.keySet() + .stream() + .filter(id -> jshellSessions.get(id).isClosed()) + .forEach(this::notifyDeath); + List toDie = jshellSessions.keySet() + .stream() + .filter(id -> jshellSessions.get(id).shouldDie()) + .toList(); + for (String id : toDie) { + try { + deleteSession(id); + } catch (DockerException ex) { + LOGGER.error("Unexpected error when deleting session.", ex); } - }, - config.schedulerSessionKillScanRateSeconds(), - config.schedulerSessionKillScanRateSeconds(), - TimeUnit.SECONDS - ); + } + }, config.schedulerSessionKillScanRateSeconds(), + config.schedulerSessionKillScanRateSeconds(), TimeUnit.SECONDS); } void notifyDeath(String id) { JShellService shellService = jshellSessions.remove(id); - if (shellService == null) return; + if (shellService == null) + return; if (!shellService.isClosed()) { LOGGER.error("JShell Service isn't dead when it should for id {}.", id); } @@ -62,10 +61,8 @@ public boolean hasSession(String id) { return jshellSessions.containsKey(id); } - public JShellService session( - String id, - @Nullable StartupScriptId startupScriptId - ) throws DockerException { + public JShellService session(String id, @Nullable StartupScriptId startupScriptId) + throws DockerException { if (!hasSession(id)) { return createSession(new SessionInfo(id, true, startupScriptId, false, config)); } @@ -73,29 +70,14 @@ public JShellService session( } public JShellService session(@Nullable StartupScriptId startupScriptId) throws DockerException { - return createSession( - new SessionInfo( - UUID.randomUUID().toString(), - false, - startupScriptId, - false, - config - ) - ); + return createSession(new SessionInfo(UUID.randomUUID().toString(), false, startupScriptId, + false, config)); } - public JShellService oneTimeSession( - @Nullable StartupScriptId startupScriptId - ) throws DockerException { - return createSession( - new SessionInfo( - UUID.randomUUID().toString(), - false, - startupScriptId, - true, - config - ) - ); + public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId) + throws DockerException { + return createSession(new SessionInfo(UUID.randomUUID().toString(), false, startupScriptId, + true, config)); } public void deleteSession(String id) throws DockerException { @@ -104,59 +86,43 @@ public void deleteSession(String id) throws DockerException { scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS); } - private synchronized JShellService createSession( - SessionInfo sessionInfo - ) throws DockerException { + private synchronized JShellService createSession(SessionInfo sessionInfo) + throws DockerException { // Just in case race condition happens just before createSession if (hasSession(sessionInfo.id())) { return jshellSessions.get(sessionInfo.id()); } if (jshellSessions.size() >= config.maxAliveSessions()) { - throw new ResponseStatusException( - HttpStatus.TOO_MANY_REQUESTS, - "Too many sessions, try again later :(." - ); + throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, + "Too many sessions, try again later :(."); } LOGGER.info("Creating session : {}.", sessionInfo); - JShellService service = - new JShellService( - dockerService, - this, - sessionInfo.id(), - sessionInfo.sessionTimeout(), - sessionInfo.renewable(), - sessionInfo.evalTimeout(), - sessionInfo.evalTimeoutValidationLeeway(), - sessionInfo.sysOutCharLimit(), - config.dockerMaxRamMegaBytes(), - config.dockerCPUsUsage(), - config.dockerCPUSetCPUs(), - startupScriptsService.get(sessionInfo.startupScriptId()) - ); + JShellService service = new JShellService(dockerService, this, sessionInfo.id(), + sessionInfo.sessionTimeout(), sessionInfo.renewable(), sessionInfo.evalTimeout(), + sessionInfo.evalTimeoutValidationLeeway(), sessionInfo.sysOutCharLimit(), + config.dockerMaxRamMegaBytes(), config.dockerCPUsUsage(), config.dockerCPUSetCPUs(), + startupScriptsService.get(sessionInfo.startupScriptId())); jshellSessions.put(sessionInfo.id(), service); return service; } /** - * Schedule the validation of the session timeout. In case the code runs for too - * long, checks if the wrapper correctly followed the eval timeout and canceled - * it, if it didn't, forcefully close the session. + * Schedule the validation of the session timeout. In case the code runs for too long, checks if + * the wrapper correctly followed the eval timeout and canceled it, if it didn't, forcefully + * close the session. * - * @param id the id of the session + * @param id the id of the session * @param timeSeconds the time to schedule */ public void scheduleEvalTimeoutValidation(String id, long timeSeconds) { - scheduler.schedule( - () -> { - JShellService service = jshellSessions.get(id); - if (service == null) return; - if (service.isInvalidEvalTimeout()) { - service.close(); - } - }, - timeSeconds, - TimeUnit.SECONDS - ); + scheduler.schedule(() -> { + JShellService service = jshellSessions.get(id); + if (service == null) + return; + if (service.isInvalidEvalTimeout()) { + service.close(); + } + }, timeSeconds, TimeUnit.SECONDS); } @Autowired diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java index cbee1da..fb5e7f6 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/SessionInfo.java @@ -1,34 +1,16 @@ package org.togetherjava.jshellapi.service; import org.springframework.lang.Nullable; + import org.togetherjava.jshellapi.Config; -public record SessionInfo( - String id, - long sessionTimeout, - boolean renewable, - long evalTimeout, - long evalTimeoutValidationLeeway, - int sysOutCharLimit, - @Nullable StartupScriptId startupScriptId -) { - public SessionInfo( - String id, - boolean renewable, - StartupScriptId startupScriptId, - boolean isOneTime, - Config config - ) { - this( - id, - isOneTime - ? config.oneTimeSessionTimeoutSeconds() - : config.regularSessionTimeoutSeconds(), - renewable, - config.evalTimeoutSeconds(), - config.evalTimeoutValidationLeeway(), - config.sysOutCharLimit(), - startupScriptId - ); +public record SessionInfo(String id, long sessionTimeout, boolean renewable, long evalTimeout, + long evalTimeoutValidationLeeway, int sysOutCharLimit, + @Nullable StartupScriptId startupScriptId) { + public SessionInfo(String id, boolean renewable, StartupScriptId startupScriptId, + boolean isOneTime, Config config) { + this(id, isOneTime ? config.oneTimeSessionTimeoutSeconds() + : config.regularSessionTimeoutSeconds(), renewable, config.evalTimeoutSeconds(), + config.evalTimeoutValidationLeeway(), config.sysOutCharLimit(), startupScriptId); } } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java index 2420cc3..a6cd5ce 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/StartupScriptsService.java @@ -1,5 +1,8 @@ package org.togetherjava.jshellapi.service; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -8,9 +11,6 @@ import java.util.Map; import java.util.Objects; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Service; - @Service public class StartupScriptsService { @@ -19,15 +19,11 @@ public class StartupScriptsService { private StartupScriptsService() { scripts = new EnumMap<>(StartupScriptId.class); for (StartupScriptId id : StartupScriptId.values()) { - try ( - InputStream scriptStream = + try (InputStream scriptStream = Objects.requireNonNull( - StartupScriptsService.class.getResourceAsStream( - "/jshell_startup/" + id + ".jsh" - ), - "Couldn't load script " + id - ) - ) { + StartupScriptsService.class + .getResourceAsStream("/jshell_startup/" + id + ".jsh"), + "Couldn't load script " + id)) { String script = new String(scriptStream.readAllBytes(), StandardCharsets.UTF_8); script = cleanEndLines(script); scripts.put(id, script); diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java index 6642470..0c204e5 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/Utils.java @@ -7,11 +7,8 @@ import java.util.stream.Stream; public class Utils { - public static , X extends Exception> E nameOrElseThrow( - Class c, - String name, - Function exceptionFunction - ) throws X { + public static , X extends Exception> E nameOrElseThrow(Class c, + String name, Function exceptionFunction) throws X { return name(c, name).orElseThrow(() -> exceptionFunction.apply(name)); } @@ -19,11 +16,8 @@ public static > Optional name(Class c, String name) { return predicate(c, e -> e.name().equals(name)).findAny(); } - public static , K> Optional key( - Class c, - Function keyMapper, - K name - ) { + public static , K> Optional key(Class c, Function keyMapper, + K name) { return predicate(c, e -> keyMapper.apply(e).equals(name)).findAny(); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java index 2906d09..194ffe3 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/Config.java @@ -12,8 +12,7 @@ public static Config load() { public Config { if (evalTimeoutSeconds <= 0) throw new IllegalArgumentException( - "Invalid evalTimeoutSeconds : " + evalTimeoutSeconds - ); + "Invalid evalTimeoutSeconds : " + evalTimeoutSeconds); if (sysOutCharLimit <= 0) throw new IllegalArgumentException("Invalid sysOutCharLimit : " + sysOutCharLimit); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java index 6417c3d..264c53f 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/EvalResult.java @@ -1,7 +1,8 @@ package org.togetherjava.jshell.wrapper; -import java.util.List; - import jdk.jshell.SnippetEvent; -public record EvalResult(List events, JShellEvalAbortion abortion) {} +import java.util.List; + +public record EvalResult(List events, JShellEvalAbortion abortion) { +} diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java index 03c43a4..6bc5267 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortion.java @@ -1,7 +1,5 @@ package org.togetherjava.jshell.wrapper; -public record JShellEvalAbortion( - String sourceCause, - String remainingSource, - JShellEvalAbortionCause cause -) {} +public record JShellEvalAbortion(String sourceCause, String remainingSource, + JShellEvalAbortionCause cause) { +} diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java index 674a500..0cb3e54 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalAbortionCause.java @@ -4,14 +4,16 @@ public sealed interface JShellEvalAbortionCause { - record TimeoutAbortionCause() implements JShellEvalAbortionCause {} + record TimeoutAbortionCause() implements JShellEvalAbortionCause { + } - record UnhandledExceptionAbortionCause( - String exceptionClass, - String exceptionMessage - ) implements JShellEvalAbortionCause {} + record UnhandledExceptionAbortionCause(String exceptionClass, + String exceptionMessage) implements JShellEvalAbortionCause { + } - record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause {} + record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause { + } - record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause {} + record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause { + } } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java index e07a874..9c5e7e3 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellEvalStop.java @@ -1,9 +1,9 @@ package org.togetherjava.jshell.wrapper; -import java.util.concurrent.atomic.AtomicBoolean; - import jdk.jshell.JShell; +import java.util.concurrent.atomic.AtomicBoolean; + public record JShellEvalStop(JShell shell, AtomicBoolean hasStopped) implements Runnable { @Override public void run() { diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java index 33bd49a..535a052 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/JShellWrapper.java @@ -1,5 +1,7 @@ package org.togetherjava.jshell.wrapper; +import jdk.jshell.*; + import java.io.InputStream; import java.io.PrintStream; import java.util.*; @@ -7,13 +9,11 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import jdk.jshell.*; - /** - * Enter eval to evaluate code, snippets to see snippets, exit to stop. How to - * use : at startup, one line need to be sent, startup script, first enter the - * command, for example eval or snippets, then any needed argument. Then "OK" - * should immediately be sent back, then after some time, the rest of the data. + * Enter eval to evaluate code, snippets to see snippets, exit to stop. How to use : at startup, one + * line need to be sent, startup script, first enter the command, for example eval or snippets, then + * any needed argument. Then "OK" should immediately be sent back, then after some time, the rest of + * the data. */ public class JShellWrapper { @@ -45,31 +45,25 @@ public void run(Config config, InputStream in, PrintStream processOut) { private void verifyStartupEval(EvalResult result) { JShellEvalAbortion abortion = result.abortion(); - if (abortion == null) return; + if (abortion == null) + return; SnippetEvent event = result.events().get(result.events().size() - 1); // TODO Replace with switch if (abortion.cause() instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { throw new RuntimeException("Timeout exceeded."); - } else if ( - abortion.cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause - ) { + } else if (abortion + .cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause) { + throw new RuntimeException("Following startup script resulted in an exception : " + + sanitize(abortion.sourceCause()), event.exception()); + } else if (abortion + .cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause) { throw new RuntimeException( - "Following startup script resulted in an exception : " - + sanitize(abortion.sourceCause()), - event.exception() - ); + "Following startup script was REJECTED : " + sanitize(abortion.sourceCause())); + } else if (abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { + throw new RuntimeException("Following startup script has a syntax error : " + + sanitize(abortion.sourceCause())); } else - if (abortion.cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause) { - throw new RuntimeException( - "Following startup script was REJECTED : " + sanitize(abortion.sourceCause()) - ); - } else - if (abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { - throw new RuntimeException( - "Following startup script has a syntax error : " - + sanitize(abortion.sourceCause()) - ); - } else throw new AssertionError(); + throw new AssertionError(); } private void ok(PrintStream processOut) { @@ -83,32 +77,20 @@ private EvalResult eval(JShell shell, String code, AtomicBoolean hasStopped) { while (!code.isEmpty()) { var completion = shell.sourceCodeAnalysis().analyzeCompletion(clean(code)); if (!completion.completeness().isComplete()) { - abortion = - new JShellEvalAbortion( - code, - "", - new JShellEvalAbortionCause.SyntaxErrorAbortionCause() - ); + abortion = new JShellEvalAbortion(code, "", + new JShellEvalAbortionCause.SyntaxErrorAbortionCause()); break; } List evalEvents = shell.eval(completion.source()); JShellEvalAbortionCause abortionCause = handleEvents(shell, evalEvents, resultEvents); if (abortionCause != null) { - abortion = - new JShellEvalAbortion( - completion.source(), - completion.remaining(), - abortionCause - ); + abortion = new JShellEvalAbortion(completion.source(), completion.remaining(), + abortionCause); break; } if (hasStopped.get()) { - abortion = - new JShellEvalAbortion( - completion.source(), - completion.remaining(), - new JShellEvalAbortionCause.TimeoutAbortionCause() - ); + abortion = new JShellEvalAbortion(completion.source(), completion.remaining(), + new JShellEvalAbortionCause.TimeoutAbortionCause()); break; } code = completion.remaining(); @@ -116,49 +98,39 @@ private EvalResult eval(JShell shell, String code, AtomicBoolean hasStopped) { return new EvalResult(resultEvents, abortion); } - private JShellEvalAbortionCause handleEvents( - JShell shell, - List evalEvents, - List resultEvents - ) { + private JShellEvalAbortionCause handleEvents(JShell shell, List evalEvents, + List resultEvents) { for (SnippetEvent event : evalEvents) { if (event.causeSnippet() == null) { // Only keep snippet creation events resultEvents.add(event); if (event.status() == Snippet.Status.REJECTED) return createCompileErrorCause(shell, event); - if (event.exception() != null) return createExceptionCause(event); + if (event.exception() != null) + return createExceptionCause(event); } } return null; } private JShellEvalAbortionCause.UnhandledExceptionAbortionCause createExceptionCause( - SnippetEvent event - ) { + SnippetEvent event) { if (event.exception() == null) { return null; } else if (event.exception() instanceof EvalException evalException) { return new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - evalException.getExceptionClassName(), - evalException.getMessage() - ); + evalException.getExceptionClassName(), evalException.getMessage()); } else { return new JShellEvalAbortionCause.UnhandledExceptionAbortionCause( - event.exception().getClass().getName(), - event.exception().getMessage() - ); + event.exception().getClass().getName(), event.exception().getMessage()); } } private JShellEvalAbortionCause.CompileTimeErrorAbortionCause createCompileErrorCause( - JShell shell, - SnippetEvent event - ) { + JShell shell, SnippetEvent event) { return new JShellEvalAbortionCause.CompileTimeErrorAbortionCause( - shell.diagnostics(event.snippet()) - .map(d -> sanitize(d.getMessage(Locale.ENGLISH))) - .toList() - ); + shell.diagnostics(event.snippet()) + .map(d -> sanitize(d.getMessage(Locale.ENGLISH))) + .toList()); } /** @@ -179,24 +151,15 @@ private JShellEvalAbortionCause.CompileTimeErrorAbortionCause createCompileError * stdout
* */ - private void eval( - Scanner processIn, - PrintStream processOut, - Config config, - JShell shell, - StringOutputStream jshellOut - ) { + private void eval(Scanner processIn, PrintStream processOut, Config config, JShell shell, + StringOutputStream jshellOut) { AtomicBoolean hasStopped = new AtomicBoolean(); - TimeoutWatcher watcher = - new TimeoutWatcher( - config.evalTimeoutSeconds(), - new JShellEvalStop(shell, hasStopped) - ); + TimeoutWatcher watcher = new TimeoutWatcher(config.evalTimeoutSeconds(), + new JShellEvalStop(shell, hasStopped)); int lineCount = Integer.parseInt(processIn.nextLine()); - String code = - IntStream.range(0, lineCount) - .mapToObj(i -> processIn.nextLine()) - .collect(Collectors.joining("\n")); + String code = IntStream.range(0, lineCount) + .mapToObj(i -> processIn.nextLine()) + .collect(Collectors.joining("\n")); ok(processOut); watcher.start(); @@ -221,23 +184,20 @@ private List writeEvalResult(EvalResult result, StringOutputStream jshel // TODO replace with switch if (abortion.cause() instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { outBuffer.add("TIMEOUT"); - } else if ( - abortion - .cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c - ) { + } else if (abortion + .cause() instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c) { outBuffer.add("UNCAUGHT_EXCEPTION"); outBuffer.add(getExceptionFromCause(c)); - } else if ( - abortion.cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c - ) { + } else if (abortion + .cause() instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c) { outBuffer.add("COMPILE_TIME_ERROR"); outBuffer.add(String.valueOf(c.errors().size())); outBuffer.addAll(c.errors()); - } else if ( - abortion.cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause c - ) { + } else if (abortion + .cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause c) { outBuffer.add("SYNTAX_ERROR"); - } else throw new AssertionError(); + } else + throw new AssertionError(); outBuffer.add(sanitize(abortion.sourceCause())); outBuffer.add(sanitize(abortion.remainingSource())); } else { @@ -261,12 +221,11 @@ private List writeEvalResult(EvalResult result, StringOutputStream jshel * */ private void writeEvalSnippetEvent(List outBuffer, SnippetEvent event) { - String status = - switch (event.status()) { - case VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED -> - event.status().name(); - default -> throw new RuntimeException("Invalid status"); - }; + String status = switch (event.status()) { + case VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED -> + event.status().name(); + default -> throw new RuntimeException("Invalid status"); + }; outBuffer.add(status); if (event.previousStatus() == Snippet.Status.NONEXISTENT) { outBuffer.add("ADDITION"); @@ -279,8 +238,7 @@ private void writeEvalSnippetEvent(List outBuffer, SnippetEvent event) { } private String getExceptionFromCause( - JShellEvalAbortionCause.UnhandledExceptionAbortionCause cause - ) { + JShellEvalAbortionCause.UnhandledExceptionAbortionCause cause) { return sanitize(cause.exceptionClass() + ":" + cause.exceptionMessage()); } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java index 42449dd..dc70ca8 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/StringOutputStream.java @@ -54,21 +54,19 @@ public void write(byte[] b, int off, int len) { } public Result readAll() { - if (index > bytes.length) throw new IllegalStateException(); // Should never happen - String s = - new String( - index == bytes.length ? bytes : Arrays.copyOf(bytes, index), - StandardCharsets.UTF_8 - ); + if (index > bytes.length) + throw new IllegalStateException(); // Should never happen + String s = new String(index == bytes.length ? bytes : Arrays.copyOf(bytes, index), + StandardCharsets.UTF_8); index = 0; if (byteOverflow) { byteOverflow = false; return new Result( - s.charAt(s.length() - 1) == UNKNOWN_CHAR ? s.substring(0, s.length() - 1) : s, - true - ); + s.charAt(s.length() - 1) == UNKNOWN_CHAR ? s.substring(0, s.length() - 1) : s, + true); } - if (s.length() > maxSize) return new Result(s.substring(0, maxSize), true); + if (s.length() > maxSize) + return new Result(s.substring(0, maxSize), true); return new Result(s, false); } @@ -77,5 +75,6 @@ public void close() { bytes = null; } - public record Result(String content, boolean isOverflow) {} + public record Result(String content, boolean isOverflow) { + } } diff --git a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java index 1e5ac65..f3a3d37 100644 --- a/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java +++ b/JShellWrapper/src/main/java/org/togetherjava/jshell/wrapper/TimeoutWatcher.java @@ -5,16 +5,15 @@ public class TimeoutWatcher { private boolean timeout; public TimeoutWatcher(int timeoutSeconds, Runnable timeoutAction) { - Runnable runnable = - () -> { - try { - Thread.sleep(timeoutSeconds * 1000L); - } catch (InterruptedException e) { // Stopped - return; - } - timeout = true; - timeoutAction.run(); - }; + Runnable runnable = () -> { + try { + Thread.sleep(timeoutSeconds * 1000L); + } catch (InterruptedException e) { // Stopped + return; + } + timeout = true; + timeoutAction.run(); + }; thread = new Thread(runnable); thread.setName("Timeout Watcher"); } diff --git a/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java b/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java index 27061e3..3657eaa 100644 --- a/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java +++ b/JShellWrapper/src/test/java/JShellWrapperStartupScriptTest.java @@ -1,29 +1,26 @@ -import static org.junit.jupiter.api.Assertions.*; - -import java.io.PrintStream; - import org.junit.jupiter.api.Test; + import org.togetherjava.jshell.wrapper.Config; import org.togetherjava.jshell.wrapper.JShellWrapper; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + class JShellWrapperStartupScriptTest { @Test void testDoubleSnippets() { Config config = new Config(5, 1024); - StringInputStream inputStream = - new StringInputStream( - """ - import java.util.*; void println(Object o) { System.out.println(o); } - eval - 1 - println(List.of("a", "b", "c")) - exit""" - ); + StringInputStream inputStream = new StringInputStream(""" + import java.util.*; void println(Object o) { System.out.println(o); } + eval + 1 + println(List.of("a", "b", "c")) + exit"""); UnboundStringOutputStream outputStream = new UnboundStringOutputStream(); JShellWrapper jshell = new JShellWrapper(); jshell.run(config, inputStream, new PrintStream(outputStream)); - assertEquals( - """ + assertEquals(""" OK 2 OK @@ -37,8 +34,6 @@ void testDoubleSnippets() { false [a, b, c]\\n OK - """, - outputStream.readAll() - ); + """, outputStream.readAll()); } } diff --git a/JShellWrapper/src/test/java/JShellWrapperTest.java b/JShellWrapper/src/test/java/JShellWrapperTest.java index 784aaf3..1d8c8f9 100644 --- a/JShellWrapper/src/test/java/JShellWrapperTest.java +++ b/JShellWrapper/src/test/java/JShellWrapperTest.java @@ -1,13 +1,14 @@ -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.InputStream; -import java.io.PrintStream; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; + import org.togetherjava.jshell.wrapper.Config; import org.togetherjava.jshell.wrapper.JShellWrapper; +import java.io.InputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + class JShellWrapperTest { static Config config; static JShellWrapper jshell; @@ -28,12 +29,10 @@ void evalTest(String input, String expectedOutput) { @Test void testHelloWorld() { - evalTest( - """ + evalTest(""" eval 1 - System.out.println("Hello world!")""", - """ + System.out.println("Hello world!")""", """ OK 0 OK @@ -45,21 +44,18 @@ void testHelloWorld() { false - Hello world!\\n""" - ); + Hello world!\\n"""); } @Test void testMultilinesInput() { - evalTest( - """ + evalTest(""" eval 4 for(int i = 0; i < 10; i++) { System.out.print(i); } - System.out.println();""", - """ + System.out.println();""", """ OK 0 OK @@ -76,18 +72,15 @@ void testMultilinesInput() { false - 0123456789\\n""" - ); + 0123456789\\n"""); } @Test void testStdoutOverflow() { - evalTest( - """ + evalTest(""" eval 1 - for(int i = 0; i < 1024; i++) System.out.print(0)""", - """ + for(int i = 0; i < 1024; i++) System.out.print(0)""", """ OK 0 OK @@ -99,15 +92,11 @@ void testStdoutOverflow() { false - %s""" - .formatted("0".repeat(1024)) - ); - evalTest( - """ + %s""".formatted("0".repeat(1024))); + evalTest(""" eval 1 - for(int i = 0; i <= 1024; i++) System.out.print(0)""", - """ + for(int i = 0; i <= 1024; i++) System.out.print(0)""", """ OK 0 OK @@ -119,20 +108,16 @@ void testStdoutOverflow() { true - %s""" - .formatted("0".repeat(1024)) - ); + %s""".formatted("0".repeat(1024))); } @Test void testModificationAndMultiplesSnippets() { - evalTest( - """ + evalTest(""" eval 2 int i = 0; - int i = 2;""", - """ + int i = 2;""", """ OK 0 OK @@ -149,18 +134,15 @@ void testModificationAndMultiplesSnippets() { 2 false - """ - ); + """); } @Test void testUseId() { - evalTest( - """ + evalTest(""" eval 1 - System.out.println("Hello world!")""", - """ + System.out.println("Hello world!")""", """ OK 0 OK @@ -172,18 +154,15 @@ void testUseId() { false - Hello world!\\n""" - ); + Hello world!\\n"""); } @Test void testTimeout() { - evalTest( - """ + evalTest(""" eval 1 - while(true);""", - """ + while(true);""", """ OK 0 OK @@ -197,18 +176,15 @@ void testTimeout() { while(true); false - """ - ); + """); } @Test void testUncaughtException() { // TODO other kind of exception, not in EvalException - evalTest( - """ + evalTest(""" eval 1 - throw new RuntimeException("Some message : fail")""", - """ + throw new RuntimeException("Some message : fail")""", """ OK 0 OK @@ -223,18 +199,15 @@ void testUncaughtException() { // TODO other kind of exception, not in EvalExcep throw new RuntimeException("Some message : fail"); false - """ - ); + """); } @Test void testRejected() { - evalTest( - """ + evalTest(""" eval 1 - print""", - """ + print""", """ OK 0 OK @@ -250,19 +223,16 @@ void testRejected() { print false - """ - ); + """); } @Test void testSyntaxError() { // DEFINITELY_INCOMPLETE - evalTest( - """ + evalTest(""" eval 1 - print(""", - """ + print(""", """ OK 0 OK @@ -271,15 +241,12 @@ void testSyntaxError() { print( false - """ - ); + """); // CONSIDERED_INCOMPLETE - evalTest( - """ + evalTest(""" eval 1 - while(true)""", - """ + while(true)""", """ OK 0 OK @@ -288,14 +255,11 @@ void testSyntaxError() { while(true) false - """ - ); - evalTest( - """ + """); + evalTest(""" eval 1 - for(int i = 0; i < 10; i++)""", - """ + for(int i = 0; i < 10; i++)""", """ OK 0 OK @@ -304,20 +268,17 @@ void testSyntaxError() { for(int i = 0; i < 10; i++) false - """ - ); + """); } @Test void testRejectedAndMultiples() { - evalTest( - """ + evalTest(""" eval 3 int i = 0; print; - System.out.println(i);""", - """ + System.out.println(i);""", """ OK 0 OK @@ -338,20 +299,17 @@ void testRejectedAndMultiples() { \\nprint; \\nSystem.out.println(i); false - """ - ); + """); } @Test void testMultilinesAndHardcodedNewLineInString() { - evalTest( - """ + evalTest(""" eval 3 { System.out.println("\\n"); - }""", - """ + }""", """ OK 0 OK @@ -363,7 +321,6 @@ void testMultilinesAndHardcodedNewLineInString() { false - \\n\\n""" - ); + \\n\\n"""); } } diff --git a/JShellWrapper/src/test/java/StringInputStream.java b/JShellWrapper/src/test/java/StringInputStream.java index 8bd679b..1293d9e 100644 --- a/JShellWrapper/src/test/java/StringInputStream.java +++ b/JShellWrapper/src/test/java/StringInputStream.java @@ -11,7 +11,8 @@ public StringInputStream(String content) { @Override public int read() { - if (i == content.length) return -1; + if (i == content.length) + return -1; return content[i++] & 0xFF; } } diff --git a/JShellWrapper/src/test/java/StringOutputStreamTest.java b/JShellWrapper/src/test/java/StringOutputStreamTest.java index eab7987..8129f11 100644 --- a/JShellWrapper/src/test/java/StringOutputStreamTest.java +++ b/JShellWrapper/src/test/java/StringOutputStreamTest.java @@ -1,12 +1,13 @@ -import static org.junit.jupiter.api.Assertions.*; - -import java.nio.charset.StandardCharsets; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.togetherjava.jshell.wrapper.StringOutputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + class StringOutputStreamTest { static final String E_ACUTE = "\u00E9"; static final String SMILEY = "\uD83D\uDE0A"; @@ -83,17 +84,13 @@ void testWriteOverload() { assertResult(true, E_ACUTE.repeat(10), stream.readAll()); final String eAcuteX5AndSmileyX5 = E_ACUTE.repeat(5) + SMILEY.repeat(5); - stream.write( - eAcuteX5AndSmileyX5.getBytes(StandardCharsets.UTF_8), 2 * 3, (2 * 2) + (4 * 4) - ); + stream.write(eAcuteX5AndSmileyX5.getBytes(StandardCharsets.UTF_8), 2 * 3, + (2 * 2) + (4 * 4)); assertResult(false, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll()); final String eAcuteX5AndSmileyX5AndA = E_ACUTE.repeat(5) + SMILEY.repeat(5) + 'a'; - stream.write( - eAcuteX5AndSmileyX5AndA.getBytes(StandardCharsets.UTF_8), - 2 * 3, - (2 * 2) + (4 * 4) + 1 - ); + stream.write(eAcuteX5AndSmileyX5AndA.getBytes(StandardCharsets.UTF_8), 2 * 3, + (2 * 2) + (4 * 4) + 1); assertResult(true, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll()); } diff --git a/build.gradle b/build.gradle index d5ec115..81ebaa4 100644 --- a/build.gradle +++ b/build.gradle @@ -16,12 +16,15 @@ subprojects { spotless { java { - target('**/java/**/*.java') - trimTrailingWhitespace() + targetExclude("build/**") + endWithNewline() removeUnusedImports() - // '\\#' = static imports, '' = everything else - importOrder('\\#', 'java', 'javax', 'com.javadiscord', '') - eclipse().configFile("${rootProject.rootDir}/spotless.xml") + // empty string '' covers all imports that aren't explicitly specified, + // we use it as catch-all for external dependencies like JDA + // '\\#` is prefix for static imports + importOrder('','org.togetherjava', 'javax', 'java', '\\#') + // TODO: pinning version because of spotless error https://github.com/diffplug/spotless/issues/1992 + eclipse("4.31").configFile("${rootProject.rootDir}/spotless.xml") } } diff --git a/spotless.xml b/spotless.xml index 8af22c7..331dc6b 100644 --- a/spotless.xml +++ b/spotless.xml @@ -1,404 +1,336 @@ - - - - - - + + + + + + + + + + + + + + - + + + + + + + + + + + + - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - + + + + + - - - - - - - - - + + - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + +