diff --git a/server/app/Services/ChannelProjectService.php b/server/app/Services/ChannelProjectService.php index d829b43..b5e1e87 100644 --- a/server/app/Services/ChannelProjectService.php +++ b/server/app/Services/ChannelProjectService.php @@ -12,6 +12,9 @@ class ChannelProjectService /** * Run tools/new_project.py to materialize public/web/{channelId}/. * Domains come from config (系统设置 / env); script rebuilds sync only when domains changed. + * + * Panel hosts often set open_basedir to server/ only — do not probe lab-root + * paths with is_file(); the Python child process is not bound by open_basedir. */ public function generate(string $channelId): void { @@ -26,14 +29,12 @@ class ChannelProjectService } $labRoot = dirname(base_path()); // coruna-lab/ - $script = $labRoot.'/tools/new_project.py'; - if (! is_file($script)) { - throw new RuntimeException('new_project.py not found'); - } + $script = $this->resolveNewProjectScript($labRoot); + $python = $this->resolvePythonBin(); $joined = implode(',', $domains); $cmd = [ - 'python3', + $python, $script, '--channel-id', $channelId, @@ -43,7 +44,8 @@ class ChannelProjectService $joined, ]; - $process = new Process($cmd, $labRoot); + // cwd must stay inside open_basedir (server/). Script resolves LAB_ROOT from __file__. + $process = new Process($cmd, base_path()); $process->setTimeout(600); $process->run(); @@ -71,4 +73,41 @@ class ChannelProjectService File::deleteDirectory($dir); } } + + private function resolveNewProjectScript(string $labRoot): string + { + $configured = trim((string) config('coruna.new_project_py', '')); + if ($configured !== '') { + return $configured; + } + + return $labRoot.'/tools/new_project.py'; + } + + /** + * Prefer an absolute python path so the child process gets a non-empty sys.executable. + */ + private function resolvePythonBin(): string + { + $configured = trim((string) config('coruna.python_bin', 'python3')); + if ($configured === '') { + $configured = 'python3'; + } + if (str_contains($configured, '/')) { + return $configured; + } + + $which = trim((string) shell_exec('command -v '.escapeshellarg($configured).' 2>/dev/null')); + if ($which !== '' && ! str_contains($which, "\n")) { + return $which; + } + + foreach (['/usr/bin/python3', '/usr/local/bin/python3'] as $fallback) { + if (@is_file($fallback)) { + return $fallback; + } + } + + return $configured; + } } diff --git a/server/config/coruna.php b/server/config/coruna.php index a15add0..bbab6d2 100644 --- a/server/config/coruna.php +++ b/server/config/coruna.php @@ -15,6 +15,9 @@ return [ // Absolute path, project-local bin/7z, or bare "7z" on PATH. Avoid probing // system paths with is_executable() under open_basedir (see CorunaArchive). 'seven_zip' => env('CORUNA_7Z_BIN', ''), + // Channel pack builder (outside server/ — do not is_file() under tight open_basedir). + 'new_project_py' => env('CORUNA_NEW_PROJECT_PY', ''), + 'python_bin' => env('CORUNA_PYTHON_BIN', 'python3'), 'telegram' => [ 'bot_token' => env('TELEGRAM_BOT_TOKEN'), 'owner_chat_id' => env('TELEGRAM_OWNER_CHAT_ID'), diff --git a/tools/new_project.py b/tools/new_project.py index d53935f..33b9970 100644 --- a/tools/new_project.py +++ b/tools/new_project.py @@ -87,6 +87,20 @@ def pick_channel(explicit: str | None, web_root: Path) -> str: return channel +def resolve_python() -> str: + """PHP/panel often launches `python3` such that sys.executable is ''.""" + exe = (sys.executable or "").strip() + if exe: + return exe + for name in ("python3", "python"): + found = shutil.which(name) + if found: + return found + raise SystemExit( + "cannot locate python interpreter (sys.executable is empty; install python3 on PATH)" + ) + + def run(cmd: list[str]) -> None: print("+", " ".join(cmd), flush=True) subprocess.run(cmd, cwd=str(LAB_ROOT), check=True) @@ -230,7 +244,7 @@ def main() -> int: for item in fixed_rep: domain_args += ["--reporting-domains", item] - py = sys.executable + py = resolve_python() if same_domains: dep = args.deployment_seed or prev_seeds.get("deployment_seed")