Files
coruna-lab/server/app/Services/ChannelProjectService.php
T
2026-08-07 06:05:33 +08:00

114 lines
3.5 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use RuntimeException;
use Symfony\Component\Process\Process;
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
{
$channelId = strtolower(trim($channelId));
if (! preg_match('/^[a-z0-9]{32}$/', $channelId)) {
throw new RuntimeException('Invalid channel id');
}
$domains = array_values(array_filter(config('coruna.channel_domains', [])));
if ($domains === []) {
throw new RuntimeException('投放域名未配置(系统设置 → 投放域名)');
}
$labRoot = dirname(base_path()); // coruna-lab/
$script = $this->resolveNewProjectScript($labRoot);
$python = $this->resolvePythonBin();
$joined = implode(',', $domains);
$cmd = [
$python,
$script,
'--channel-id',
$channelId,
'--deployment-domains',
$joined,
'--reporting-domains',
$joined,
];
// cwd must stay inside open_basedir (server/). Script resolves LAB_ROOT from __file__.
$process = new Process($cmd, base_path());
$process->setTimeout(600);
$process->run();
if (! $process->isSuccessful()) {
Log::error('new_project failed', [
'channel_id' => $channelId,
'out' => $process->getOutput(),
'err' => $process->getErrorOutput(),
]);
$this->deleteWebTree($channelId);
throw new RuntimeException(
'生成渠道资源失败: '.trim($process->getErrorOutput() ?: $process->getOutput() ?: 'unknown')
);
}
}
public function deleteWebTree(string $channelId): void
{
$channelId = strtolower(trim($channelId));
if (! preg_match('/^[a-z0-9]{32}$/', $channelId)) {
return;
}
$dir = public_path('web/'.$channelId);
if (is_dir($dir)) {
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;
}
}