Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit ff84261

Browse files
committed
feat(serve): added /interact, /execute and /start endpoints
1 parent dee4eaf commit ff84261

File tree

3 files changed

+140
-17
lines changed

3 files changed

+140
-17
lines changed

src/Environment.ts

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -426,18 +426,37 @@ export default class Environment {
426426
})
427427
}
428428

429+
/**
430+
* Get an array suitable for passing to `spawn` to execute a docker command with environment variables for nixster
431+
*
432+
* @param sessionParameters SessionParameters that define the container to exec inside and the command to run
433+
* @param daemonize Should the Docker command be run with the '-d' flag?
434+
*/
435+
private getDockerExecCommand (sessionParameters: SessionParameters, daemonize: boolean): Array<string> {
436+
const nixLocation = nix.location(this.name)
437+
const shellArgs = [
438+
'exec', '--tty',
439+
// Prepend the environment path to the PATH variable
440+
'--env', `PATH=${nixLocation}/bin:${nixLocation}/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
441+
sessionParameters.containerId, sessionParameters.command
442+
]
443+
444+
if (daemonize) shellArgs.splice(1, 0, '-d')
445+
446+
return shellArgs
447+
}
448+
429449
/**
430450
* Get an array suitable for passing to `spawn` to execute a docker command with default args for nixster
431451
*
432-
* @param dockerCommand The Docker command to execute
433452
* @param sessionParameters SessionParameters to use for limiting Docker's resource usage
434453
* @param daemonize Should the Docker command be run with the '-d' flag?
435454
*/
436-
private async getDockerShellArgs (dockerCommand: string, sessionParameters: SessionParameters, daemonize: boolean = false): Promise<Array<string>> {
455+
private getDockerRunCommand (sessionParameters: SessionParameters, daemonize: boolean = false): Array<string> {
437456
const { command, cpuShares, memoryLimit } = sessionParameters
438457
const nixLocation = nix.location(this.name)
439458
const shellArgs = [
440-
dockerCommand, '--interactive', '--tty', '--rm',
459+
'run', '--interactive', '--tty', '--rm',
441460
// Prepend the environment path to the PATH variable
442461
'--env', `PATH=${nixLocation}/bin:${nixLocation}/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
443462
// We also need to tell R where to find libraries
@@ -491,7 +510,7 @@ export default class Environment {
491510
break
492511
case Platform.DOCKER:
493512
shellName = 'docker'
494-
shellArgs = await this.getDockerShellArgs('run', sessionParameters, false)
513+
shellArgs = this.getDockerRunCommand(sessionParameters, false)
495514
break
496515
default:
497516
shellName = 'bash'
@@ -544,11 +563,26 @@ export default class Environment {
544563
}
545564
}
546565

566+
const shellProcess = this.runInShell(shellPath, shellArgs, vars, stdout, stdin)
567+
568+
if (platform === Platform.UNIX && command) shellProcess.write(command + '\r')
569+
}
570+
571+
/**
572+
* Spawn a new shell with the default setup for nixster, attaching stdout and stdin
573+
*
574+
* @param shellPath The command to run
575+
* @param shellArgs Arguments to pass to the command in `shellpath`
576+
* @param environmentVariables Environment variables to set in the shell
577+
* @param stdout Stream
578+
* @param stdin Stream
579+
*/
580+
private runInShell (shellPath: string, shellArgs: Array<string>, environmentVariables: { [key: string]: string }, stdout: stream.Writable, stdin: stream.Readable) {
547581
const shellProcess = pty.spawn(shellPath, shellArgs, {
548582
name: 'xterm-color',
549583
cols: 120,
550584
rows: 30,
551-
env: vars
585+
env: environmentVariables
552586
})
553587
shellProcess.on('data', data => {
554588
stdout.write(data)
@@ -575,8 +609,23 @@ export default class Environment {
575609
}
576610
shellProcess.write(data)
577611
})
612+
return shellProcess
613+
}
578614

579-
if (platform === Platform.UNIX && command) shellProcess.write(command + '\r')
615+
/**
616+
* Attach to a running container.
617+
*
618+
* @param sessionParameters The stdout and stdin attributes from here are used to connect to the shell
619+
*/
620+
async attach (sessionParameters: SessionParameters) {
621+
if (sessionParameters.platform !== Platform.DOCKER) {
622+
throw new Error('Attach is only valid for docker')
623+
}
624+
625+
if (!this.containerIsRunning(sessionParameters.containerId)) {
626+
throw new Error(`Container ${sessionParameters.containerId} is not running.`)
627+
}
628+
this.runInShell('docker', ['attach', sessionParameters.containerId], {}, sessionParameters.stdout, sessionParameters.stdin)
580629
}
581630

582631
/**
@@ -602,15 +651,33 @@ export default class Environment {
602651
*
603652
* Returns the short ID of the container that is running.
604653
*/
605-
async execute (sessionParameters: SessionParameters): Promise<string> {
654+
async start (sessionParameters: SessionParameters): Promise<string> {
655+
if (sessionParameters.platform !== Platform.DOCKER) {
656+
throw new Error('Start is only valid with the Docker platform.')
657+
}
658+
659+
const dockerProcess = await spawn('docker', this.getDockerRunCommand(sessionParameters, true))
660+
return dockerProcess.toString().trim().substr(0, DOCKER_CONTAINER_ID_SHORT_LENGTH)
661+
}
662+
663+
/**
664+
* Execute a command in a docker container. Will output the results of the command, or daemonize it in which case an
665+
* empty string is returned
666+
*
667+
* @param sessionParameters Contains the `containerId` and `command` that define where and what to execute
668+
* @param daemonize run the command in the background (pass -d flag to docker)
669+
*/
670+
async execute (sessionParameters: SessionParameters, daemonize: boolean = false): Promise<string> {
606671
if (sessionParameters.platform !== Platform.DOCKER) {
607672
throw new Error('Execute is only valid with the Docker platform.')
608673
}
609674

610-
const shellArgs = await this.getDockerShellArgs('run', sessionParameters, true)
675+
if (!this.containerIsRunning(sessionParameters.containerId)) {
676+
throw new Error(`Container ${sessionParameters.containerId} is not running`)
677+
}
611678

612-
const dockerProcess = await spawn('docker', shellArgs)
613-
return dockerProcess.toString().trim().substr(0, DOCKER_CONTAINER_ID_SHORT_LENGTH)
679+
const dockerProcess = await spawn('docker', this.getDockerExecCommand(sessionParameters, daemonize))
680+
return dockerProcess.toString().trim()
614681
}
615682

616683
/**

src/serve.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import stream from 'stream'
33

44
import express from 'express'
55

6-
import Environment, { SessionParameters, Platform } from './Environment'
6+
import Environment, { Platform, SessionParameters } from './Environment'
77

88
const app = express()
99
const expressWs = require('express-ws')(app)
@@ -15,6 +15,8 @@ app.use(express.static(path.join(__dirname, 'static')))
1515
const jsonParser = require('body-parser').json()
1616
app.use(jsonParser)
1717

18+
const DEFAULT_ENVIRONMENT = 'multi-mega'
19+
1820
/**
1921
* Validates that all the `requiredParameters` are properties of `body`.
2022
* Returns an array of all parameters that are missing.
@@ -76,6 +78,7 @@ function doRequestValidation (request: express.Request, response: express.Respon
7678
// todo: rename shell to interact
7779
// Instantiate shell and set up data handlers
7880
expressWs.app.ws('/shell', async (ws: any, req: express.Request) => {
81+
const environment = req.query.environment || DEFAULT_ENVIRONMENT
7982
try {
8083
// Create streams that pipe between the Websocket and
8184
// the pseudo terminal
@@ -94,11 +97,10 @@ expressWs.app.ws('/shell', async (ws: any, req: express.Request) => {
9497
}
9598
})
9699

97-
// For now, use an arbitrary, small environment for testing purposes.
98-
let env = new Environment('r')
100+
let env = new Environment(environment)
99101

100102
const sessionParameters = new SessionParameters()
101-
sessionParameters.platform = Platform.DOCKER
103+
sessionParameters.platform = req.query.platform === undefined ? Platform.UNIX : req.query.platform
102104
sessionParameters.stdin = stdin
103105
sessionParameters.stdout = stdout
104106
await env.enter(sessionParameters)
@@ -107,6 +109,43 @@ expressWs.app.ws('/shell', async (ws: any, req: express.Request) => {
107109
}
108110
})
109111

112+
// Instantiate shell and set up data handlers
113+
expressWs.app.ws('/interact', async (ws: any, req: express.Request) => {
114+
const environment = req.query.environment || DEFAULT_ENVIRONMENT
115+
const containerId = req.query.containerId || ''
116+
117+
try {
118+
// Create streams that pipe between the Websocket and
119+
// the pseudo terminal
120+
121+
// A pseudo stdin that receives data from the Websocket
122+
const stdin = new stream.PassThrough()
123+
ws.on('message', (msg: any) => {
124+
stdin.write(msg)
125+
})
126+
127+
// A pseudo stdout that writes data to the Websocket
128+
const stdout = new stream.Writable({
129+
write (chunk: Buffer, encoding: any, callback: any) {
130+
ws.send(chunk)
131+
callback()
132+
}
133+
})
134+
135+
let env = new Environment(environment)
136+
137+
const sessionParameters = new SessionParameters()
138+
sessionParameters.platform = Platform.DOCKER
139+
sessionParameters.containerId = containerId
140+
sessionParameters.stdin = stdin
141+
sessionParameters.stdout = stdout
142+
143+
await env.attach(sessionParameters)
144+
} catch (error) {
145+
console.error(error)
146+
}
147+
})
148+
110149
// Error handling middleware
111150
app.use((error: Error, req: express.Request, res: express.Response, next: any) => {
112151
console.error(error.stack)
@@ -115,7 +154,7 @@ app.use((error: Error, req: express.Request, res: express.Response, next: any) =
115154
next(error)
116155
})
117156

118-
expressWs.app.post('/execute', jsonParser, async (req: express.Request, res: express.Response) => {
157+
expressWs.app.post('/start', async (req: express.Request, res: express.Response) => {
119158
if (!doRequestValidation(req, res, ['environmentId'])) {
120159
return res.end()
121160
}
@@ -126,12 +165,29 @@ expressWs.app.post('/execute', jsonParser, async (req: express.Request, res: exp
126165
sessionParameters.platform = Platform.DOCKER
127166
sessionParameters.command = req.body.command || ''
128167

129-
const containerId = await env.execute(sessionParameters)
168+
const containerId = await env.start(sessionParameters)
130169
return res.json({
131170
containerId: containerId
132171
})
133172
})
134173

174+
expressWs.app.post('/execute', async (req: express.Request, res: express.Response) => {
175+
if (!doRequestValidation(req, res, ['environmentId', 'containerId', 'command'])) {
176+
return res.end()
177+
}
178+
179+
const env = new Environment(req.body.environmentId)
180+
181+
const sessionParameters = new SessionParameters()
182+
sessionParameters.platform = Platform.DOCKER
183+
sessionParameters.containerId = req.body.containerId
184+
sessionParameters.command = req.body.command
185+
186+
return res.json({
187+
output: await env.execute(sessionParameters, req.body.daemonize === true)
188+
})
189+
})
190+
135191
expressWs.app.post('/stop', async (req: express.Request, res: express.Response) => {
136192
// req: some JSON -> with container ID that will stop the container
137193
if (!doRequestValidation(req, res, ['environmentId', 'containerId'])) {

src/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
// Connect to server
3131
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
3232
const port = location.port ? `:${location.port}` : '';
33-
const socket = new WebSocket(`${protocol}${location.hostname}${port}/shell`);
33+
const socket = new WebSocket(`${protocol}${location.hostname}${port}/interact${window.location.search}`);
3434
socket.onopen = (ev) => { term.attach(socket); };
3535
</script>
3636
</body>

0 commit comments

Comments
 (0)