11import { spawn } from "node:child_process" ;
2+ import { readFile } from "node:fs/promises" ;
3+ import path from "node:path" ;
24import { assertCommandAllowed , workspacePath } from "./security.js" ;
35
46const dockerImages : Record < string , string > = {
@@ -29,10 +31,8 @@ export async function runInDocker(workspaceId: string, language: string, command
2931 assertCommandAllowed ( command ) ;
3032 const available = await dockerAvailable ( ) ;
3133 if ( ! available ) {
32- return {
33- ok : false ,
34- output : "Docker is not installed or not on PATH. Install Docker Desktop to enable isolated sandboxes."
35- } ;
34+ // Fallback: use Piston API for code execution when Docker is unavailable
35+ return runViaPiston ( workspaceId , language , command ) ;
3636 }
3737
3838 const cwd = workspacePath ( workspaceId ) ;
@@ -76,7 +76,7 @@ export async function imageStatus() {
7676 const uniqueImages = [ ...new Set ( Object . values ( dockerImages ) ) ] ;
7777 const available = await dockerAvailable ( ) ;
7878 if ( ! available ) {
79- return uniqueImages . map ( ( image ) => ( { image, present : false , reason : "Docker unavailable" } ) ) ;
79+ return uniqueImages . map ( ( image ) => ( { image, present : false , reason : "Docker unavailable (using Piston API) " } ) ) ;
8080 }
8181
8282 return Promise . all (
@@ -90,3 +90,105 @@ export async function imageStatus() {
9090 )
9191 ) ;
9292}
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