Skip to content

Commit

Permalink
Merge pull request #8 from Together-Java/feature/multi-snippets
Browse files Browse the repository at this point in the history
Feature/multi snippets
  • Loading branch information
Alathreon authored Dec 25, 2023
2 parents 9a40356 + 2260643 commit 7bb9d99
Show file tree
Hide file tree
Showing 38 changed files with 856 additions and 507 deletions.
1 change: 1 addition & 0 deletions JShellAPI/RunDocker.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker run --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock --name jshellapi togetherjava.org:5001/togetherjava/jshellbackend:master
1 change: 1 addition & 0 deletions JShellAPI/RunDocker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker run --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock --name jshellapi togetherjava.org:5001/togetherjava/jshellbackend:master
Binary file removed JShellAPI/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 0 additions & 5 deletions JShellAPI/gradle/wrapper/gradle-wrapper.properties

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record Config(
long regularSessionTimeoutSeconds,
long oneTimeSessionTimeoutSeconds,
long evalTimeoutSeconds,
int sysOutCharLimit,
long maxAliveSessions,
int dockerMaxRamMegaBytes,
double dockerCPUsUsage,
Expand All @@ -15,6 +16,7 @@ public record Config(
if(regularSessionTimeoutSeconds <= 0) throw new RuntimeException("Invalid value " + regularSessionTimeoutSeconds);
if(oneTimeSessionTimeoutSeconds <= 0) throw new RuntimeException("Invalid value " + oneTimeSessionTimeoutSeconds);
if(evalTimeoutSeconds <= 0) throw new RuntimeException("Invalid value " + evalTimeoutSeconds);
if(sysOutCharLimit <= 0) throw new RuntimeException("Invalid value " + sysOutCharLimit);
if(maxAliveSessions <= 0) throw new RuntimeException("Invalid value " + maxAliveSessions);
if(dockerMaxRamMegaBytes <= 0) throw new RuntimeException("Invalid value " + dockerMaxRamMegaBytes);
if(dockerCPUsUsage <= 0) throw new RuntimeException("Invalid value " + dockerCPUsUsage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.togetherjava.jshellapi.dto;

public record JShellEvalAbortion(String sourceCause, String remainingSource, JShellEvalAbortionCause cause) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.togetherjava.jshellapi.dto;

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 {
}

@JsonTypeName("UNCAUGHT_EXCEPTION")
record UnhandledExceptionAbortionCause(String exceptionClass, String exceptionMessage) implements JShellEvalAbortionCause {
}

@JsonTypeName("COMPILE_TIME_ERROR")
record CompileTimeErrorAbortionCause(List<String> errors) implements JShellEvalAbortionCause {
}

@JsonTypeName("SYNTAX_ERROR")
record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause {
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@
import java.util.List;

public record JShellResult(
SnippetStatus status,
SnippetType type,
int id,
String source,
@Nullable
String result,
@Nullable
JShellExceptionResult exception,
List<JShellSnippetResult> snippetsResults,
@Nullable JShellEvalAbortion abortion,
boolean stdoutOverflow,
String stdout,
List<String> errors) {
String stdout) {
public JShellResult {
errors = List.copyOf(errors);
snippetsResults = List.copyOf(snippetsResults);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.togetherjava.jshellapi.dto;

import org.springframework.lang.Nullable;

public record JShellSnippetResult(
SnippetStatus status,
SnippetType type,
int id,
String source,
@Nullable
String result) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package org.togetherjava.jshellapi.dto;

public enum SnippetStatus {
VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED, ABORTED
VALID, RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED, REJECTED
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ public void delete(@PathVariable String id) throws DockerException {

@GetMapping("/startup_script/{id}")
public String startupScript(@PathVariable StartupScriptId id) {
String imports = startupScriptsService.getImports(id);
String sep = imports.endsWith("\n") ? "\n" : "\n\n";
return imports + sep + startupScriptsService.getScript(id);
return startupScriptsService.get(id);
}

@Autowired
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package org.togetherjava.jshellapi.service;

import org.apache.tomcat.util.http.fileupload.util.Closeable;
import org.togetherjava.jshellapi.dto.JShellExceptionResult;
import org.togetherjava.jshellapi.dto.JShellResult;
import org.togetherjava.jshellapi.dto.SnippetStatus;
import org.togetherjava.jshellapi.dto.SnippetType;
import org.togetherjava.jshellapi.dto.*;
import org.togetherjava.jshellapi.exceptions.DockerException;

import java.io.BufferedReader;
Expand All @@ -30,7 +27,7 @@ public class JShellService implements Closeable {
private final boolean renewable;
private boolean doingOperation;

public JShellService(JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int maxMemory, double cpus, String startupImports, String startupScript) throws DockerException {
public JShellService(JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int sysOutCharLimit, int maxMemory, double cpus, String startupScript) throws DockerException {
this.sessionService = sessionService;
this.id = id;
this.timeout = timeout;
Expand All @@ -56,14 +53,13 @@ public JShellService(JShellSessionService sessionService, String id, long timeou
"--cpus=" + cpus,
"--name", containerName(),
"-e", "\"evalTimeoutSeconds=%d\"".formatted(evalTimeout),
"-e", "\"sysOutCharLimit=%d\"".formatted(sysOutCharLimit),
"togetherjava.org:5001/togetherjava/jshellwrapper:master")
.directory(new File(".."))
.redirectError(errorLogs.toFile())
.start();
writer = process.outputWriter();
reader = process.inputReader();
writer.write(sanitize(startupImports));
writer.newLine();
writer.write(sanitize(startupScript));
writer.newLine();
} catch (IOException e) {
Expand All @@ -89,33 +85,53 @@ public Optional<JShellResult> eval(String code) throws DockerException {

checkContainerOK();

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 id = Integer.parseInt(reader.readLine());
String source = desanitize(reader.readLine());
String result = reader.readLine();
if(result.equals("NONE")) result = null;
String rawException = reader.readLine();
JShellExceptionResult exception = null;
if(!rawException.isEmpty()) {
String[] split = rawException.split(":");
exception = new JShellExceptionResult(split[0], split[1]);
}
boolean stdoutOverflow = Boolean.parseBoolean(reader.readLine());
String stdout = desanitize(reader.readLine());
List<String> errors = new ArrayList<>();
String error;
while(!(error = reader.readLine()).isEmpty()) {
errors.add(desanitize(error));
}
return Optional.of(new JShellResult(status, type, id, source, result, exception, stdoutOverflow, stdout, errors));
return Optional.of(readResult());
} catch (IOException | NumberFormatException ex) {
close();
throw new DockerException(ex);
} finally {
stopOperation();
}
}
private JShellResult readResult() throws IOException, NumberFormatException, DockerException {
final int snippetsCount = Integer.parseInt(reader.readLine());
List<JShellSnippetResult> 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"));
int snippetId = Integer.parseInt(reader.readLine());
String source = cleanCode(reader.readLine());
String result = reader.readLine().transform(r -> r.equals("NONE") ? null : r);
snippetResults.add(new JShellSnippetResult(status, type, snippetId, source, result));
}
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<String> errors = new ArrayList<>();
for(int i = 0; i < errorCount; i++) {
errors.add(desanitize(reader.readLine()));
}
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);
}
boolean stdoutOverflow = Boolean.parseBoolean(reader.readLine());
String stdout = desanitize(reader.readLine());
return new JShellResult(snippetResults, abortion, stdoutOverflow, stdout);
}

public Optional<List<String>> snippets() throws DockerException {
synchronized (this) {
Expand All @@ -134,7 +150,7 @@ public Optional<List<String>> snippets() throws DockerException {
List<String> snippets = new ArrayList<>();
String snippet;
while(!(snippet = reader.readLine()).isEmpty()) {
snippets.add(desanitize(snippet));
snippets.add(cleanCode(snippet));
}
return Optional.of(snippets);
} catch (IOException ex) {
Expand Down Expand Up @@ -236,5 +252,8 @@ private static String sanitize(String s) {
private static String desanitize(String text) {
return text.replace("\\n", "\n").replace("\\\\", "\\");
}
private static String cleanCode(String code) {
return code.translateEscapes();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ public boolean hasSession(String id) {

public JShellService session(String id, @Nullable StartupScriptId startupScriptId) throws DockerException {
if(!hasSession(id)) {
return createSession(id, config.regularSessionTimeoutSeconds(), true, config.evalTimeoutSeconds(), startupScriptId);
return createSession(id, config.regularSessionTimeoutSeconds(), true, config.evalTimeoutSeconds(), config.sysOutCharLimit(), startupScriptId);
}
return jshellSessions.get(id);
}
public JShellService session(@Nullable StartupScriptId startupScriptId) throws DockerException {
return createSession(UUID.randomUUID().toString(), config.regularSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), startupScriptId);
return createSession(UUID.randomUUID().toString(), config.regularSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), config.sysOutCharLimit(), startupScriptId);
}
public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId) throws DockerException {
return createSession(UUID.randomUUID().toString(), config.oneTimeSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), startupScriptId);
return createSession(UUID.randomUUID().toString(), config.oneTimeSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), config.sysOutCharLimit(), startupScriptId);
}

public void deleteSession(String id) throws DockerException {
Expand All @@ -66,7 +66,7 @@ public void deleteSession(String id) throws DockerException {
scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS);
}

private synchronized JShellService createSession(String id, long sessionTimeout, boolean renewable, long evalTimeout, @Nullable StartupScriptId startupScriptId) throws DockerException {
private synchronized JShellService createSession(String id, long sessionTimeout, boolean renewable, long evalTimeout, int sysOutCharLimit, @Nullable StartupScriptId startupScriptId) throws DockerException {
if(hasSession(id)) { //Just in case race condition happens just before createSession
return jshellSessions.get(id);
}
Expand All @@ -79,10 +79,10 @@ private synchronized JShellService createSession(String id, long sessionTimeout,
sessionTimeout,
renewable,
evalTimeout,
sysOutCharLimit,
config.dockerMaxRamMegaBytes(),
config.dockerCPUsUsage(),
startupScriptsService.getImports(startupScriptId),
startupScriptsService.getScript(startupScriptId));
startupScriptsService.get(startupScriptId));
jshellSessions.put(id, service);
return service;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,37 @@
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

@Service
public class StartupScriptsService {
private record StartupScript(String imports, String script) {}

private final Map<StartupScriptId, StartupScript> scripts;
private final Map<StartupScriptId, String> scripts;

private StartupScriptsService() {
scripts = new EnumMap<>(StartupScriptId.class);
for (StartupScriptId id : StartupScriptId.values()) {
try (
InputStream importsStream = Objects.requireNonNull(StartupScriptsService.class.getResourceAsStream("/jshell_startup/imports/" + id + ".jsh"), "Couldn't load script " + id);
InputStream scriptStream = Objects.requireNonNull(StartupScriptsService.class.getResourceAsStream("/jshell_startup/scripts/" + id + ".jsh"), "Couldn't load script " + id)) {
String imports = new String(importsStream.readAllBytes(), StandardCharsets.UTF_8);
InputStream scriptStream = Objects.requireNonNull(StartupScriptsService.class.getResourceAsStream("/jshell_startup/" + id + ".jsh"), "Couldn't load script " + id)) {
String script = new String(scriptStream.readAllBytes(), StandardCharsets.UTF_8);
scripts.put(id, new StartupScript(imports, script));
script = cleanEndLines(script);
scripts.put(id, script);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
private String get(@Nullable StartupScriptId id, Function<StartupScript, String> function) {
StartupScript startupScript = scripts.get(id);
return startupScript != null ? function.apply(startupScript) : function.apply(scripts.get(StartupScriptId.EMPTY));
}

/**
* Returns corresponding imports, or default imports if id is null
* @param id the id or the imports, can be null
* @return corresponding imports, or default imports if id is null
*/
public String getImports(@Nullable StartupScriptId id) {
return get(id, StartupScript::imports);
private String cleanEndLines(String s) {
return s.replace("\r", "");
}

/**
* Returns corresponding script, or default script if id is null
* @param id the id or the script, can be null
* @return corresponding script, or default script if id is null
*/
public String getScript(@Nullable StartupScriptId id) {
return get(id, StartupScript::script);
public String get(@Nullable StartupScriptId id) {
String startupScript = scripts.get(id);
return startupScript != null ? startupScript : scripts.get(StartupScriptId.EMPTY);
}
}
1 change: 1 addition & 0 deletions JShellAPI/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
jshellapi.regularSessionTimeoutSeconds=1800
jshellapi.oneTimeSessionTimeoutSeconds=30
jshellapi.evalTimeoutSeconds=15
jshellapi.sysOutCharLimit=1024
jshellapi.maxAliveSessions=10

# Docker limits config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ import java.util.prefs.*;
import java.util.regex.*;
import java.util.stream.*;

void print(Object o) { System.out.print(o); }
void println(Object o) { System.out.println(o); }
void printf(String s, Object... args) { System.out.printf(s, args); }

Iterable<Integer> range(int startInclusive, int endExclusive) {
return IntStream.range(startInclusive, endExclusive)::iterator;
}

This file was deleted.

Empty file.
Loading

0 comments on commit 7bb9d99

Please sign in to comment.