1
1
/*
2
2
Project run server.
3
3
4
- It may be necessary to do this to enable the user running this
5
- code to use nsjail:
6
-
7
- sudo sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=0 && sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
8
-
9
-
10
- See https://github.com/google/nsjail/issues/236#issuecomment-2267096267
11
-
12
- To make permanent:
13
-
14
- echo -e "kernel.apparmor_restrict_unprivileged_unconfined=0\nkernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/99-custom.conf && sudo sysctl --system
15
-
16
- ---
17
-
18
4
DEV -- see packages/server/conat/project/run.ts
19
5
20
6
*/
21
7
22
8
import { type Client as ConatClient } from "@cocalc/conat/core/client" ;
23
9
import { conat } from "@cocalc/backend/conat" ;
24
10
import { server as projectRunnerServer } from "@cocalc/conat/project/runner/run" ;
25
- import { isValidUUID } from "@cocalc/util/misc" ;
26
11
import { reuseInFlight } from "@cocalc/util/reuse-in-flight" ;
27
- import getLogger from "@cocalc/backend/logger" ;
28
- import { root } from "@cocalc/backend/data" ;
29
- import { basename , dirname , join } from "node:path" ;
30
- import { userInfo } from "node:os" ;
31
- import { ensureConfFilesExists , setupDataPath , writeSecretToken } from "./util" ;
32
- import { getEnvironment } from "./env" ;
33
- import { mkdir } from "fs/promises" ;
34
- import { exists } from "@cocalc/backend/misc/async-utils-node" ;
35
- import { spawn } from "node:child_process" ;
36
12
import { type Configuration } from "./types" ;
37
13
export { type Configuration } ;
38
- import { limits } from "./limits" ;
39
- import {
40
- client as createFileClient ,
41
- type Fileserver ,
42
- } from "@cocalc/conat/files/file-server" ;
43
- import { nsjail } from "@cocalc/backend/sandbox/install" ;
44
- import { once } from "@cocalc/util/async-utils" ;
45
-
46
- // for development it may be useful to just disabling using nsjail namespaces
47
- // entirely -- change this to true to do so.
48
- const DISABLE_NSJAIL = false ;
49
-
50
- const DEFAULT_UID = 2001 ;
51
-
52
- // how long from SIGTERM until SIGKILL
53
- const GRACE_PERIOD = 3000 ;
54
-
55
- const logger = getLogger ( "project-runner" ) ;
56
-
57
- const children : { [ project_id : string ] : any } = { } ;
58
-
59
- const MOUNTS = {
60
- "-R" : [ "/etc" , "/var" , "/bin" , "/lib" , "/usr" , "/lib64" , "/run" ] ,
61
- "-B" : [ "/dev" ] ,
62
- } ;
63
-
64
- let nodePath = process . execPath ;
65
- async function initMounts ( ) {
66
- for ( const type in MOUNTS ) {
67
- const v : string [ ] = [ ] ;
68
- for ( const path of MOUNTS [ type ] ) {
69
- if ( await exists ( path ) ) {
70
- v . push ( path ) ;
71
- }
72
- }
73
- MOUNTS [ type ] = v ;
74
- }
75
- MOUNTS [ "-R" ] . push ( `${ dirname ( root ) } :/cocalc` ) ;
76
-
77
- // also if this node is install via nvm, we make exactly this
78
- // version of node's install available
79
- if ( ! process . execPath . startsWith ( "/usr/" ) ) {
80
- // not already in an obvious system-wide place we included above
81
- // IMPORTANT: take care not to put the binary next to sensitive info!
82
- MOUNTS [ "-R" ] . push ( `${ dirname ( process . execPath ) } :/cocalc/bin` ) ;
83
- nodePath = join ( "/cocalc/bin" , basename ( process . execPath ) ) ;
84
- }
85
- }
86
-
87
- let fsclient : Fileserver | null = null ;
88
- function getFsClient ( ) {
89
- if ( client == null ) {
90
- throw Error ( "not initialized" ) ;
91
- }
92
- fsclient ??= createFileClient ( { client } ) ;
93
- return fsclient ;
94
- }
95
-
96
- async function setQuota ( project_id : string , size : number | string ) {
97
- const c = getFsClient ( ) ;
98
- await c . setQuota ( { project_id, size } ) ;
99
- }
100
-
101
- async function mountHome ( project_id : string ) : Promise < string > {
102
- const c = getFsClient ( ) ;
103
- const { path } = await c . mount ( { project_id } ) ;
104
- return path ;
105
- }
106
-
107
- async function start ( {
108
- project_id,
109
- config,
110
- } : {
111
- project_id : string ;
112
- config ?: Configuration ;
113
- } ) {
114
- if ( ! isValidUUID ( project_id ) ) {
115
- throw Error ( "start: project_id must be valid" ) ;
116
- }
117
- logger . debug ( "start" , { project_id, config : { ...config , secret : "xxx" } } ) ;
118
- if ( children [ project_id ] != null && children [ project_id ] . exitCode == null ) {
119
- logger . debug ( "start -- already running" ) ;
120
- return ;
121
- }
122
- let uid , gid ;
123
- if ( userInfo ( ) . uid ) {
124
- // server running as non-root user -- single user mode
125
- uid = gid = userInfo ( ) . uid ;
126
- } else {
127
- // server is running as root -- multiuser mode
128
- uid = gid = DEFAULT_UID ;
129
- }
130
-
131
- const home = await mountHome ( project_id ) ;
132
- await mkdir ( home , { recursive : true } ) ;
133
- await ensureConfFilesExists ( home ) ;
134
- const env = getEnvironment ( {
135
- project_id,
136
- env : config ?. env ,
137
- HOME : home ,
138
- } ) ;
139
- await setupDataPath ( home ) ;
140
- if ( config ?. secret ) {
141
- await writeSecretToken ( home , config . secret ) ;
142
- }
143
-
144
- if ( config ?. disk ) {
145
- // TODO: maybe this should be done in parallel with other things
146
- // to make startup time slightly faster (?) -- could also be incorporated
147
- // into mount.
148
- await setQuota ( project_id , config . disk ) ;
149
- }
150
-
151
- let script : string ,
152
- cmd : string ,
153
- args : string [ ] = [ ] ;
154
- if ( DISABLE_NSJAIL ) {
155
- // DANGEROUS: no safety at all here!
156
- // This may be useful in some environments, especially for debugging.
157
- cmd = process . execPath ;
158
- script = join ( root , "packages/project/bin/cocalc-project.js" ) ;
159
- } else {
160
- script = "/cocalc/src/packages/project/bin/cocalc-project.js" ;
161
- args . push (
162
- "-q" , // not too verbose
163
- "-Mo" , // run a command once
164
- "--disable_clone_newnet" , // [ ] TODO: for now we have the full host network
165
- "--keep_env" , // this just keeps env
166
- "--keep_caps" , // [ ] TODO: maybe NOT needed!
167
- "--skip_setsid" , // evidently needed for terminal signals (e.g., ctrl+z); dangerous. [ ] TODO -- really needed?
168
- ) ;
169
-
170
- args . push ( "--hostname" , `project-${ env . COCALC_PROJECT_ID } ` ) ;
171
-
172
- if ( uid != null && gid != null ) {
173
- args . push ( "-u" , `${ uid } ` , "-g" , `${ gid } ` ) ;
174
- }
175
-
176
- for ( const type in MOUNTS ) {
177
- for ( const path of MOUNTS [ type ] ) {
178
- args . push ( type , path ) ;
179
- }
180
- }
181
- // need a /tmp directory
182
- args . push ( "-m" , "none:/tmp:tmpfs:size=500000000" ) ;
183
-
184
- args . push ( "-B" , `${ home } :${ env . HOME } ` ) ;
185
- args . push ( ...limits ( config ) ) ;
186
- args . push ( "--" ) ;
187
- args . push ( nodePath ) ;
188
- cmd = nsjail ;
189
- }
190
-
191
- args . push ( script , "--init" , "project_init.sh" ) ;
192
-
193
- //logEnv(env);
194
- // console.log(`${cmd} ${args.join(" ")}`);
195
- logger . debug ( `${ cmd } ${ args . join ( " " ) } ` ) ;
196
- const child = spawn ( cmd , args , {
197
- env,
198
- uid,
199
- gid : uid ,
200
- } ) ;
201
- children [ project_id ] = child ;
202
-
203
- child . stdout . on ( "data" , ( chunk : Buffer ) => {
204
- logger . debug ( `project_id=${ project_id } .stdout: ` , chunk . toString ( ) ) ;
205
- } ) ;
206
- child . stderr . on ( "data" , ( chunk : Buffer ) => {
207
- logger . debug ( `project_id=${ project_id } .stderr: ` , chunk . toString ( ) ) ;
208
- } ) ;
209
- }
14
+ import { init as initFilesystem } from "./filesystem" ;
15
+ import getLogger from "@cocalc/backend/logger" ;
16
+ import * as nsjail from "./nsjail" ;
17
+ import { init as initMounts } from "./mounts" ;
210
18
211
- async function stop ( { project_id } ) {
212
- if ( ! isValidUUID ( project_id ) ) {
213
- throw Error ( "stop: project_id must be valid" ) ;
214
- }
215
- logger . debug ( "stop" , { project_id } ) ;
216
- const child = children [ project_id ] ;
217
- if ( child != null && child . exitCode == null ) {
218
- const exit = once ( child , "exit" , GRACE_PERIOD ) ;
219
- child . kill ( "SIGTERM" ) ;
220
- try {
221
- await exit ;
222
- } catch {
223
- const exit2 = once ( child , "exit" ) ;
224
- child . kill ( "SIGKILL" ) ;
225
- await exit2 ;
226
- }
227
- delete children [ project_id ] ;
228
- }
229
- }
230
-
231
- async function status ( { project_id } ) {
232
- if ( ! isValidUUID ( project_id ) ) {
233
- throw Error ( "status: project_id must be valid" ) ;
234
- }
235
- logger . debug ( "status" , { project_id } ) ;
236
- let state ;
237
- if ( children [ project_id ] == null || children [ project_id ] . exitCode ) {
238
- state = "opened" ;
239
- } else {
240
- state = "running" ;
241
- }
242
- // [ ] TODO: ip -- need to figure out the networking story for running projects
243
- // The following will only work on a single machine with global network address space
244
- return { state, ip : "127.0.0.1" } ;
245
- }
19
+ const logger = getLogger ( "project-runner:run" ) ;
246
20
247
21
let client : ConatClient | null = null ;
248
- export async function init ( opts : { client ?: ConatClient } = { } ) {
22
+ export async function init (
23
+ opts : { client ?: ConatClient ; runtime ?: "nsjail" | "podman" } = { } ,
24
+ ) {
25
+ logger . debug ( "init" , opts . runtime ) ;
26
+ let runtime ;
27
+ switch ( opts . runtime ) {
28
+ case "nsjail" :
29
+ runtime = nsjail ;
30
+ break ;
31
+ default :
32
+ throw Error ( `runtime '${ opts . runtime } ' not implemented` ) ;
33
+ }
249
34
client = opts . client ?? conat ( ) ;
35
+ initFilesystem ( { client } ) ;
250
36
await initMounts ( ) ;
37
+
38
+ const { start, stop, status } = runtime ;
251
39
return await projectRunnerServer ( {
252
40
client,
253
41
start : reuseInFlight ( start ) ,
@@ -256,27 +44,6 @@ export async function init(opts: { client?: ConatClient } = {}) {
256
44
} ) ;
257
45
}
258
46
259
- export function killAllProjects ( ) {
260
- for ( const project_id in children ) {
261
- logger . debug ( `killing project_id=${ project_id } ` ) ;
262
- children [ project_id ] ?. kill ( "SIGKILL" ) ;
263
- delete children [ project_id ] ;
264
- }
47
+ export function close ( ) {
48
+ nsjail . close ( ) ;
265
49
}
266
-
267
- // important to killAllProjects, because it kills all
268
- // the processes that were spawned
269
- process . once ( "exit" , killAllProjects ) ;
270
- [ "SIGINT" , "SIGTERM" , "SIGQUIT" ] . forEach ( ( sig ) => {
271
- process . once ( sig , ( ) => {
272
- process . exit ( ) ;
273
- } ) ;
274
- } ) ;
275
-
276
- // function logEnv(env) {
277
- // let s = "export ";
278
- // for (const key in env) {
279
- // s += `${key}="${env[key]}" `;
280
- // }
281
- // console.log(s);
282
- // }
0 commit comments