diff --git a/shell/update.sh b/shell/update.sh index c78191f5..466f941e 100755 --- a/shell/update.sh +++ b/shell/update.sh @@ -213,7 +213,56 @@ run_extra_shell() { ## 查看青龙服务日志 show_service_logs() { - pm2 logs qinglong "$@" + local lines="15" + local no_stream="false" + while [[ $# -gt 0 ]]; do + case "$1" in + --lines) + if [[ ! "${2:-}" =~ ^[0-9]+$ ]]; then + echo "ql log: --lines requires a non-negative integer" >&2 + return 2 + fi + lines="$2" + shift 2 + ;; + --lines=*) + lines="${1#--lines=}" + if [[ ! "$lines" =~ ^[0-9]+$ ]]; then + echo "ql log: --lines requires a non-negative integer" >&2 + return 2 + fi + shift + ;; + --nostream) + no_stream="true" + shift + ;; + *) + echo "ql log: unknown option: $1" >&2 + return 2 + ;; + esac + done + + local system_log_dir="$dir_data/syslog" + local latest_system_log + latest_system_log=$(ls -1t "$system_log_dir"/*.log* 2>/dev/null | head -n 1) + if [[ "$lines" != "0" ]] && [[ -f "$latest_system_log" ]]; then + tail -n "$lines" "$latest_system_log" + fi + + if [[ "$no_stream" == "true" ]]; then + if [[ -z "$latest_system_log" ]]; then + echo "ql log: no system log found in $system_log_dir" >&2 + return 1 + fi + return 0 + fi + + # Winston writes the active runtime log by date. Follow the filename so + # size-based rotations are handled without involving the PM2 log files. + local current_system_log="$system_log_dir/$(date +%F).log" + tail -n 0 --retry -F "$current_system_log" } ## 脚本用法 @@ -226,7 +275,7 @@ usage() { echo -e "5. $cmd_update rmlog # 删除旧日志" echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "7. $cmd_update check # 检测青龙环境并修复" - echo -e "8. $cmd_update log [pm2-log-options] # 查看青龙服务日志" + echo -e "8. $cmd_update log [--lines ] [--nostream] # 查看青龙运行日志" echo -e "9. $cmd_update resetlet # 重置登录错误次数" echo -e "10. $cmd_update resettfa # 禁用两步登录" echo -e "11. $cmd_update resetpwd # 修改登录密码" diff --git a/test/back/qlServiceLogCommand.test.cjs b/test/back/qlServiceLogCommand.test.cjs index 4f64dabb..74fab549 100644 --- a/test/back/qlServiceLogCommand.test.cjs +++ b/test/back/qlServiceLogCommand.test.cjs @@ -16,9 +16,10 @@ function writeExecutable(filePath, content) { fs.writeFileSync(filePath, content, { mode: 0o755 }); } -test('ql log scopes PM2 output to qinglong and forwards log options', () => { - const shellDir = path.join(fixtureRoot, 'shell'); - const binDir = path.join(fixtureRoot, 'bin'); +function prepareFixture(name) { + const testRoot = path.join(fixtureRoot, name); + const shellDir = path.join(testRoot, 'shell'); + const binDir = path.join(testRoot, 'bin'); fs.mkdirSync(shellDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true }); @@ -28,29 +29,100 @@ test('ql log scopes PM2 output to qinglong and forwards log options', () => { ); fs.writeFileSync( path.join(shellDir, 'share.sh'), - 'load_ql_envs() { :; }\nimport_config() { :; }\n', + [ + 'dir_data="${QL_DATA_DIR:-$QL_DIR/data}"', + 'load_ql_envs() { :; }', + 'import_config() { :; }', + '', + ].join('\n'), ); fs.writeFileSync(path.join(shellDir, 'api.sh'), ''); fs.writeFileSync(path.join(shellDir, 'env.sh'), ''); writeExecutable( path.join(binDir, 'pm2'), - '#!/usr/bin/env bash\nprintf "%s\\n" "$@"\n', + '#!/usr/bin/env bash\necho "pm2 must not be called" >&2\nexit 99\n', + ); + + return { binDir, shellDir, testRoot }; +} + +test('ql log reads the requested system log history without PM2', () => { + const { binDir, shellDir, testRoot } = prepareFixture('host'); + const systemLogDir = path.join(testRoot, 'data/syslog'); + fs.mkdirSync(systemLogDir, { recursive: true }); + fs.writeFileSync( + path.join(systemLogDir, '2026-08-20.log'), + 'system-one\nsystem-two\nsystem-three\n', ); const result = spawnSync( '/bin/bash', - [path.join(shellDir, 'update.sh'), 'log', '--lines', '25', '--nostream'], + [path.join(shellDir, 'update.sh'), 'log', '--lines', '2', '--nostream'], { encoding: 'utf8', env: { ...process.env, PATH: `${binDir}:${process.env.PATH}`, - QL_DIR: fixtureRoot, + QL_CONTAINER: 'false', + QL_DIR: testRoot, }, }, ); assert.equal(result.status, 0, result.stderr); - assert.equal(result.stdout, 'logs\nqinglong\n--lines\n25\n--nostream\n'); - assert.equal(fs.existsSync(path.join(fixtureRoot, 'data/log/log')), false); + assert.equal(result.stdout, 'system-two\nsystem-three\n'); + assert.equal(fs.existsSync(path.join(testRoot, 'data/log/log')), false); +}); + +test('ql log follows the current Winston system log without PM2', () => { + const { binDir, shellDir, testRoot } = prepareFixture('container'); + const systemLogDir = path.join(testRoot, 'data/syslog'); + const currentDate = spawnSync('date', ['+%F'], { encoding: 'utf8' }).stdout.trim(); + const currentLog = path.join(systemLogDir, `${currentDate}.log`); + fs.mkdirSync(systemLogDir, { recursive: true }); + fs.writeFileSync(currentLog, 'system-one\n'); + writeExecutable( + path.join(binDir, 'tail'), + '#!/usr/bin/env bash\nprintf "tail"\nprintf " <%s>" "$@"\nprintf "\\n"\n', + ); + + const result = spawnSync( + '/bin/bash', + [path.join(shellDir, 'update.sh'), 'log', '--lines', '0'], + { + encoding: 'utf8', + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH}`, + QL_CONTAINER: 'true', + QL_DIR: testRoot, + }, + }, + ); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, `tail <-n> <0> <--retry> <-F> <${currentLog}>\n`); + assert.equal(fs.existsSync(path.join(testRoot, 'data/log/log')), false); +}); + +test('ql log reports when no persisted system log exists', () => { + const { binDir, shellDir, testRoot } = prepareFixture('container-nostream'); + + const result = spawnSync( + '/bin/bash', + [path.join(shellDir, 'update.sh'), 'log', '--nostream'], + { + encoding: 'utf8', + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH}`, + QL_CONTAINER: 'true', + QL_DIR: testRoot, + }, + }, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout, ''); + assert.match(result.stderr, /no system log found/); });