Skip to content

Commit dc1a181

Browse files
committed
Use docker java client
1 parent 7bb9d99 commit dc1a181

4 files changed

Lines changed: 155 additions & 37 deletions

File tree

JShellAPI/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ repositories {
2424
dependencies {
2525
implementation project(':JShellWrapper')
2626
implementation 'org.springframework.boot:spring-boot-starter-web'
27+
implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.4'
28+
implementation 'com.github.docker-java:docker-java-core:3.3.4'
29+
2730
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2831
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
2932
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.togetherjava.jshellapi.service;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.async.ResultCallback;
5+
import com.github.dockerjava.api.command.PullImageResultCallback;
6+
import com.github.dockerjava.api.model.*;
7+
import com.github.dockerjava.core.DefaultDockerClientConfig;
8+
import com.github.dockerjava.core.DockerClientImpl;
9+
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
10+
import org.springframework.stereotype.Service;
11+
12+
import java.io.*;
13+
import java.nio.charset.StandardCharsets;
14+
import java.time.Duration;
15+
import java.util.Arrays;
16+
import java.util.Set;
17+
import java.util.concurrent.TimeUnit;
18+
19+
@Service
20+
public class DockerService implements AutoCloseable {
21+
22+
private final DockerClient client;
23+
24+
public DockerService() {
25+
DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
26+
ApacheDockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
27+
.dockerHost(clientConfig.getDockerHost())
28+
.sslConfig(clientConfig.getSSLConfig())
29+
.responseTimeout(Duration.ofSeconds(60))
30+
.connectionTimeout(Duration.ofSeconds(60))
31+
.build();
32+
this.client = DockerClientImpl.getInstance(clientConfig, httpClient);
33+
}
34+
35+
public String spawnContainer(
36+
long maxMemoryMegs, long cpus, String name, Duration evalTimeout, long sysoutLimit
37+
) throws InterruptedException {
38+
String imageName = "togetherjava.org:5001/togetherjava/jshellwrapper";
39+
boolean presentLocally = client.listImagesCmd()
40+
.withImageNameFilter(imageName)
41+
.exec()
42+
.stream()
43+
.anyMatch(it -> Arrays.asList(it.getRepoTags()).contains("master"));
44+
45+
if (!presentLocally) {
46+
client.pullImageCmd(imageName)
47+
.withTag("master")
48+
.exec(new PullImageResultCallback())
49+
.awaitCompletion(5, TimeUnit.MINUTES);
50+
}
51+
52+
return client.createContainerCmd(
53+
imageName + ":master"
54+
)
55+
.withHostConfig(
56+
HostConfig.newHostConfig()
57+
.withAutoRemove(true)
58+
.withInit(true)
59+
.withCapDrop(Capability.ALL)
60+
.withNetworkMode("none")
61+
.withPidsLimit(2000L)
62+
.withReadonlyRootfs(true)
63+
.withMemory(maxMemoryMegs * 1024 * 1024)
64+
.withCpuCount(cpus)
65+
)
66+
.withStdinOpen(true)
67+
.withAttachStdin(true)
68+
.withAttachStderr(true)
69+
.withAttachStdout(true)
70+
.withEnv("evalTimeoutSeconds=" + evalTimeout.toSeconds(), "sysOutCharLimit=" + sysoutLimit)
71+
.withName(name)
72+
.exec()
73+
.getId();
74+
}
75+
76+
public InputStream startAndAttachToContainer(String containerId, InputStream stdin) throws IOException {
77+
PipedInputStream pipeIn = new PipedInputStream();
78+
PipedOutputStream pipeOut = new PipedOutputStream(pipeIn);
79+
80+
client.attachContainerCmd(containerId)
81+
.withLogs(true)
82+
.withFollowStream(true)
83+
.withStdOut(true)
84+
.withStdErr(true)
85+
.withStdIn(stdin)
86+
.exec(new ResultCallback.Adapter<>() {
87+
@Override
88+
public void onNext(Frame object) {
89+
try {
90+
String payloadString = new String(object.getPayload(), StandardCharsets.UTF_8);
91+
if (object.getStreamType() == StreamType.STDOUT) {
92+
pipeOut.write(object.getPayload());
93+
} else {
94+
System.err.println(":( " + payloadString);
95+
}
96+
} catch (IOException e) {
97+
throw new UncheckedIOException(e);
98+
}
99+
}
100+
});
101+
102+
client.startContainerCmd(containerId).exec();
103+
return pipeIn;
104+
}
105+
106+
public void killContainerByName(String name) {
107+
for (Container container : client.listContainersCmd().withNameFilter(Set.of(name)).exec()) {
108+
client.killContainerCmd(container.getId()).exec();
109+
}
110+
}
111+
112+
@Override
113+
public void close() throws Exception {
114+
client.close();
115+
}
116+
117+
public boolean isDead(String containerName) {
118+
return client.listContainersCmd().withNameFilter(Set.of(containerName)).exec().isEmpty();
119+
}
120+
}

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
import org.togetherjava.jshellapi.dto.*;
55
import org.togetherjava.jshellapi.exceptions.DockerException;
66

7-
import java.io.BufferedReader;
8-
import java.io.BufferedWriter;
9-
import java.io.File;
10-
import java.io.IOException;
7+
import java.io.*;
118
import java.nio.file.Files;
129
import java.nio.file.Path;
10+
import java.time.Duration;
1311
import java.time.Instant;
1412
import java.util.ArrayList;
1513
import java.util.List;
@@ -18,16 +16,17 @@
1816
public class JShellService implements Closeable {
1917
private final JShellSessionService sessionService;
2018
private final String id;
21-
private Process process;
2219
private final BufferedWriter writer;
2320
private final BufferedReader reader;
2421

2522
private Instant lastTimeoutUpdate;
2623
private final long timeout;
2724
private final boolean renewable;
2825
private boolean doingOperation;
26+
private final DockerService dockerService;
2927

30-
public JShellService(JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int sysOutCharLimit, int maxMemory, double cpus, String startupScript) throws DockerException {
28+
public JShellService(DockerService dockerService, JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int sysOutCharLimit, int maxMemory, double cpus, String startupScript) throws DockerException {
29+
this.dockerService = dockerService;
3130
this.sessionService = sessionService;
3231
this.id = id;
3332
this.timeout = timeout;
@@ -39,30 +38,23 @@ public JShellService(JShellSessionService sessionService, String id, long timeou
3938
Files.createDirectories(errorLogs.getParent());
4039
Files.createFile(errorLogs);
4140
}
42-
process = new ProcessBuilder(
43-
"docker",
44-
"run",
45-
"--rm",
46-
"-i",
47-
"--init",
48-
"--cap-drop=ALL",
49-
"--network=none",
50-
"--pids-limit=2000",
51-
"--read-only",
52-
"--memory=" + maxMemory + "m",
53-
"--cpus=" + cpus,
54-
"--name", containerName(),
55-
"-e", "\"evalTimeoutSeconds=%d\"".formatted(evalTimeout),
56-
"-e", "\"sysOutCharLimit=%d\"".formatted(sysOutCharLimit),
57-
"togetherjava.org:5001/togetherjava/jshellwrapper:master")
58-
.directory(new File(".."))
59-
.redirectError(errorLogs.toFile())
60-
.start();
61-
writer = process.outputWriter();
62-
reader = process.inputReader();
41+
String containerId = dockerService.spawnContainer(
42+
maxMemory,
43+
(long) Math.ceil(cpus),
44+
containerName(),
45+
Duration.ofSeconds(evalTimeout),
46+
sysOutCharLimit
47+
);
48+
PipedInputStream containerInput = new PipedInputStream();
49+
this.writer = new BufferedWriter(new OutputStreamWriter(new PipedOutputStream(containerInput)));
50+
InputStream containerOutput = dockerService.startAndAttachToContainer(
51+
containerId,
52+
containerInput
53+
);
54+
reader = new BufferedReader(new InputStreamReader(containerOutput));
6355
writer.write(sanitize(startupScript));
6456
writer.newLine();
65-
} catch (IOException e) {
57+
} catch (IOException | InterruptedException e) {
6658
throw new DockerException(e);
6759
}
6860
this.doingOperation = false;
@@ -86,7 +78,7 @@ public Optional<JShellResult> eval(String code) throws DockerException {
8678
checkContainerOK();
8779

8880
return Optional.of(readResult());
89-
} catch (IOException | NumberFormatException ex) {
81+
} catch (DockerException | IOException | NumberFormatException ex) {
9082
close();
9183
throw new DockerException(ex);
9284
} finally {
@@ -185,27 +177,22 @@ public String id() {
185177

186178
@Override
187179
public void close() {
188-
process.destroyForcibly();
189180
try {
190181
try {
191182
writer.close();
192183
} finally {
193184
reader.close();
194185
}
195-
new ProcessBuilder("docker", "kill", containerName())
196-
.directory(new File(".."))
197-
.start()
198-
.waitFor();
199-
} catch(IOException | InterruptedException ex) {
186+
dockerService.killContainerByName(containerName());
187+
} catch(IOException ex) {
200188
throw new RuntimeException(ex);
201189
}
202-
process = null;
203190
sessionService.notifyDeath(id);
204191
}
205192

206193
@Override
207194
public boolean isClosed() {
208-
return process == null;
195+
return dockerService.isDead(containerName());
209196
}
210197

211198
private void updateLastTimeout() {

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ public class JShellSessionService {
1818
private Config config;
1919
private StartupScriptsService startupScriptsService;
2020
private ScheduledExecutorService scheduler;
21+
private DockerService dockerService;
2122
private final Map<String, JShellService> jshellSessions = new HashMap<>();
23+
2224
private void initScheduler() {
2325
scheduler = Executors.newSingleThreadScheduledExecutor();
2426
scheduler.scheduleAtFixedRate(() -> {
@@ -74,6 +76,7 @@ private synchronized JShellService createSession(String id, long sessionTimeout,
7476
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Too many sessions, try again later :(.");
7577
}
7678
JShellService service = new JShellService(
79+
dockerService,
7780
this,
7881
id,
7982
sessionTimeout,
@@ -97,4 +100,9 @@ public void setConfig(Config config) {
97100
public void setStartupScriptsService(StartupScriptsService startupScriptsService) {
98101
this.startupScriptsService = startupScriptsService;
99102
}
103+
104+
@Autowired
105+
public void setDockerService(DockerService dockerService) {
106+
this.dockerService = dockerService;
107+
}
100108
}

0 commit comments

Comments
 (0)