Skip to content

Commit 668ceae

Browse files
committed
Added startup scripts
1 parent fdb5a6a commit 668ceae

11 files changed

Lines changed: 171 additions & 24 deletions

File tree

JShellAPI/GUIDE.MD

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
### Eval
33
Everything works with sessions, the code is evaluated on a session which has a certain lifetime.
44
```
5-
POST /eval/{id} body=code -> JShellResult
6-
POST /eval body=code -> JShellResultWithId
7-
POST /single-eval body=code -> JShellResult
5+
POST /eval/{id} body=code -> JShellResult params=startupScriptId:id
6+
POST /eval body=code -> JShellResultWithId params=startupScriptId:id
7+
POST /single-eval body=code -> JShellResult params=startupScriptId:id
88
```
99
- The first one takes an id, create a session from this id, or use an existing session if this id already exists.
1010
- The second one creates a new session each time, with a random id, and so it returns the generated id, in order to be reused.
1111
- The third one creates a session that can only be used once, not only that, but this session is called a one-time session and has lower timeout.
12+
- the optional parameter startupScriptId may be added to specify the startup script id.
13+
1214
#### Response
1315
In all three, a string containing code to be evaluated needs to be supplied, and a JShellResult will be returned containing the result :
1416
```java
@@ -40,11 +42,14 @@ record JShellResultWithId(
4042
```
4143
### Other endpoints
4244
```
43-
GET /snippets/{id} body=none -> List<String>
44-
DELETE /{id} body=none -> none
45+
GET /snippets/{id} body=none -> List<String> params=includeStartupScript:boolean
46+
DELETE /{id} body=none -> none
47+
GET /startup_script/{id} body=none -> String
4548
```
4649
- The first one will retrieve all snippets of a given session
4750
- The second one will forcibly delete the given session
51+
- the optional parameter includeStartupScript may be supplied to indicate if the startup script should be returned, by default false
52+
- The third one will return the startup script for the corresponding startup script id
4853
### Errors
4954
if a problem happens, by default a 5XX error will be thrown, except in those cases :
5055
- Given [id](#Identifiers) is invalid : 400 Bad Request.
@@ -74,6 +79,13 @@ The identifier must match the following regex :
7479
[a-zA-Z0-9][a-zA-Z0-9_.-]+
7580
```
7681
A random identifier is simply a random uuid.
82+
### Startup script
83+
A startup script can be used, so it's easier to use jshell.
84+
They are special scripts that are automatically evaluated at the launch of a session, to change the startup script of a session, the session must be deleted.
85+
The startup scripts are the two first scripts of a session, the first being the imports, the second the rest of the startup script.
86+
The following startup scripts id can be used :
87+
- EMPTY : no startup script
88+
- CUSTOM_DEFAULT : contains basic imports, print methods and range method
7789
## Configuration
7890
Properties can be defined in resources/application.properties
7991
### jshellapi.regularSessionTimeoutSeconds

JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,38 @@
99
import org.togetherjava.jshellapi.exceptions.DockerException;
1010
import org.togetherjava.jshellapi.service.JShellService;
1111
import org.togetherjava.jshellapi.service.JShellSessionService;
12+
import org.togetherjava.jshellapi.service.StartupScriptId;
13+
import org.togetherjava.jshellapi.service.StartupScriptsService;
1214

1315
import java.util.List;
1416

1517
@RequestMapping("jshell")
1618
@RestController
1719
public class JShellController {
1820
private JShellSessionService service;
21+
private StartupScriptsService startupScriptsService;
1922

2023
@PostMapping("/eval/{id}")
21-
public JShellResult eval(@PathVariable String id, @RequestBody String code) throws DockerException {
24+
public JShellResult eval(@PathVariable String id, @RequestParam(required = false) StartupScriptId startupScriptId, @RequestBody String code) throws DockerException {
2225
validateId(id);
23-
return service.session(id).eval(code).orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running"));
26+
return service.session(id, startupScriptId).eval(code).orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running"));
2427
}
2528
@PostMapping("/eval")
26-
public JShellResultWithId eval(@RequestBody String code) throws DockerException {
27-
JShellService jShellService = service.session();
29+
public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId startupScriptId, @RequestBody String code) throws DockerException {
30+
JShellService jShellService = service.session(startupScriptId);
2831
return new JShellResultWithId(jShellService.id(), jShellService.eval(code).orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running")));
2932
}
3033
@PostMapping("/single-eval")
31-
public JShellResult singleEval(@RequestBody String code) throws DockerException {
32-
JShellService jShellService = service.oneTimeSession();
34+
public JShellResult singleEval(@RequestParam(required = false) StartupScriptId startupScriptId, @RequestBody String code) throws DockerException {
35+
JShellService jShellService = service.oneTimeSession(startupScriptId);
3336
return jShellService.eval(code).orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running"));
3437
}
3538

3639
@GetMapping("/snippets/{id}")
37-
public List<String> snippets(@PathVariable String id) throws DockerException {
40+
public List<String> snippets(@PathVariable String id, @RequestParam(required = false) boolean includeStartupScript) throws DockerException {
3841
validateId(id);
3942
if(!service.hasSession(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found");
40-
return service.session(id).snippets().orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running"));
43+
return service.session(id, null).snippets().map(l -> includeStartupScript || l.size() < 2 ? l : l.subList(2, l.size())).orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, "An operation is already running"));
4144
}
4245

4346
@DeleteMapping("/{id}")
@@ -47,11 +50,23 @@ public void delete(@PathVariable String id) throws DockerException {
4750
service.deleteSession(id);
4851
}
4952

53+
@GetMapping("/startup_script/{id}")
54+
public String startupScript(@PathVariable StartupScriptId id) {
55+
String imports = startupScriptsService.getImports(id);
56+
String sep = imports.endsWith("\n") ? "\n" : "\n\n";
57+
return imports + sep + startupScriptsService.getScript(id);
58+
}
59+
5060
@Autowired
5161
public void setService(JShellSessionService service) {
5262
this.service = service;
5363
}
5464

65+
@Autowired
66+
public void setStartupScriptsService(StartupScriptsService startupScriptsService) {
67+
this.startupScriptsService = startupScriptsService;
68+
}
69+
5570
private static void validateId(String id) throws ResponseStatusException {
5671
if(!id.matches("[a-zA-Z0-9][a-zA-Z0-9_.-]+")) {
5772
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+");

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class JShellService implements Closeable {
3030
private final boolean renewable;
3131
private boolean doingOperation;
3232

33-
public JShellService(JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int maxMemory, double cpus) throws DockerException {
33+
public JShellService(JShellSessionService sessionService, String id, long timeout, boolean renewable, long evalTimeout, int maxMemory, double cpus, String startupImports, String startupScript) throws DockerException {
3434
this.sessionService = sessionService;
3535
this.id = id;
3636
this.timeout = timeout;
@@ -63,6 +63,10 @@ public JShellService(JShellSessionService sessionService, String id, long timeou
6363
.start();
6464
writer = process.outputWriter();
6565
reader = process.inputReader();
66+
writer.write(sanitize(startupImports));
67+
writer.newLine();
68+
writer.write(sanitize(startupScript));
69+
writer.newLine();
6670
} catch (IOException e) {
6771
throw new DockerException(e);
6872
}
@@ -226,6 +230,10 @@ private void stopOperation() {
226230
doingOperation = false;
227231
}
228232

233+
private static String sanitize(String s) {
234+
return s.replace("\\", "\\\\").replace("\n", "\\n");
235+
}
236+
229237
private static String desanitize(String text) {
230238
return text.replace("\\n", "\n").replace("\\\\", "\\");
231239
}

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.springframework.beans.factory.annotation.Autowired;
44
import org.springframework.http.HttpStatus;
5+
import org.springframework.lang.Nullable;
56
import org.springframework.stereotype.Service;
67
import org.springframework.web.server.ResponseStatusException;
78
import org.togetherjava.jshellapi.Config;
@@ -15,6 +16,7 @@
1516
@Service
1617
public class JShellSessionService {
1718
private Config config;
19+
private StartupScriptsService startupScriptsService;
1820
private ScheduledExecutorService scheduler;
1921
private final Map<String, JShellService> jshellSessions = new HashMap<>();
2022
private void initScheduler() {
@@ -45,17 +47,17 @@ public boolean hasSession(String id) {
4547
return jshellSessions.containsKey(id);
4648
}
4749

48-
public JShellService session(String id) throws DockerException {
50+
public JShellService session(String id, @Nullable StartupScriptId startupScriptId) throws DockerException {
4951
if(!hasSession(id)) {
50-
return createSession(id, config.regularSessionTimeoutSeconds(), true, config.evalTimeoutSeconds());
52+
return createSession(id, config.regularSessionTimeoutSeconds(), true, config.evalTimeoutSeconds(), startupScriptId);
5153
}
5254
return jshellSessions.get(id);
5355
}
54-
public JShellService session() throws DockerException {
55-
return createSession(UUID.randomUUID().toString(), config.regularSessionTimeoutSeconds(), false, config.evalTimeoutSeconds());
56+
public JShellService session(@Nullable StartupScriptId startupScriptId) throws DockerException {
57+
return createSession(UUID.randomUUID().toString(), config.regularSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), startupScriptId);
5658
}
57-
public JShellService oneTimeSession() throws DockerException {
58-
return createSession(UUID.randomUUID().toString(), config.oneTimeSessionTimeoutSeconds(), false, config.evalTimeoutSeconds());
59+
public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId) throws DockerException {
60+
return createSession(UUID.randomUUID().toString(), config.oneTimeSessionTimeoutSeconds(), false, config.evalTimeoutSeconds(), startupScriptId);
5961
}
6062

6163
public void deleteSession(String id) throws DockerException {
@@ -64,14 +66,23 @@ public void deleteSession(String id) throws DockerException {
6466
scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS);
6567
}
6668

67-
private synchronized JShellService createSession(String id, long sessionTimeout, boolean renewable, long evalTimeout) throws DockerException {
69+
private synchronized JShellService createSession(String id, long sessionTimeout, boolean renewable, long evalTimeout, @Nullable StartupScriptId startupScriptId) throws DockerException {
6870
if(hasSession(id)) { //Just in case race condition happens just before createSession
6971
return jshellSessions.get(id);
7072
}
7173
if(jshellSessions.size() >= config.maxAliveSessions()) {
7274
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Too many sessions, try again later :(.");
7375
}
74-
JShellService service = new JShellService(this, id, sessionTimeout, renewable, evalTimeout, config.dockerMaxRamMegaBytes(), config.dockerCPUsUsage());
76+
JShellService service = new JShellService(
77+
this,
78+
id,
79+
sessionTimeout,
80+
renewable,
81+
evalTimeout,
82+
config.dockerMaxRamMegaBytes(),
83+
config.dockerCPUsUsage(),
84+
startupScriptsService.getImports(startupScriptId),
85+
startupScriptsService.getScript(startupScriptId));
7586
jshellSessions.put(id, service);
7687
return service;
7788
}
@@ -81,4 +92,9 @@ public void setConfig(Config config) {
8192
this.config = config;
8293
initScheduler();
8394
}
95+
96+
@Autowired
97+
public void setStartupScriptsService(StartupScriptsService startupScriptsService) {
98+
this.startupScriptsService = startupScriptsService;
99+
}
84100
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.togetherjava.jshellapi.service;
2+
3+
public enum StartupScriptId {
4+
EMPTY, CUSTOM_DEFAULT;
5+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.togetherjava.jshellapi.service;
2+
3+
import org.springframework.lang.Nullable;
4+
import org.springframework.stereotype.Service;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.io.UncheckedIOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.EnumMap;
11+
import java.util.Map;
12+
import java.util.Objects;
13+
import java.util.function.Function;
14+
15+
@Service
16+
public class StartupScriptsService {
17+
private record StartupScript(String imports, String script) {}
18+
19+
private final Map<StartupScriptId, StartupScript> scripts;
20+
21+
private StartupScriptsService() {
22+
scripts = new EnumMap<>(StartupScriptId.class);
23+
for (StartupScriptId id : StartupScriptId.values()) {
24+
try (
25+
InputStream importsStream = Objects.requireNonNull(StartupScriptsService.class.getResourceAsStream("/jshell_startup/imports/" + id + ".jsh"), "Couldn't load script " + id);
26+
InputStream scriptStream = Objects.requireNonNull(StartupScriptsService.class.getResourceAsStream("/jshell_startup/scripts/" + id + ".jsh"), "Couldn't load script " + id)) {
27+
String imports = new String(importsStream.readAllBytes(), StandardCharsets.UTF_8);
28+
String script = new String(scriptStream.readAllBytes(), StandardCharsets.UTF_8);
29+
scripts.put(id, new StartupScript(imports, script));
30+
} catch (IOException e) {
31+
throw new UncheckedIOException(e);
32+
}
33+
}
34+
}
35+
private String get(@Nullable StartupScriptId id, Function<StartupScript, String> function) {
36+
StartupScript startupScript = scripts.get(id);
37+
return startupScript != null ? function.apply(startupScript) : function.apply(scripts.get(StartupScriptId.EMPTY));
38+
}
39+
40+
/**
41+
* Returns corresponding imports, or default imports if id is null
42+
* @param id the id or the imports, can be null
43+
* @return corresponding imports, or default imports if id is null
44+
*/
45+
public String getImports(@Nullable StartupScriptId id) {
46+
return get(id, StartupScript::imports);
47+
}
48+
49+
/**
50+
* Returns corresponding script, or default script if id is null
51+
* @param id the id or the script, can be null
52+
* @return corresponding script, or default script if id is null
53+
*/
54+
public String getScript(@Nullable StartupScriptId id) {
55+
return get(id, StartupScript::script);
56+
}
57+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import java.nio.charset.*;
2+
import java.nio.file.*;
3+
import java.text.*;
4+
import java.time.*;
5+
import java.time.chrono.*;
6+
import java.time.format.*;
7+
import java.time.temporal.*;
8+
import java.time.zone.*;
9+
import java.nio.charset.*;
10+
import java.util.concurrent.atomic.*;
11+
import java.util.concurrent.locks.*;
12+
import java.util.random.*;
13+
import java.math.*;
14+
import java.util.*;
15+
import java.util.concurrent.*;
16+
import java.util.function.*;
17+
import java.util.prefs.*;
18+
import java.util.regex.*;
19+
import java.util.stream.*;
20+

JShellAPI/src/main/resources/jshell_startup/imports/EMPTY.jsh

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void print(Object o) { System.out.print(o); }
2+
void println(Object o) { System.out.println(o); }
3+
void printf(String s, Object... args) { System.out.printf(s, args); }
4+
5+
Iterable<Integer> range(int startInclusive, int endExclusive) {
6+
return IntStream.range(startInclusive, endExclusive)::iterator;
7+
}

JShellAPI/src/main/resources/jshell_startup/scripts/EMPTY.jsh

Whitespace-only changes.

0 commit comments

Comments
 (0)