Execute the wrapped script via the host's `python3` interpreter. Used only when DEPLOYMENT_MODE != cloud. No sandboxing, no isolation: this is intended for the developer running their own code on their own machine.
(code: &str)
| 378 | /// Used only when DEPLOYMENT_MODE != cloud. No sandboxing, no isolation: this |
| 379 | /// is intended for the developer running their own code on their own machine. |
| 380 | async fn run_local_python(code: &str) -> Result<ExecOutcome, String> { |
| 381 | let output = Command::new("python3") |
| 382 | .arg("-c") |
| 383 | .arg(code) |
| 384 | .output() |
| 385 | .await |
| 386 | .map_err(|e| format!("Failed to spawn local python3: {}", e))?; |
| 387 | |
| 388 | let stdout_full = String::from_utf8_lossy(&output.stdout).into_owned(); |
| 389 | let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); |
| 390 | |
| 391 | if !output.status.success() { |
| 392 | let msg = if stderr.is_empty() { |
| 393 | format!("python3 exited with status {}", output.status) |
| 394 | } else { |
| 395 | stderr.trim_end().to_string() |
| 396 | }; |
| 397 | return Ok(ExecOutcome::UserError(msg)); |
| 398 | } |
| 399 | |
| 400 | // Wrapper always prefixes the marker with "\n" and json.dumps produces a |
| 401 | // single line, so we can anchor on "\nWEFT_RESULT_JSON=" regardless of |
| 402 | // whether the user's last print had a trailing newline. Using the LAST |
| 403 | // marker occurrence means a user who prints the literal "WEFT_RESULT_JSON=" |
| 404 | // themselves cannot hijack us. Content AFTER the marker's terminating "\n" |
| 405 | // (e.g. atexit / CPython shutdown prints) is preserved in the stdout port. |
| 406 | const MARKER_ANCHOR: &str = "\nWEFT_RESULT_JSON="; |
| 407 | let pos = stdout_full.rfind(MARKER_ANCHOR).ok_or_else(|| { |
| 408 | "Local python3 finished without emitting a result marker. The wrapper may have been altered.".to_string() |
| 409 | })?; |
| 410 | let after = &stdout_full[pos + MARKER_ANCHOR.len()..]; |
| 411 | let (json_str, tail_after_marker) = match after.find('\n') { |
| 412 | Some(nl) => (&after[..nl], &after[nl + 1..]), |
| 413 | None => (after, ""), |
| 414 | }; |
| 415 | let parsed: Value = serde_json::from_str(json_str) |
| 416 | .map_err(|e| format!("Could not parse local result JSON: {} (raw: {})", e, json_str))?; |
| 417 | let ports = parsed.as_object() |
| 418 | .ok_or_else(|| format!("Local result was not a JSON object: {}", parsed))? |
| 419 | .clone(); |
| 420 | |
| 421 | let mut user_stdout = stdout_full[..pos].to_string(); |
| 422 | if !tail_after_marker.is_empty() { |
| 423 | if !user_stdout.is_empty() && !user_stdout.ends_with('\n') { |
| 424 | user_stdout.push('\n'); |
| 425 | } |
| 426 | user_stdout.push_str(tail_after_marker); |
| 427 | } |
| 428 | |
| 429 | Ok(ExecOutcome::Ok { ports, stdout: user_stdout, stderr }) |
| 430 | } |
| 431 | |
| 432 | /// Extracted sandbox handle. Owned by the caller; the caller is responsible |
| 433 | /// for calling `delete_sandbox` once it is done. |