Skip to content

Commit f98b3b7

Browse files
committed
Added docker command to lj-example
1 parent ba04843 commit f98b3b7

File tree

6 files changed

+396
-0
lines changed

6 files changed

+396
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.javadiscord.bot.commands.slash;
2+
3+
import java.awt.*;
4+
import java.io.OutputStream;
5+
import java.time.Instant;
6+
import java.util.Optional;
7+
import java.util.concurrent.Executors;
8+
import java.util.concurrent.ScheduledExecutorService;
9+
import java.util.concurrent.ThreadLocalRandom;
10+
import java.util.concurrent.TimeUnit;
11+
12+
import com.javadiscord.bot.utils.docker.*;
13+
import com.javadiscord.jdi.core.CommandOptionType;
14+
import com.javadiscord.jdi.core.annotations.CommandOption;
15+
import com.javadiscord.jdi.core.annotations.SlashCommand;
16+
import com.javadiscord.jdi.core.interaction.SlashCommandEvent;
17+
import com.javadiscord.jdi.core.models.application.ApplicationCommandOption;
18+
import com.javadiscord.jdi.core.models.message.embed.Embed;
19+
import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor;
20+
import com.javadiscord.jdi.core.models.user.User;
21+
22+
import com.github.dockerjava.api.DockerClient;
23+
import com.github.dockerjava.api.command.CreateContainerResponse;
24+
import org.apache.logging.log4j.LogManager;
25+
import org.apache.logging.log4j.Logger;
26+
27+
public class LinuxCommand {
28+
private static final Logger LOGGER = LogManager.getLogger(LinuxCommand.class);
29+
private static final ScheduledExecutorService EXECUTOR_SERVICE =
30+
Executors.newSingleThreadScheduledExecutor();
31+
32+
private final DockerClient dockerClient;
33+
public final DockerSessions dockerSessions;
34+
private final DockerCommandRunner commandRunner;
35+
36+
public LinuxCommand(
37+
DockerClient dockerClient,
38+
DockerSessions dockerSessions,
39+
DockerCommandRunner commandRunner
40+
) {
41+
this.dockerClient = dockerClient;
42+
this.dockerSessions = dockerSessions;
43+
this.commandRunner = commandRunner;
44+
45+
Runtime.getRuntime()
46+
.addShutdownHook(
47+
new Thread(
48+
() -> dockerSessions
49+
.getSessions()
50+
.forEach(dockerSessions::stopContainer)
51+
)
52+
);
53+
54+
EXECUTOR_SERVICE.scheduleAtFixedRate(
55+
new ContainerCleanupTask(dockerSessions), 0, 5, TimeUnit.MINUTES
56+
);
57+
}
58+
59+
@SlashCommand(
60+
name = "linux", description = "Run commands in your very own Linux session", options = {
61+
@CommandOption(
62+
name = "code", description = "The command you would like to run", type = CommandOptionType.STRING
63+
)
64+
}
65+
)
66+
public void handle(SlashCommandEvent event) {
67+
event.deferReply();
68+
69+
Optional<ApplicationCommandOption> codeOption = event.option("code");
70+
User user = event.user();
71+
72+
codeOption.ifPresent(option -> {
73+
LOGGER.info("Running command {}", option.valueAsString());
74+
75+
Thread.ofVirtual().start(() -> handleLinuxCommand(event, option.valueAsString(), user));
76+
});
77+
}
78+
79+
private void handleLinuxCommand(SlashCommandEvent event, String command, User member) {
80+
String memberId = String.valueOf(member.id());
81+
Session session = getSessionForUser(memberId);
82+
try (OutputStream output = commandRunner.sendCommand(session, command)) {
83+
String reply =
84+
"""
85+
Ran command:
86+
```
87+
$ %s
88+
```
89+
90+
```java
91+
%s
92+
```
93+
94+
Session expires in %s
95+
"""
96+
.formatted(command, output, getSessionExpiry(session));
97+
98+
Embed embed =
99+
new Embed.Builder()
100+
.author(new EmbedAuthor(member.asMention(), null, null, null))
101+
.description(shortenOutput(reply))
102+
.color(Color.RED)
103+
.build();
104+
105+
event.reply(embed);
106+
} catch (Exception e) {
107+
LOGGER.error(e);
108+
event.reply("An error occurred: " + e.getMessage());
109+
}
110+
}
111+
112+
private String getSessionExpiry(Session session) {
113+
Instant expiry = session.getStartTime().plusSeconds(TimeUnit.MINUTES.toSeconds(5));
114+
long epochSeconds = expiry.getEpochSecond();
115+
return "<t:" + epochSeconds + ":R>";
116+
}
117+
118+
private String shortenOutput(String input) {
119+
String concatMessage = "\n**Rest of the output as been removed as it was too long**\n";
120+
if (input.length() > 4096) {
121+
input = input.substring(0, 4096 - concatMessage.length()) + concatMessage;
122+
}
123+
StringBuilder sb = new StringBuilder();
124+
String[] parts = input.split("\n");
125+
if (parts.length > 50) {
126+
for (int i = 50; i > 0; i--) {
127+
sb.append(parts[i]).append("\n");
128+
}
129+
sb.append(concatMessage);
130+
} else {
131+
sb.append(input);
132+
}
133+
return sb.toString();
134+
}
135+
136+
private Session getSessionForUser(String name) {
137+
if (!dockerSessions.hasSession(name)) {
138+
139+
LOGGER.info("Creating new session for {}", name);
140+
141+
DockerContainerCreator containerCreator = new DockerContainerCreator(dockerClient);
142+
143+
CreateContainerResponse createContainerResponse =
144+
containerCreator.createContainerStarted(
145+
"session-" + ThreadLocalRandom.current().nextInt(),
146+
"ubuntu:latest",
147+
mb(256),
148+
mb(256),
149+
512,
150+
100000,
151+
cpuQuota(100000, 0.5)
152+
);
153+
154+
return dockerSessions.createSession(name, createContainerResponse.getId());
155+
}
156+
157+
LOGGER.info("Found existing session for {}", name);
158+
159+
return dockerSessions.getSessionForUser(name);
160+
}
161+
162+
public static long mb(long megabytes) {
163+
return megabytes * 1024 * 1024;
164+
}
165+
166+
public static long cpuQuota(int cpuPeriod, double percentage) {
167+
return (long) (cpuPeriod * (percentage / 10.0));
168+
}
169+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.javadiscord.bot.utils.docker;
2+
3+
import java.time.Instant;
4+
import java.util.Iterator;
5+
import java.util.concurrent.TimeUnit;
6+
7+
public class ContainerCleanupTask implements Runnable {
8+
private static final long CONTAINER_DURATION = TimeUnit.MINUTES.toSeconds(5);
9+
10+
private final DockerSessions dockerSessions;
11+
12+
public ContainerCleanupTask(DockerSessions dockerSessions) {
13+
this.dockerSessions = dockerSessions;
14+
}
15+
16+
@Override
17+
public void run() {
18+
Instant now = Instant.now();
19+
Iterator<Session> it = dockerSessions.getSessions().iterator();
20+
while (it.hasNext()) {
21+
Session session = it.next();
22+
Instant sessionStart = session.getStartTime();
23+
boolean sessionExpired = sessionStart.plusSeconds(CONTAINER_DURATION).isAfter(now);
24+
if (sessionExpired) {
25+
dockerSessions.stopContainer(session);
26+
it.remove();
27+
}
28+
}
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.javadiscord.bot.utils.docker;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.OutputStream;
5+
6+
import com.github.dockerjava.api.DockerClient;
7+
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
8+
import com.github.dockerjava.core.command.ExecStartResultCallback;
9+
10+
public class DockerCommandRunner {
11+
private final DockerClient dockerClient;
12+
13+
public DockerCommandRunner(DockerClient dockerClient) {
14+
this.dockerClient = dockerClient;
15+
}
16+
17+
public OutputStream sendCommand(Session session, String command) throws InterruptedException {
18+
session.updateHistory(command);
19+
20+
OutputStream output = new ByteArrayOutputStream();
21+
22+
ExecCreateCmdResponse execCreateCmdResponse =
23+
dockerClient
24+
.execCreateCmd(session.getContainerId())
25+
.withAttachStdout(true)
26+
.withAttachStderr(true)
27+
.withCmd("bash", "-c", command.trim())
28+
.exec();
29+
30+
dockerClient
31+
.execStartCmd(execCreateCmdResponse.getId())
32+
.exec(new ExecStartResultCallback(output, output))
33+
.awaitCompletion();
34+
35+
return output;
36+
}
37+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.javadiscord.bot.utils.docker;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.command.CreateContainerResponse;
5+
import com.github.dockerjava.api.model.HostConfig;
6+
7+
public class DockerContainerCreator {
8+
private final DockerClient dockerClient;
9+
10+
public DockerContainerCreator(DockerClient dockerClient) {
11+
this.dockerClient = dockerClient;
12+
}
13+
14+
public CreateContainerResponse createContainerStarted(
15+
String name,
16+
String image,
17+
long memoryLimit,
18+
long memorySwapLimit,
19+
int cpuShares,
20+
long cpuPeriod,
21+
long cpuQuota
22+
) {
23+
24+
CreateContainerResponse createContainerResponse =
25+
createContainer(
26+
name, image, memoryLimit, memorySwapLimit, cpuShares, cpuPeriod, cpuQuota
27+
);
28+
29+
startContainer(createContainerResponse.getId());
30+
31+
return createContainerResponse;
32+
}
33+
34+
public CreateContainerResponse createContainer(
35+
String name,
36+
String image,
37+
long memoryLimit,
38+
long memorySwapLimit,
39+
int cpuShares,
40+
long cpuPeriod,
41+
long cpuQuota
42+
) {
43+
44+
HostConfig hostConfig =
45+
HostConfig.newHostConfig()
46+
.withAutoRemove(true)
47+
.withInit(true)
48+
.withMemory(memoryLimit)
49+
.withMemorySwap(memorySwapLimit)
50+
.withCpuShares(cpuShares)
51+
.withCpuPeriod(cpuPeriod)
52+
.withCpuQuota(cpuQuota);
53+
54+
return dockerClient
55+
.createContainerCmd(image)
56+
.withHostConfig(hostConfig)
57+
.withStdinOpen(true)
58+
.withName(name)
59+
.exec();
60+
}
61+
62+
public void startContainer(String containerId) {
63+
dockerClient.startContainerCmd(containerId).exec();
64+
}
65+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.javadiscord.bot.utils.docker;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import com.github.dockerjava.api.DockerClient;
7+
import org.apache.logging.log4j.LogManager;
8+
import org.apache.logging.log4j.Logger;
9+
10+
public class DockerSessions {
11+
private static final Logger LOGGER = LogManager.getLogger();
12+
private final DockerClient dockerClient;
13+
private final List<Session> sessions = new ArrayList<>();
14+
15+
public DockerSessions(DockerClient dockerClient) {
16+
this.dockerClient = dockerClient;
17+
}
18+
19+
public Session createSession(String userId, String containerId) {
20+
Session session = new Session(userId, containerId);
21+
sessions.add(session);
22+
LOGGER.info("Created new session for {}", userId);
23+
return session;
24+
}
25+
26+
public void removeSession(Session session) {
27+
sessions.remove(session);
28+
dockerClient.stopContainerCmd(session.getContainerId()).exec();
29+
}
30+
31+
public void stopContainer(Session session) {
32+
dockerClient.stopContainerCmd(session.getContainerId()).exec();
33+
}
34+
35+
public List<Session> getSessions() {
36+
return sessions;
37+
}
38+
39+
public boolean hasSession(String userId) {
40+
for (Session session : sessions) {
41+
if (session.getSessionId().equalsIgnoreCase(userId)) {
42+
return true;
43+
}
44+
}
45+
return false;
46+
}
47+
48+
public Session getSessionForUser(String userId) {
49+
for (Session session : sessions) {
50+
if (session.getSessionId().equals(userId)) {
51+
return session;
52+
}
53+
}
54+
throw new RuntimeException("No session found for user");
55+
}
56+
}

0 commit comments

Comments
 (0)