11import { spawn } from "node:child_process" ;
2- import { readFile } from "node:fs/promises" ;
3- import path from "node:path" ;
42import { assertCommandAllowed , workspacePath } from "./security.js" ;
53
64const dockerImages : Record < string , string > = {
@@ -31,8 +29,8 @@ export async function runInDocker(workspaceId: string, language: string, command
3129 assertCommandAllowed ( command ) ;
3230 const available = await dockerAvailable ( ) ;
3331 if ( ! available ) {
34- // Fallback: use Piston API for code execution when Docker is unavailable
35- return runViaPiston ( workspaceId , language , command ) ;
32+ // Fallback: execute directly with a sanitized environment
33+ return runDirectly ( workspaceId , language , command ) ;
3634 }
3735
3836 const cwd = workspacePath ( workspaceId ) ;
@@ -72,11 +70,63 @@ export async function runInDocker(workspaceId: string, language: string, command
7270 } ) ;
7371}
7472
73+ // ── Direct execution fallback (sanitized env, no secrets) ─────────
74+ function runDirectly ( workspaceId : string , _language : string , command : string ) : Promise < { ok : boolean ; output : string } > {
75+ assertCommandAllowed ( command ) ;
76+ const cwd = workspacePath ( workspaceId ) ;
77+
78+ // Clean environment: only PATH and language-specific vars, NO secrets
79+ const safeEnv : Record < string , string > = {
80+ PATH : "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ,
81+ HOME : "/tmp" ,
82+ LANG : "C.UTF-8" ,
83+ TERM : "dumb" ,
84+ JAVA_HOME : "/usr/lib/jvm/java-17-openjdk" ,
85+ } ;
86+
87+ return new Promise < { ok : boolean ; output : string } > ( ( resolve ) => {
88+ const child = spawn ( "sh" , [ "-c" , command ] , {
89+ cwd,
90+ env : safeEnv ,
91+ shell : false ,
92+ } ) ;
93+ let output = "" ;
94+ let outputSize = 0 ;
95+ const MAX_OUTPUT = 100_000 ; // 100KB max output
96+
97+ const timer = setTimeout ( ( ) => {
98+ child . kill ( "SIGKILL" ) ;
99+ output += "\n[TIMEOUT] Process killed after 15s" ;
100+ } , 15_000 ) ;
101+
102+ child . stdout . on ( "data" , ( chunk : Buffer ) => {
103+ if ( outputSize < MAX_OUTPUT ) {
104+ output += chunk . toString ( ) ;
105+ outputSize += chunk . length ;
106+ }
107+ } ) ;
108+ child . stderr . on ( "data" , ( chunk : Buffer ) => {
109+ if ( outputSize < MAX_OUTPUT ) {
110+ output += chunk . toString ( ) ;
111+ outputSize += chunk . length ;
112+ }
113+ } ) ;
114+ child . on ( "exit" , ( code ) => {
115+ clearTimeout ( timer ) ;
116+ resolve ( { ok : code === 0 , output : output . trim ( ) || "(No output)" } ) ;
117+ } ) ;
118+ child . on ( "error" , ( err ) => {
119+ clearTimeout ( timer ) ;
120+ resolve ( { ok : false , output : `Execution error: ${ err . message } ` } ) ;
121+ } ) ;
122+ } ) ;
123+ }
124+
75125export async function imageStatus ( ) {
76126 const uniqueImages = [ ...new Set ( Object . values ( dockerImages ) ) ] ;
77127 const available = await dockerAvailable ( ) ;
78128 if ( ! available ) {
79- return uniqueImages . map ( ( image ) => ( { image, present : false , reason : "Docker unavailable (using Piston API )" } ) ) ;
129+ return uniqueImages . map ( ( image ) => ( { image, present : false , reason : "Using direct execution (compilers installed )" } ) ) ;
80130 }
81131
82132 return Promise . all (
@@ -90,105 +140,3 @@ export async function imageStatus() {
90140 )
91141 ) ;
92142}
93-
94- // ── Piston API fallback for environments without Docker ───────────
95- const PISTON_URL = "https://emkc.org/api/v2/piston/execute" ;
96-
97- const pistonLangMap : Record < string , { language : string ; version : string } > = {
98- javascript : { language : "javascript" , version : "18.15.0" } ,
99- node : { language : "javascript" , version : "18.15.0" } ,
100- typescript : { language : "typescript" , version : "5.0.3" } ,
101- python : { language : "python" , version : "3.10.0" } ,
102- c : { language : "c" , version : "10.2.0" } ,
103- cpp : { language : "c++" , version : "10.2.0" } ,
104- "c++" : { language : "c++" , version : "10.2.0" } ,
105- java : { language : "java" , version : "15.0.2" } ,
106- rust : { language : "rust" , version : "1.68.2" } ,
107- html : { language : "javascript" , version : "18.15.0" } ,
108- css : { language : "javascript" , version : "18.15.0" } ,
109- } ;
110-
111- function extractFilename ( command : string ) : string | null {
112- // Match common patterns: python 'file.py', node 'file.js', gcc 'file.c', etc.
113- const patterns = [
114- / p y t h o n \s + ' ( [ ^ ' ] + ) ' / ,
115- / p y t h o n \s + ( \S + ) / ,
116- / n o d e \s + ' ( [ ^ ' ] + ) ' / ,
117- / n o d e \s + ( \S + ) / ,
118- / n p x \s + t s x \s + ' ( [ ^ ' ] + ) ' / ,
119- / n p x \s + t s x \s + ( \S + ) / ,
120- / g c c \s + ' ( [ ^ ' ] + ) ' / ,
121- / g c c \s + ( \S + ) / ,
122- / g \+ \+ \s + ' ( [ ^ ' ] + ) ' / ,
123- / g \+ \+ \s + ( \S + ) / ,
124- / j a v a c \s + ' ( [ ^ ' ] + ) ' / ,
125- / j a v a c \s + ( \S + ) / ,
126- / r u s t c \s + ' ( [ ^ ' ] + ) ' / ,
127- / r u s t c \s + ( \S + ) / ,
128- ] ;
129- for ( const pattern of patterns ) {
130- const match = command . match ( pattern ) ;
131- if ( match ) return match [ 1 ] ;
132- }
133- return null ;
134- }
135-
136- async function runViaPiston ( workspaceId : string , language : string , command : string ) : Promise < { ok : boolean ; output : string } > {
137- const mapping = pistonLangMap [ language ] ;
138- if ( ! mapping ) {
139- return { ok : false , output : `Language "${ language } " is not supported for online execution.` } ;
140- }
141-
142- // Extract filename from the command and read the source file
143- const filename = extractFilename ( command ) ;
144- if ( ! filename ) {
145- return { ok : false , output : `Could not determine source file from command: ${ command } ` } ;
146- }
147-
148- const cwd = workspacePath ( workspaceId ) ;
149- const filePath = path . join ( cwd , filename ) ;
150- let sourceCode : string ;
151- try {
152- sourceCode = await readFile ( filePath , "utf-8" ) ;
153- } catch {
154- return { ok : false , output : `File not found: ${ filename } ` } ;
155- }
156-
157- try {
158- const controller = new AbortController ( ) ;
159- const timeout = setTimeout ( ( ) => controller . abort ( ) , 30_000 ) ;
160-
161- const response = await fetch ( PISTON_URL , {
162- method : "POST" ,
163- headers : { "Content-Type" : "application/json" } ,
164- signal : controller . signal ,
165- body : JSON . stringify ( {
166- language : mapping . language ,
167- version : mapping . version ,
168- files : [ { name : filename , content : sourceCode } ] ,
169- } ) ,
170- } ) ;
171- clearTimeout ( timeout ) ;
172-
173- if ( ! response . ok ) {
174- return { ok : false , output : `Code execution service returned ${ response . status } . Please try again.` } ;
175- }
176-
177- const data = await response . json ( ) as { run ?: { stdout ?: string ; stderr ?: string ; code ?: number } ; message ?: string } ;
178- if ( data . message ) {
179- return { ok : false , output : data . message } ;
180- }
181- const run = data . run ;
182- if ( ! run ) {
183- return { ok : false , output : "Unexpected response from execution service." } ;
184- }
185-
186- const output = [ run . stdout , run . stderr ] . filter ( Boolean ) . join ( "\n" ) . trim ( ) || "(No output)" ;
187- return { ok : run . code === 0 , output } ;
188- } catch ( err : any ) {
189- if ( err . name === "AbortError" ) {
190- return { ok : false , output : "[TIMEOUT] Execution timed out after 30s" } ;
191- }
192- return { ok : false , output : `Execution service error: ${ err . message } ` } ;
193- }
194- }
0 commit comments