diff --git a/docker-compose.build.yml b/docker-compose.build.yml index 765a511..bd9e3cc 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -7,7 +7,7 @@ services: dockerfile: server/deploy/Dockerfile context: . runtime: - image: ehenoma/jsheets-latest:latest + image: ehenoma/jsheets-runtime:latest build: dockerfile: runtime/deploy/Dockerfile context: . \ No newline at end of file diff --git a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionControl.java b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionControl.java index aab6520..84ed0ec 100644 --- a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionControl.java +++ b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionControl.java @@ -24,6 +24,7 @@ public final class ForkedExecutionControl extends JdiExecutionControl { private final Lock stopLock = new ReentrantLock(); private boolean userCodeRunning = false; + private volatile boolean closed; private final String remoteAgentClass; private final ClassFileStore classFileStore; @@ -72,6 +73,9 @@ public String invoke(String classname, String methodName) throws RunException, EngineTerminationException, InternalException { String result; + if (isClosed()) { + throw new IllegalStateException("closed"); + } updateUserCodeRunning(true); try { result = super.invoke(classname, methodName); @@ -96,6 +100,7 @@ public void stop() throws EngineTerminationException, InternalException { try { if (userCodeRunning) { new RemoteInterrupt(vm(), remoteAgentClass).runInSuspendedMode(); + closed = true; } } finally { stopLock.unlock(); @@ -107,6 +112,12 @@ public void stop() throws EngineTerminationException, InternalException { public void close() { super.close(); disposeMachine(); + stopLock.lock(); + closed = true; + } + + public boolean isClosed() { + return closed; } synchronized void disposeMachine() { diff --git a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionEnvironment.java b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionEnvironment.java index 4511e5a..2b73b00 100644 --- a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionEnvironment.java +++ b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkedExecutionEnvironment.java @@ -2,6 +2,10 @@ import java.util.Collection; import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; import jdk.jshell.spi.ExecutionControlProvider; import jsheets.evaluation.shell.environment.ClassFileStore; @@ -14,23 +18,39 @@ public static ForkedExecutionEnvironment create( ) { Objects.requireNonNull(store, "store"); Objects.requireNonNull(virtualMachineOptions, "virtualMachineOptions"); - return new ForkedExecutionEnvironment(store, virtualMachineOptions); + return new ForkedExecutionEnvironment( + store, + virtualMachineOptions, + createDaemonScheduler() + ); + } + + private static ScheduledExecutorService createDaemonScheduler() { + var factory = new ThreadFactoryBuilder().setDaemon(true).build(); + return Executors.newScheduledThreadPool(1, factory); } private final ClassFileStore store; private final Collection virtualMachineOptions; + private final ScheduledExecutorService scheduler; private ForkedExecutionEnvironment( ClassFileStore store, - Collection virtualMachineOptions + Collection virtualMachineOptions, + ScheduledExecutorService scheduler ) { this.store = store; this.virtualMachineOptions = virtualMachineOptions; + this.scheduler = scheduler; } @Override public ExecutionControlProvider control(String name) { - return ForkingExecutionControlProvider.create(virtualMachineOptions, store); + return ForkingExecutionControlProvider.create( + virtualMachineOptions, + store, + scheduler + ); } @Override diff --git a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkingExecutionControlProvider.java b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkingExecutionControlProvider.java index dd4adeb..b2ca932 100644 --- a/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkingExecutionControlProvider.java +++ b/evaluation/src/main/java/jsheets/evaluation/shell/environment/fork/ForkingExecutionControlProvider.java @@ -8,16 +8,21 @@ import java.net.InetAddress; import java.net.ServerSocket; import java.time.Duration; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Consumer; +import com.google.common.flogger.FluentLogger; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + import com.sun.jdi.VirtualMachine; import jdk.jshell.execution.JdiInitiator; import jdk.jshell.execution.RemoteExecutionControl; @@ -33,42 +38,57 @@ public final class ForkingExecutionControlProvider implements ExecutionControlProvider { - private static final Duration defaultTimeout = Duration.ofMillis(3000); + private static final FluentLogger log = FluentLogger.forEnclosingClass(); public static ForkingExecutionControlProvider create() { - return create(List.of(), EmptyClassFileStore.create()); + return create( + List.of(), + EmptyClassFileStore.create(), + Executors.newScheduledThreadPool( + 1, + new ThreadFactoryBuilder().setDaemon(true).build() + ) + ); } + private static final Duration defaultTimeout = Duration.ofMillis(3000); + private static final Duration defaultExecutionTimeout = + Duration.ofSeconds(30); + public static ForkingExecutionControlProvider create( Collection rawVirtualMachineOptions, - ClassFileStore classFileStore + ClassFileStore classFileStore, + ScheduledExecutorService scheduler ) { Objects.requireNonNull(classFileStore, "classFileStore"); Objects.requireNonNull(rawVirtualMachineOptions, "rawVirtualMachineOptions"); - var host = InetAddress.getLoopbackAddress().getHostName(); return new ForkingExecutionControlProvider( - host, + defaultExecutionTimeout, defaultTimeout, List.copyOf(rawVirtualMachineOptions), - classFileStore + classFileStore, + scheduler ); } - private final String host; - private final Duration timeout; + private final Duration connectTimeout; + private final Duration executionTimeout; private final List rawVirtualMachineOptions; + private final ScheduledExecutorService scheduler; private final ClassFileStore classFileStore; private ForkingExecutionControlProvider( - String host, - Duration timeout, + Duration executionTimeout, + Duration connectTimeout, List rawVirtualMachineOptions, - ClassFileStore classFileStore + ClassFileStore classFileStore, + ScheduledExecutorService scheduler ) { - this.host = host; - this.timeout = timeout; + this.executionTimeout = executionTimeout; + this.connectTimeout = connectTimeout; this.rawVirtualMachineOptions = rawVirtualMachineOptions; this.classFileStore = classFileStore; + this.scheduler = scheduler; } @Override @@ -101,7 +121,7 @@ private Box initiate(int port) { /* remoteAgentClassName */ remoteAgentClassName, /* controlledLaunch */ false, /* host */ "", - /* timeout */ (int) timeout.toMillis(), + /* timeout */ (int) connectTimeout.toMillis(), /* connectorOptions*/ Collections.emptyMap() ); return new Box( @@ -115,7 +135,7 @@ private Box initiate(int port) { ExecutionControl create(ExecutionEnv environment) throws IOException { var address = InetAddress.getLoopbackAddress(); try (var listener = new ServerSocket(0, backlog, address)) { - listener.setSoTimeout((int) timeout.toMillis()); + listener.setSoTimeout((int) connectTimeout.toMillis()); var box = initiate(listener.getLocalPort()); return accept(listener, environment, box); } @@ -164,10 +184,28 @@ private BiFunction createControl( ); hooks.add(event -> environment.closeDown()); hooks.add(event -> control.disposeMachine()); + scheduleExecutionTimeout(control); return control; }; } + private void scheduleExecutionTimeout(ForkedExecutionControl control) { + scheduler.schedule(() -> { + if (!control.isClosed()) { + log.atInfo().log("stopping long running remote"); + try { + control.stop(); + } catch (Exception failedStop) { + log.atWarning() + .withCause(failedStop) + .atMostEvery(5, TimeUnit.SECONDS) + .log("failed to stop long running remote"); + } + control.close(); + } + }, executionTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + private Map createOutputs(ExecutionEnv environment) { return Map.of( "out", environment.userOut(), diff --git a/evaluation/src/main/java/jsheets/evaluation/shell/environment/sandbox/SandboxClassFileCheck.java b/evaluation/src/main/java/jsheets/evaluation/shell/environment/sandbox/SandboxClassFileCheck.java index b2c0785..73c3a8d 100644 --- a/evaluation/src/main/java/jsheets/evaluation/shell/environment/sandbox/SandboxClassFileCheck.java +++ b/evaluation/src/main/java/jsheets/evaluation/shell/environment/sandbox/SandboxClassFileCheck.java @@ -23,6 +23,15 @@ private SandboxClassFileCheck(Collection rules) { @Override public void redefine(ExecutionControl.ClassBytecodes[] bytecodes) { + analyze(bytecodes); + } + + @Override + public void load(ExecutionControl.ClassBytecodes[] bytecodes) { + analyze(bytecodes); + } + + private void analyze(ExecutionControl.ClassBytecodes[] bytecodes) { var analysis = Analysis.create(); var check = SandboxBytecodeCheck.withRules(rules); for (var binary : bytecodes) { @@ -30,9 +39,4 @@ public void redefine(ExecutionControl.ClassBytecodes[] bytecodes) { } analysis.reportViolations(); } - - @Override - public void load(ExecutionControl.ClassBytecodes[] bytecodes) { - - } } \ No newline at end of file diff --git a/runtime/src/main/java/jsheets/runtime/App.java b/runtime/src/main/java/jsheets/runtime/App.java index 04c3e5e..07f3c87 100644 --- a/runtime/src/main/java/jsheets/runtime/App.java +++ b/runtime/src/main/java/jsheets/runtime/App.java @@ -1,12 +1,20 @@ package jsheets.runtime; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; import com.google.common.flogger.FluentLogger; import com.google.inject.Guice; import com.google.inject.Injector; +import jdk.jshell.JShell; +import jsheets.evaluation.sandbox.access.AccessGraph; +import jsheets.evaluation.sandbox.validation.ForbiddenMemberFilter; +import jsheets.evaluation.shell.environment.fork.ForkingExecutionControlProvider; +import jsheets.evaluation.shell.environment.sandbox.SandboxClassFileCheck; import jsheets.runtime.evaluation.EvaluationModule; public final class App { diff --git a/runtime/src/main/java/jsheets/runtime/evaluation/EvaluationModule.java b/runtime/src/main/java/jsheets/runtime/evaluation/EvaluationModule.java index 7dfdf5e..cff23f4 100644 --- a/runtime/src/main/java/jsheets/runtime/evaluation/EvaluationModule.java +++ b/runtime/src/main/java/jsheets/runtime/evaluation/EvaluationModule.java @@ -63,5 +63,4 @@ Collection listVirtualMachineOptions(Config config) { virtualMachineOptionsKey().in(config).or("").trim().split("\n") ); } - } \ No newline at end of file diff --git a/runtime/src/main/resources/runtime/evaluation/sandbox/accessGraph.txt b/runtime/src/main/resources/runtime/evaluation/sandbox/accessGraph.txt index 6afde38..4a34b50 100644 --- a/runtime/src/main/resources/runtime/evaluation/sandbox/accessGraph.txt +++ b/runtime/src/main/resources/runtime/evaluation/sandbox/accessGraph.txt @@ -4,6 +4,12 @@ java.lang !java.lang.invoke java.lang.System !java.lang.System#exit +!java.lang.Runtime +!java.lang.Process +!java.lang.ProcessBuilder +!java.lang.SecurityManager +!java.lang.ThreadDeath +!java.lang.ThreadGroup !java.util.concurrent java.text java.time diff --git a/website/src/App.tsx b/website/src/App.tsx index a1c979b..25fc83d 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -12,6 +12,10 @@ import { import {useShare} from './sheet/useShare' import ImportedSheet from './sheet/ImportedSheet' import ShareModal from './sheet/ShareModal' +import {createBlankSheet, createWelcomeSheet} from './sheet/defaultSheet' + +const welcomeSheet = createWelcomeSheet() +const blankSheet = createBlankSheet() export default function App() { const [evaluate, evaluating] = useEvaluate() @@ -41,8 +45,17 @@ export default function App() { captureSnippet={captureSnippet} /> + + + void @@ -24,6 +25,11 @@ export default function Header(properties: HeaderProperties) { onClick={properties.onShare} >{t('menu.share')} +