From dca5b2c9c4c814c7933338dfa33522b20cd6e520 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 2 May 2026 20:49:45 +0900 Subject: [PATCH] Wrap Windows Python calls through cmd --- src/python-runtime.ts | 54 ++++++++++++++++++++++++++---- src/services/python-json-worker.ts | 7 ++-- src/setup-python.ts | 8 +++-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/python-runtime.ts b/src/python-runtime.ts index 77b7d94..0659e1a 100644 --- a/src/python-runtime.ts +++ b/src/python-runtime.ts @@ -6,14 +6,52 @@ import process from "node:process"; import type { AppConfig } from "./config.js"; +export interface PythonCommandSpec { + command: string; + args: string[]; + viaCmdShell?: boolean; +} + function splitCommand(command: string): string[] { const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? []; return parts.map((part) => part.replace(/^"(.*)"$/, "$1")); } -async function canRun(command: string, args: string[]): Promise { +function quoteWindowsCmdArg(value: string): string { + return `"${value.replace(/"/g, '""')}"`; +} + +function buildWindowsCommandLine(parts: string[]): string { + return parts.map((part) => quoteWindowsCmdArg(part)).join(" "); +} + +export function buildPythonInvocation(spec: PythonCommandSpec, extraArgs: string[]): PythonCommandSpec { + if (process.platform === "win32" && spec.viaCmdShell) { + return { + command: "cmd.exe", + args: ["/d", "/s", "/c", buildWindowsCommandLine([spec.command, ...spec.args, ...extraArgs])], + }; + } + + return { + command: spec.command, + args: [...spec.args, ...extraArgs], + }; +} + +async function canRun(command: string, args: string[], viaCmdShell = false): Promise { + const invocation = viaCmdShell + ? { + command: "cmd.exe", + args: ["/d", "/s", "/c", buildWindowsCommandLine([command, ...args, "--version"])], + } + : { + command, + args: [...args, "--version"], + }; + return await new Promise((resolve) => { - const child = spawn(command, [...args, "--version"], { + const child = spawn(invocation.command, invocation.args, { stdio: ["ignore", "ignore", "ignore"], windowsHide: true, }); @@ -90,14 +128,18 @@ export async function resolvePythonCommand(config: AppConfig): Promise<{ command return await resolveWorkerPythonCommand(config); } -export async function resolveBasePythonCommand(config: AppConfig): Promise<{ command: string; args: string[] }> { +export async function resolveBasePythonCommand(config: AppConfig): Promise { const configured = config.LOCAL_AI_PYTHON?.trim(); if (configured) { const [command, ...args] = splitCommand(configured); if (!command) { throw new Error("LOCAL_AI_PYTHON 값이 비어 있습니다."); } - return { command, args }; + return { + command, + args, + viaCmdShell: process.platform === "win32" && !path.isAbsolute(command), + }; } const venvPath = resolveVenvPythonPath(config); @@ -118,8 +160,8 @@ export async function resolveBasePythonCommand(config: AppConfig): Promise<{ com return { command: absolute, args: candidate.args }; } - if (await canRun(candidate.command, candidate.args)) { - return candidate; + if (await canRun(candidate.command, candidate.args, true)) { + return { ...candidate, viaCmdShell: true }; } } diff --git a/src/services/python-json-worker.ts b/src/services/python-json-worker.ts index c783acf..421b80b 100644 --- a/src/services/python-json-worker.ts +++ b/src/services/python-json-worker.ts @@ -4,7 +4,7 @@ import { createInterface } from "node:readline"; import type { AppConfig } from "../config.js"; import type { Logger } from "../logger.js"; -import { resolveWorkerPythonCommand, resolveWorkerScript } from "../python-runtime.js"; +import { buildPythonInvocation, resolveWorkerPythonCommand, resolveWorkerScript } from "../python-runtime.js"; interface RpcSuccess { id: string; @@ -49,10 +49,11 @@ export class PythonJsonWorker { throw new Error(`${this.logPrefix} worker is shutting down`); } - const { command, args } = await resolveWorkerPythonCommand(this.config); + const python = await resolveWorkerPythonCommand(this.config); const scriptPath = resolveWorkerScript(this.scriptName); + const invocation = buildPythonInvocation(python, [scriptPath]); - this.processRef = spawn(command, [...args, scriptPath], { + this.processRef = spawn(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { diff --git a/src/setup-python.ts b/src/setup-python.ts index 3b83075..b48eecd 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -4,13 +4,14 @@ import path from "node:path"; import { spawn } from "node:child_process"; import { loadConfig } from "./config.js"; -import { resolveBasePythonCommand, resolveVenvPythonPath } from "./python-runtime.js"; +import { buildPythonInvocation, resolveBasePythonCommand, resolveVenvPythonPath } from "./python-runtime.js"; async function run(command: string, args: string[], cwd: string): Promise { await new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, stdio: "inherit", + windowsHide: true, }); child.on("exit", (code) => { if (code === 0) { @@ -25,14 +26,15 @@ async function run(command: string, args: string[], cwd: string): Promise async function main(): Promise { const config = loadConfig(); - const { command, args } = await resolveBasePythonCommand(config); + const python = await resolveBasePythonCommand(config); const venvRoot = path.resolve(process.cwd(), config.LOCAL_AI_VENV_PATH); const requirementsPath = path.resolve(process.cwd(), "python", "requirements.txt"); await mkdir(path.dirname(venvRoot), { recursive: true }); console.log(`가상환경 생성: ${venvRoot}`); - await run(command, [...args, "-m", "venv", venvRoot], process.cwd()); + const createVenv = buildPythonInvocation(python, ["-m", "venv", venvRoot]); + await run(createVenv.command, createVenv.args, process.cwd()); const venvPython = resolveVenvPythonPath(config); await run(venvPython, ["-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], process.cwd());