mirror of
https://gitee.com/wonderful-code/buildadmin
synced 2024-11-22 15:24:09 +00:00
371 lines
12 KiB
PHP
371 lines
12 KiB
PHP
<?php
|
||
|
||
namespace ba;
|
||
|
||
use think\Response;
|
||
use think\facade\Config;
|
||
use think\facade\Cookie;
|
||
use app\admin\library\Auth;
|
||
use think\exception\HttpResponseException;
|
||
|
||
/**
|
||
* 命令执行类
|
||
*/
|
||
class CommandExec
|
||
{
|
||
/**
|
||
* @var object 对象实例
|
||
*/
|
||
protected static $instance;
|
||
|
||
/**
|
||
* 配置文件名
|
||
*/
|
||
static $buildConfigFileName = 'buildadmin.php';
|
||
|
||
/**
|
||
* 结果输出扩展
|
||
* 每次命令执行有输出时,同时携带扩展数据
|
||
*/
|
||
protected $outputExtend = null;
|
||
|
||
/**
|
||
* 状态标识
|
||
*/
|
||
protected $flag = [
|
||
// 连接成功
|
||
'link-success' => 'command-link-success',
|
||
// 执行成功
|
||
'exec-success' => 'command-exec-success',
|
||
// 执行完成 - 执行完成但未成功则为失败
|
||
'exec-completed' => 'command-exec-completed',
|
||
// 执行出错 - 不区分命令
|
||
'exec-error' => 'command-exec-error',
|
||
];
|
||
|
||
/**
|
||
* 当前执行的命令,$command 的 key
|
||
* @var string
|
||
*/
|
||
protected $currentCommandKey = '';
|
||
|
||
/**
|
||
* 对可以执行的命令进行限制
|
||
* @var string[]
|
||
*/
|
||
protected $command = [];
|
||
|
||
/**
|
||
* 自动构建的前端文件的 outDir(相对于根目录)
|
||
*/
|
||
protected $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist';
|
||
|
||
/**
|
||
* 构造函数
|
||
* @param bool $authentication 是否鉴权
|
||
*/
|
||
public function __construct($authentication)
|
||
{
|
||
set_time_limit(200);
|
||
if ($authentication) {
|
||
$token = request()->server('HTTP_BATOKEN', request()->request('batoken', Cookie::get('batoken') ?: false));
|
||
$auth = Auth::instance();
|
||
$auth->init($token);
|
||
if (!$auth->isLogin()) {
|
||
$this->execError('Error: Please login first', true);
|
||
}
|
||
}
|
||
$this->command = Config::get('buildadmin.allowed_commands');
|
||
}
|
||
|
||
/**
|
||
* 初始化
|
||
*/
|
||
public static function instance($authentication = true)
|
||
{
|
||
if (is_null(self::$instance)) {
|
||
self::$instance = new static($authentication);
|
||
}
|
||
|
||
return self::$instance;
|
||
}
|
||
|
||
/**
|
||
* 获取命令
|
||
* @param string $key 命令key
|
||
* @param bool $outputError 是否直接输出错误(通过event-stream)
|
||
* @return string
|
||
*/
|
||
protected function getCommand(string $key, bool $outputError = true): string
|
||
{
|
||
if (!$key) {
|
||
if ($outputError) $this->execError('Error: Command not allowed', true);
|
||
$this->break();
|
||
}
|
||
|
||
$this->currentCommandKey = $key;
|
||
|
||
if (stripos($key, '.')) {
|
||
$key = explode('.', $key);
|
||
if (!array_key_exists($key[0], $this->command) || !is_array($this->command[$key[0]]) || !array_key_exists($key[1], $this->command[$key[0]])) {
|
||
if ($outputError) $this->execError('Error: Command not allowed', true);
|
||
$this->break();
|
||
}
|
||
return $this->command[$key[0]][$key[1]];
|
||
} else {
|
||
if (!array_key_exists($key, $this->command)) {
|
||
if ($outputError) $this->execError('Error: Command not allowed', true);
|
||
$this->break();
|
||
}
|
||
return $this->command[$key];
|
||
}
|
||
}
|
||
|
||
public function execError($error, $break = false)
|
||
{
|
||
$this->output($error);
|
||
$this->outputFlag('exec-error');
|
||
if ($break) $this->break();
|
||
}
|
||
|
||
/**
|
||
* 终端
|
||
*/
|
||
public function terminal()
|
||
{
|
||
header('Content-Type: text/event-stream');
|
||
header('Cache-Control: no-cache');
|
||
|
||
ob_end_flush();
|
||
ob_implicit_flush(1);// 开启绝对刷新
|
||
|
||
$this->outputExtend = request()->param('extend');
|
||
$command = $this->getCommand(request()->param('command'));
|
||
|
||
$this->beforeExecution();
|
||
$this->outputFlag('link-success');
|
||
$this->output('> ' . $command, false);
|
||
if (ob_get_level() == 0) ob_start();
|
||
$handle = popen($command . ' 2>&1', 'r');
|
||
while (!feof($handle)) {
|
||
$this->output(fgets($handle));
|
||
@ob_flush();// 刷新浏览器缓冲区
|
||
}
|
||
pclose($handle);
|
||
$this->outputFlag('exec-completed');
|
||
}
|
||
|
||
/**
|
||
* 命令执行前进行一些操作
|
||
*/
|
||
private function beforeExecution()
|
||
{
|
||
if ($this->currentCommandKey == 'test-install.pnpm') {
|
||
$pnpmLockFile = root_path() . 'public' . DIRECTORY_SEPARATOR . 'npm-install-test' . DIRECTORY_SEPARATOR . 'pnpm-lock.yaml';
|
||
@unlink($pnpmLockFile);
|
||
} elseif ($this->currentCommandKey == 'web-install.pnpm') {
|
||
$pnpmLockFile = root_path() . 'web' . DIRECTORY_SEPARATOR . 'pnpm-lock.yaml';
|
||
@unlink($pnpmLockFile);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 输出 EventSource 数据
|
||
* @param string $data
|
||
* @param bool $callback
|
||
*/
|
||
public function output(string $data, bool $callback = true)
|
||
{
|
||
$data = $this->filterMark($data);
|
||
$data = str_replace(["\r\n", "\r", "\n"], "", $data);
|
||
$data = mb_convert_encoding($data, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
|
||
$data = [
|
||
'data' => $data,
|
||
'extend' => $this->outputExtend,
|
||
'key' => $this->currentCommandKey,
|
||
];
|
||
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
if ($data) {
|
||
echo 'data: ' . $data . "\n\n";
|
||
if ($callback) $this->outputCallback($data);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 输出状态标记
|
||
* @param string $flag
|
||
* @param string $command
|
||
* @param bool $callback
|
||
*/
|
||
public function outputFlag(string $flag)
|
||
{
|
||
$this->output($this->flag[$flag], false);
|
||
}
|
||
|
||
public function filterMark($str)
|
||
{
|
||
$preg = '/\[(.*?)m/i';
|
||
$str = preg_replace($preg, '', $str);
|
||
return $str;
|
||
}
|
||
|
||
public function filterASCII($str)
|
||
{
|
||
if (!$str) return '';
|
||
$newStr = '';
|
||
for ($i = 0; isset($str[$i]); $i++) {
|
||
$ascCode = ord($str[$i]);
|
||
if ($ascCode > 31 && $ascCode != 127) {
|
||
$newStr .= $str[$i];
|
||
}
|
||
}
|
||
return $newStr;
|
||
}
|
||
|
||
/**
|
||
* npm Install 时检测是否执行成功
|
||
* @param string $output
|
||
*/
|
||
public function npmInstallCallback(string $output, $name)
|
||
{
|
||
if ($name == 'npm') {
|
||
$preg[] = "[added|removed|changed|audited] ([0-9]*) package";
|
||
return preg_match('/' . implode('|', $preg) . '/i', $output);
|
||
} elseif ($name == 'cnpm') {
|
||
return strpos(strtolower($output), 'all packages installed') !== false;
|
||
} elseif ($name == 'pnpm') {
|
||
$preg = "/Progress: resolved ([0-9]*), reused ([0-9]*), downloaded ([0-9]*), added ([0-9]*), done/i";
|
||
$preg2 = "/Lockfile is up-to-date, resolution step is skipped/i";
|
||
return preg_match($preg, $output) || preg_match($preg2, $output);
|
||
} elseif ($name == 'yarn') {
|
||
return strpos(strtolower($output), 'done in ') !== false;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 输出检测,检测命令执行状态等操作
|
||
* @param $output
|
||
*/
|
||
public function outputCallback($output)
|
||
{
|
||
if (stripos($this->currentCommandKey, '.')) {
|
||
$commandKeyArr = explode('.', $this->currentCommandKey);
|
||
$commandPKey = $commandKeyArr[0] ?? '';
|
||
} else {
|
||
$commandPKey = $this->currentCommandKey;
|
||
}
|
||
|
||
if ($commandPKey == 'test-install' || $commandPKey == 'web-install') {
|
||
if (isset($commandKeyArr[1])) {
|
||
if ($commandKeyArr[1] == 'ni' && ($this->npmInstallCallback($output, 'npm') || $this->npmInstallCallback($output, 'cnpm') || $this->npmInstallCallback($output, 'pnpm') || $this->npmInstallCallback($output, 'yarn'))) {
|
||
$this->outputFlag('exec-success');
|
||
} elseif ($this->npmInstallCallback($output, $commandKeyArr[1])) {
|
||
$this->outputFlag('exec-success');
|
||
}
|
||
}
|
||
} elseif ($commandPKey == 'install-package-manager') {
|
||
if ($this->npmInstallCallback($output, 'npm')) {
|
||
// 获取一次版本号
|
||
if (isset($commandKeyArr[1]) && in_array($commandKeyArr[1], ['cnpm', 'yarn', 'pnpm']) && Version::getVersion($commandKeyArr[1])) {
|
||
$this->outputFlag('exec-success');
|
||
} elseif (isset($commandKeyArr[1]) && $commandKeyArr[1] == 'ni') {
|
||
$this->outputFlag('exec-success');
|
||
}
|
||
}
|
||
} elseif ($commandPKey == 'web-build') {
|
||
if (strpos(strtolower($output), 'build successfully!') !== false) {
|
||
if ($this->mvDist()) {
|
||
$this->outputFlag('exec-success');
|
||
} else {
|
||
$this->output('Build succeeded, but move file failed. Please operate manually.');
|
||
}
|
||
}
|
||
} elseif ($this->currentCommandKey == 'version-view.npm') {
|
||
$preg = "/([0-9]+)\.([0-9]+)\.([0-9]+)/";
|
||
if (preg_match($preg, $output)) {
|
||
$this->outputFlag('exec-success');
|
||
}
|
||
} elseif (isset($commandKeyArr[0]) && $commandKeyArr[0] == 'set-registry') {
|
||
$urls = [
|
||
'npm' => 'https://registry.npmjs.org/',
|
||
'taobao' => 'https://registry.npm.taobao.org/',
|
||
'rednpm' => 'http://registry.mirror.cqupt.edu.cn/',
|
||
];
|
||
$output = json_decode($output, true);
|
||
if (in_array($commandKeyArr[1], array_keys($urls)) && $output['data'] == $urls[$commandKeyArr[1]]) {
|
||
$this->outputFlag('exec-success');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行一个命令并以字符串数组的方式返回执行输出
|
||
* 代替 exec 使用,这样就只需要解除 popen 的函数禁用了
|
||
* @param $commandKey
|
||
* @return array | bool
|
||
*/
|
||
public function getOutputFromPopen($commandKey)
|
||
{
|
||
if (!function_exists('popen') || !function_exists('pclose') || !function_exists('feof') || !function_exists('fgets')) {
|
||
return false;
|
||
}
|
||
$command = $this->getCommand($commandKey, false);
|
||
|
||
$res = [];
|
||
$handle = popen($command . ' 2>&1', 'r');
|
||
while (!feof($handle)) {
|
||
$res[] = fgets($handle);
|
||
}
|
||
pclose($handle);
|
||
return $res;
|
||
}
|
||
|
||
public function break()
|
||
{
|
||
throw new HttpResponseException(Response::create()->contentType('text/event-stream'));
|
||
}
|
||
|
||
public function mvDist()
|
||
{
|
||
$distPath = root_path() . $this->distDir . DIRECTORY_SEPARATOR;
|
||
$indexHtmlPath = $distPath . 'index.html';
|
||
$assetsPath = $distPath . 'assets';
|
||
if (!file_exists($indexHtmlPath) || !file_exists($assetsPath)) {
|
||
return false;
|
||
}
|
||
|
||
$toIndexHtmlPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'index.html';
|
||
$toAssetsPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'assets';
|
||
@unlink($toIndexHtmlPath);
|
||
deldir($toAssetsPath);
|
||
|
||
if (rename($indexHtmlPath, $toIndexHtmlPath) && rename($assetsPath, $toAssetsPath)) {
|
||
deldir($distPath);
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public function changeTerminalConfig($config = [])
|
||
{
|
||
// 不保存在数据库中,因为切换包管理器时,数据库资料可能还未配置
|
||
$oldPort = Config::get('buildadmin.install_service_port');
|
||
$oldPackageManager = Config::get('buildadmin.npm_package_manager');
|
||
$newPort = request()->post('port', $config['port'] ?? $oldPort);
|
||
$newPackageManager = request()->post('manager', $config['manager'] ?? $oldPackageManager);
|
||
|
||
if ($oldPort == $newPort && $oldPackageManager == $newPackageManager) {
|
||
return true;
|
||
}
|
||
|
||
$buildConfigFile = config_path() . self::$buildConfigFileName;
|
||
$buildConfigContent = @file_get_contents($buildConfigFile);
|
||
$buildConfigContent = preg_replace("/'install_service_port'(\s+)=>(\s+)'{$oldPort}'/", "'install_service_port'\$1=>\$2'{$newPort}'", $buildConfigContent);
|
||
$buildConfigContent = preg_replace("/'npm_package_manager'(\s+)=>(\s+)'{$oldPackageManager}'/", "'npm_package_manager'\$1=>\$2'{$newPackageManager}'", $buildConfigContent);
|
||
$result = @file_put_contents($buildConfigFile, $buildConfigContent);
|
||
return (bool)$result;
|
||
}
|
||
} |