需求:让积分榜能在飞腾 D2000(ARM64)的 Linux 上跑起来,**同时不破坏 Windows**。
## 一、Go 服务端:交叉编译即可(**已实测**)
- SQLite 用的是 `modernc.org/sqlite`(纯 Go 驱动),全项目**没有一处 `import "C"`** → `CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build` 直接出;
- 产物是**静态链接**(`ELF 64-bit LSB executable, ARM aarch64, statically linked`)→ **不依赖 glibc 版本**,对国产系统友好;
- 本机(arm64)**实际启动验证**:数据目录、局域网地址探测、数据库迁移(0001/0002/0003)全部正常;
- 产物:`debug/服务端-arm64/scoreboard`(29 MB,`-s -w`)。
## 二、Console(Electron 宿主):平台差异收口到一个新文件
- **新增 `console/main/platform.js`**:平台差异集中在这 45 行里,其余代码不写平台判断。
⚠️ **`platform` 参数缺省按 Windows 解析** —— 漏传参数走的就是改动前那条老路(这比"改了再小心"更保守:**默认行为不变,新行为必须显式传参**)。
- `userpaths.js` / `datadir.js`:Windows 分支 `%LOCALAPPDATA%\ScoreboardConsole\` **逐字保留**;Linux 分支按 XDG(`$XDG_DATA_HOME`,缺省 `~/.local/share`)。两平台各自的"拿不到用户目录"提示文案分开写(免得现场去设一个本平台不存在的环境变量)。
- `config.js`:服务端文件名按平台取(Windows `scoreboard.exe` / Linux `scoreboard`)。
- **新增 `console/main/autostart.js`**:Linux 用 `~/.config/autostart/*.desktop`(开=写文件、关=删文件;Linux 没有"已注册但未启用"这个中间态)。
- 托盘与窗口行为:Linux 的通知区多数不派发 double-click,因此按平台分支成单击。
## 三、怎么保证没弄坏 Windows
1. **设计上**:平台差异集中 + 缺省值就是 Windows(见上);
2. **逐行核对 diff**:Windows 分支那几行原样未动;
3. **测试**:Console 由 **68 → 96 例**,新增全是平台分支用例,**既有断言一条没改**;
4. **真机验证**:构建机重打 Windows 包并跑(这一道才算收口)。
## 四、自检
全量自检两轮**全绿、`EXIT=0`**:服务端 14 包 / 前端 **944 例** / Console **96 例**。
## 版本
`dev_0.5.9` → **`dev_0.5.10`**;安装器素材同步重生成(照例只动 `brand.bmp` 两张)。
117 lines
5.5 KiB
JavaScript
117 lines
5.5 KiB
JavaScript
'use strict'
|
||
|
||
const assert = require('node:assert/strict')
|
||
const fs = require('node:fs')
|
||
const os = require('node:os')
|
||
const path = require('node:path')
|
||
const test = require('node:test')
|
||
|
||
const { DEFAULTS, loadConfig } = require('./config')
|
||
|
||
function makeDir(t) {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scoreboard-config-'))
|
||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }))
|
||
return dir
|
||
}
|
||
|
||
// 现场机器上那份用户配置:内容原样写进文件,读的也必须是文件里真实写了什么
|
||
// platform 与 env 一样是入参,缺省按 Windows 解析(改动前的既有口径)
|
||
function loadWith(t, raw, platform = 'win32') {
|
||
const dir = makeDir(t)
|
||
const file = path.join(dir, 'config.json')
|
||
fs.writeFileSync(file, typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2), 'utf8')
|
||
return loadConfig(file, platform)
|
||
}
|
||
|
||
test('缺省的 allowClickThrough 不穿透(默认值就是 false)', (t) => {
|
||
assert.equal(DEFAULTS.display.allowClickThrough, false)
|
||
const cfg = loadWith(t, { display: {} })
|
||
assert.equal(cfg.values.display.allowClickThrough, false)
|
||
assert.deepEqual(cfg.notices, [])
|
||
})
|
||
|
||
test('显式写 allowClickThrough: true 才穿透,且不产生废弃提示', (t) => {
|
||
const cfg = loadWith(t, { display: { allowClickThrough: true } })
|
||
assert.equal(cfg.values.display.allowClickThrough, true)
|
||
assert.deepEqual(cfg.notices, [])
|
||
})
|
||
|
||
// 本次修复的核心回归钉子:老机器升级后,用户配置文件里那个旧版模板写下的
|
||
// ignoreMouse: true 必须彻底失效(userpaths.js 不会覆盖用户文件,只能靠不读旧键)
|
||
test('老配置里的旧键 ignoreMouse 已失效:既不生效、也不再是穿透开关', (t) => {
|
||
const cfg = loadWith(t, { display: { ignoreMouse: true } })
|
||
|
||
assert.equal('ignoreMouse' in DEFAULTS.display, false, 'DEFAULTS 里不能再有旧键')
|
||
assert.equal(cfg.values.display.allowClickThrough, false, '旧键 true 不能把穿透带回来')
|
||
assert.equal(cfg.values.display.ignoreMouse, undefined, '旧键不该被 merge 进最终配置')
|
||
|
||
// 但必须留下一条一次性提示,让现场知道该改哪个键
|
||
assert.equal(cfg.notices.length, 1)
|
||
const [notice] = cfg.notices
|
||
assert.match(notice, /display\.ignoreMouse/)
|
||
assert.match(notice, /display\.allowClickThrough/)
|
||
assert.match(notice, /废弃/, '要明说这个键已经不用了')
|
||
assert.match(notice, /不穿透/, '要明说现在的默认是不穿透')
|
||
assert.match(notice, /true/, '要给出「确实想穿透」的写法')
|
||
})
|
||
|
||
test('旧键提示只跟着「文件里写了旧键」出现,缺省或新键都不提示', (t) => {
|
||
// 旧版模板整份配置(现场最常见的形态):只提示一次,不会按重排/每帧刷
|
||
const legacyTemplate = loadWith(t, {
|
||
display: { selection: 'all-secondary', kiosk: true, ignoreMouse: true }
|
||
})
|
||
assert.equal(legacyTemplate.notices.length, 1)
|
||
|
||
// 新模板(已去掉旧键、写上 new key)不再提示
|
||
const currentTemplate = loadWith(t, {
|
||
display: { selection: 'all-secondary', kiosk: true, allowClickThrough: false }
|
||
})
|
||
assert.deepEqual(currentTemplate.notices, [])
|
||
|
||
// 两个键都在:仍然按「有旧键」提示一次(提示说的是旧键不生效,与新键无关)
|
||
const both = loadWith(t, { display: { ignoreMouse: true, allowClickThrough: true } })
|
||
assert.equal(both.notices.length, 1)
|
||
assert.equal(both.values.display.allowClickThrough, true, '新键显式 true 仍然算数')
|
||
})
|
||
|
||
test('配置文件读不出来/内容不是对象时不报废弃键、也不炸', (t) => {
|
||
const missing = loadConfig(path.join(makeDir(t), 'nope.json'))
|
||
assert.deepEqual(missing.notices, [])
|
||
assert.equal(missing.warning !== null, true)
|
||
assert.equal(missing.values.display.allowClickThrough, false)
|
||
|
||
const broken = loadWith(t, '{ 这不是 JSON')
|
||
assert.deepEqual(broken.notices, [])
|
||
|
||
assert.deepEqual(loadWith(t, 'null').notices, [])
|
||
assert.deepEqual(loadWith(t, '[1,2,3]').notices, [])
|
||
})
|
||
|
||
// 服务端可执行文件名按平台取:Windows 是 scoreboard.exe,Linux(UOS / 麒麟)上没有 .exe。
|
||
// 内置默认值保持 Windows 那个名字不变(改动前的口径),按平台换值只发生在 loadConfig 里。
|
||
test('Windows 下不写 exe 时默认仍是 scoreboard.exe', (t) => {
|
||
assert.equal(loadWith(t, { server: {} }).values.server.exe, 'scoreboard.exe')
|
||
assert.equal(DEFAULTS.server.exe, 'scoreboard.exe')
|
||
})
|
||
|
||
test('Linux 下不写 exe 时默认是 scoreboard', (t) => {
|
||
assert.equal(loadWith(t, { server: {} }, 'linux').values.server.exe, 'scoreboard')
|
||
})
|
||
|
||
test('Linux 下配置文件里写着模板留下的 scoreboard.exe 时按 scoreboard 走', (t) => {
|
||
// 随包 config.json 模板与老机器的用户配置里都是 scoreboard.exe。那不是现场改过的值,
|
||
// 而是模板写进去的默认值;Linux 上照它去找一个 .exe 只会「未找到服务端程序」。
|
||
// 与 RENAMED_KEYS 同一个道理:模板落进老文件里的值,光改默认值救不了。
|
||
assert.equal(loadWith(t, { server: { exe: 'scoreboard.exe' } }, 'linux').values.server.exe, 'scoreboard')
|
||
})
|
||
|
||
test('Windows 下文件里写着别的名字就照用(两边都不做猜测)', (t) => {
|
||
assert.equal(loadWith(t, { server: { exe: 'my-scoreboard.exe' } }).values.server.exe, 'my-scoreboard.exe')
|
||
assert.equal(loadWith(t, { server: { exe: 'my-scoreboard' } }, 'linux').values.server.exe, 'my-scoreboard')
|
||
})
|
||
|
||
test('server 段被写成 null 时 loadConfig 不炸(与改动前一致)', (t) => {
|
||
const cfg = loadWith(t, { server: null }, 'linux')
|
||
assert.equal(cfg.values.server, null)
|
||
})
|