114 lines
3.5 KiB
PHP
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;
|
|
}
|
|
}
|