97 lines
4.1 KiB
JavaScript
97 lines
4.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { spawn, spawnSync } from 'node:child_process'
|
||
import { once } from 'node:events'
|
||
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
||
import os from 'node:os'
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import test from 'node:test'
|
||
|
||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||
const lockScript = path.join(root, 'deploy', 'workspace-lock.ps1')
|
||
const shellArguments = ['-NoProfile', '-ExecutionPolicy', 'Bypass']
|
||
const windowsOnly = { skip: process.platform !== 'win32', timeout: 30000 }
|
||
const quote = value => `'${value.replaceAll("'", "''")}'`
|
||
|
||
function acquire(directory) {
|
||
return `$ErrorActionPreference = 'Stop'; . ${quote(lockScript)}; $lock = Enter-SmailWorkspaceLock -Root ${quote(directory)};`
|
||
}
|
||
|
||
function probe(directory) {
|
||
return spawnSync('powershell.exe', [...shellArguments, '-Command',
|
||
`${acquire(directory)} try { Write-Output 'acquired' } finally { $lock.ReleaseMutex(); $lock.Dispose() }`,
|
||
], { encoding: 'utf8', windowsHide: true, timeout: 10000 })
|
||
}
|
||
|
||
async function hold(directory, context) {
|
||
const child = spawn('powershell.exe', [...shellArguments, '-Command',
|
||
`${acquire(directory)} try { Write-Output 'ready'; [Console]::In.ReadLine() | Out-Null } finally { $lock.ReleaseMutex(); $lock.Dispose() }`,
|
||
], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
|
||
context.after(() => {
|
||
if (child.exitCode === null) child.kill()
|
||
})
|
||
await new Promise((resolve, reject) => {
|
||
let output = ''
|
||
let errors = ''
|
||
child.stderr.on('data', data => { errors += data })
|
||
child.stdout.on('data', data => {
|
||
output += data
|
||
if (output.includes('ready')) resolve()
|
||
})
|
||
child.once('error', reject)
|
||
child.once('exit', code => reject(new Error(`锁持有进程提前退出:${code};${errors}`)))
|
||
})
|
||
return child
|
||
}
|
||
|
||
async function release(child) {
|
||
const exited = once(child, 'exit')
|
||
child.stdin.end('\n')
|
||
await exited
|
||
}
|
||
|
||
test('构建入口在计算指纹和移动资源之前获取互斥量', async () => {
|
||
const source = await readFile(path.join(root, 'deploy', 'build.ps1'), 'utf8')
|
||
const acquireIndex = source.indexOf('$workspaceLock = Enter-SmailWorkspaceLock')
|
||
assert.ok(acquireIndex > 0)
|
||
assert.ok(acquireIndex < source.indexOf('fingerprint'))
|
||
assert.ok(acquireIndex < source.indexOf('Move-Item'))
|
||
assert.match(source, /finally\s*\{\s*\$workspaceLock\.ReleaseMutex\(\)/)
|
||
})
|
||
|
||
test('同工作区互斥,不同工作区可以并行,正常退出后可重试', windowsOnly, async context => {
|
||
const directory = await mkdtemp(path.join(os.tmpdir(), 'smail-lock-'))
|
||
const other = await mkdtemp(path.join(os.tmpdir(), 'smail-lock-other-'))
|
||
const child = await hold(directory, context)
|
||
assert.notEqual(probe(directory).status, 0)
|
||
assert.equal(probe(other).status, 0)
|
||
await release(child)
|
||
assert.equal(probe(directory).status, 0)
|
||
})
|
||
|
||
test('持有进程异常退出后无需删除文件即可重新获取锁', windowsOnly, async context => {
|
||
const directory = await mkdtemp(path.join(os.tmpdir(), 'smail-lock-crash-'))
|
||
const child = await hold(directory, context)
|
||
const exited = once(child, 'exit')
|
||
child.kill()
|
||
await exited
|
||
assert.equal(probe(directory).status, 0)
|
||
})
|
||
|
||
test('Go 入口与构建共用工作区锁,并保留 Go 退出码', windowsOnly, async context => {
|
||
const directory = await mkdtemp(path.join(os.tmpdir(), 'smail-go-lock-'))
|
||
const invocation = path.join(directory, 'invocation.json')
|
||
const run = () => spawnSync('powershell.exe', [...shellArguments, '-File',
|
||
path.join(root, 'test', 'deployment', 'run-go.ps1'), '-InvocationFile', invocation,
|
||
], { encoding: 'utf8', windowsHide: true, timeout: 10000 })
|
||
await writeFile(invocation, JSON.stringify({ arguments: ['version'] }))
|
||
const child = await hold(root, context)
|
||
assert.notEqual(run().status, 0)
|
||
await release(child)
|
||
const success = run()
|
||
assert.equal(success.status, 0, success.stderr)
|
||
assert.match(success.stdout, /go version/)
|
||
await writeFile(invocation, JSON.stringify({ arguments: ['smail-invalid-test-command'] }))
|
||
assert.notEqual(run().status, 0)
|
||
})
|