@mzzsfy/dsh-cron-board 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/index.js +12 -4
- package/src/store.mjs +2 -0
- package/test/index.test.mjs +39 -7
- package/test/routes.test.mjs +27 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
## 依赖
|
|
43
43
|
|
|
44
44
|
- `croner`:cron 表达式解析与触发点计算
|
|
45
|
-
- 宿主服务:settings / timer(软依赖)、webServer
|
|
45
|
+
- 宿主服务:settings / timer(软依赖)、webServer;sessionController 在 apply 内探测(缺失时打日志干净禁用,插件整体不激活,含脚本任务;0.1.1-rc.2 及更早版本无此服务)
|
|
46
46
|
|
|
47
47
|
## 开发
|
|
48
48
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -18,15 +18,18 @@ import { createStore } from './store.mjs'
|
|
|
18
18
|
|
|
19
19
|
export const name = 'dsh-cron-board'
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
21
|
+
// 会话任务依赖的 sessionController 由 dsh-web-app 的 session-controller 条目挂载
|
|
22
|
+
// (0.1.1-rc.2 无此服务,inject 声明会让 fiber 永久 pending,旧版 boot 对 pending
|
|
23
|
+
// 条目抛错杀掉整个进程,连带已监听的 web 一起退出);host 服务为同步注册,apply 内
|
|
24
|
+
// 探测无时序竞态:缺失即打日志干净禁用,整体不激活(含脚本任务)的语义不变
|
|
25
|
+
export const inject = ['webServer']
|
|
24
26
|
|
|
25
27
|
// 数据目录 env 覆盖:测试注入临时目录(对齐 dsh-usage-panel 先例)
|
|
26
28
|
const DATA_DIR_ENV = 'DSH_CRON_BOARD_DATA_DIR'
|
|
27
29
|
const API_PREFIX = '/api/cron-board'
|
|
28
30
|
const SETTINGS_NS = 'cron-board'
|
|
29
31
|
const TIMER_UNAVAILABLE_REASON = '宿主定时服务不可用,自动调度已停用'
|
|
32
|
+
const SESSION_UNAVAILABLE_REASON = '宿主会话服务 sessionController 不可用(需 dsh-web-app 挂载 session-controller,0.1.1-rc.2 及更早版本缺失),插件整体禁用'
|
|
30
33
|
const DEFAULT_LOG_KEEP_PER_JOB = 200
|
|
31
34
|
|
|
32
35
|
// 设置 schema(schemastery 声明式):tick 周期 / 全局并发 / 日志与运行元数据保留份数 / 会话投递变量掩码
|
|
@@ -44,7 +47,12 @@ export function resolveDataDir(env = process.env) {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function apply(ctx, config) {
|
|
47
|
-
|
|
50
|
+
// ctx.get 不受 inject 门控(cordis 明文契约:缺失返回 undefined),属性访问会被拦截抛错
|
|
51
|
+
const sessionController = ctx.get('sessionController')
|
|
52
|
+
if (!sessionController) {
|
|
53
|
+
if (ctx.logger && ctx.logger.warn) ctx.logger.warn('[cron-board] ' + SESSION_UNAVAILABLE_REASON)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
48
56
|
const dataDir = resolveDataDir()
|
|
49
57
|
// 装配惰性单例:首个请求 / timer 激活触发初始化,各组件只建一次
|
|
50
58
|
let runtimePromise = null
|
package/src/store.mjs
CHANGED
package/test/index.test.mjs
CHANGED
|
@@ -19,15 +19,18 @@ test('index:数据目录默认 ~/.dsh/cron-board,env 可覆盖', () => {
|
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
test('index:apply 注册 prefix 路由且请求走通(列表接口)', async () => {
|
|
22
|
-
// Given webServer 桩:收集注册路由(sessionController
|
|
22
|
+
// Given webServer 桩:收集注册路由(sessionController 经 get 桩供给,对齐 ctx.get 探测路径)
|
|
23
23
|
const routes = new Map()
|
|
24
|
+
const sessionController = { create: async () => ({ sessionId: 's-x' }), prompt: async () => ({ accepted: true }) }
|
|
24
25
|
const ctx = {
|
|
25
26
|
effect(fn) {
|
|
26
27
|
fn()
|
|
27
28
|
},
|
|
28
29
|
inject() {},
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
get(name) {
|
|
31
|
+
if (name === 'sessionController') return sessionController
|
|
32
|
+
return undefined
|
|
33
|
+
},
|
|
31
34
|
webServer: {
|
|
32
35
|
register(route) {
|
|
33
36
|
routes.set(route.path, route.handler)
|
|
@@ -114,10 +117,39 @@ function makeFullCtx({ timerAvailable = true } = {}) {
|
|
|
114
117
|
return { ctx, routes, registered, intervals, settingsService, sessions }
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
test('index:inject
|
|
118
|
-
// Given
|
|
119
|
-
//
|
|
120
|
-
|
|
120
|
+
test('index:inject 仅声明 webServer(sessionController 走 apply 内探测)', async () => {
|
|
121
|
+
// Given sessionController 在旧版宿主(0.1.1-rc.2)不存在,inject 声明会让 fiber 永久
|
|
122
|
+
// pending,旧版 boot 对 pending 条目抛错杀掉整个进程;host 服务同步注册,探测无竞态
|
|
123
|
+
// Then 静态锁定 inject 声明
|
|
124
|
+
assert.deepEqual(mod.inject, ['webServer'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('index:sessionController 缺失时干净禁用(不注册路由与设置)', async () => {
|
|
128
|
+
// Given 无 sessionController 的宿主上下文
|
|
129
|
+
const routes = new Map()
|
|
130
|
+
const registered = []
|
|
131
|
+
const warnings = []
|
|
132
|
+
const ctx = {
|
|
133
|
+
effect(fn) {
|
|
134
|
+
fn()
|
|
135
|
+
},
|
|
136
|
+
inject() {},
|
|
137
|
+
logger: { warn: (line) => warnings.push(line) },
|
|
138
|
+
get: () => undefined,
|
|
139
|
+
webServer: {
|
|
140
|
+
register(route) {
|
|
141
|
+
routes.set(route.path, route.handler)
|
|
142
|
+
return () => {}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
// When apply
|
|
147
|
+
apply(ctx)
|
|
148
|
+
// Then 无路由注册、无设置注册,且打日志说明原因
|
|
149
|
+
assert.equal(routes.size, 0)
|
|
150
|
+
assert.deepEqual(registered, [])
|
|
151
|
+
assert.equal(warnings.length, 1)
|
|
152
|
+
assert.ok(warnings[0].includes('sessionController'))
|
|
121
153
|
})
|
|
122
154
|
|
|
123
155
|
test('index:settings 注册 cron-board 命名空间且 timer 承载默认周期 tick', async () => {
|
package/test/routes.test.mjs
CHANGED
|
@@ -15,8 +15,13 @@ import { createApi } from '../src/api.mjs'
|
|
|
15
15
|
|
|
16
16
|
async function makeApi(t, overrides = {}) {
|
|
17
17
|
const dir = await mkdtemp(join(tmpdir(), 'cron-board-api-'))
|
|
18
|
-
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
19
18
|
const store = await createStore({ dir: join(dir, 'data') })
|
|
19
|
+
// 清理前等写链排空:终态只保证内存可见,收尾写(任务卡回填、日志裁剪)提交晚于 flush,
|
|
20
|
+
// rmdir 与其赛跑会 ENOTEMPTY;retries 吸收(镜像 executor/scheduler 测试同款参数)
|
|
21
|
+
t.after(async () => {
|
|
22
|
+
await store.flush()
|
|
23
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
24
|
+
})
|
|
20
25
|
const logger = createLogger({ rootDir: join(dir, 'logs') })
|
|
21
26
|
const executor = createExecutor({ store, logger, readMaxConcurrent: () => 2 })
|
|
22
27
|
const api = createApi({ store, logger, executor, ...overrides })
|
|
@@ -239,6 +244,27 @@ test('jobs 路由:同名多值展开为多条 RunRecord 与多次运行', async
|
|
|
239
244
|
assert.deepEqual([...logs].sort(), ['one', 'two'])
|
|
240
245
|
})
|
|
241
246
|
|
|
247
|
+
test('jobs 路由:慢盘下运行终态后清理不与收尾写赛跑(ENOTEMPTY 回归)', async (t) => {
|
|
248
|
+
// Given 持久化注入慢盘延迟(放大终态内存可见与收尾写落盘的窗口)
|
|
249
|
+
const { store, api } = await makeApi(t)
|
|
250
|
+
const rawUpdate = store.runs.update.bind(store.runs)
|
|
251
|
+
store.runs.update = (runId, patch) => new Promise((resolve) => {
|
|
252
|
+
setTimeout(() => rawUpdate(runId, patch).then(resolve, resolve), 25)
|
|
253
|
+
})
|
|
254
|
+
const job = await store.jobs.create({
|
|
255
|
+
name: 'slowdisk', kind: 'shell',
|
|
256
|
+
command: `${process.execPath} -e ""`,
|
|
257
|
+
schedule: '* * * * *', enabled: true, timeoutMs: 10 * 1000,
|
|
258
|
+
})
|
|
259
|
+
// When 手动运行到终态后测试即结束(清理走 makeApi 的 flush+retries)
|
|
260
|
+
const runRes = await call(api, 'POST', '/api/cron-board/jobs/' + job.id + '/run')
|
|
261
|
+
const { runIds } = runRes.payload
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
const row = store.runs.get(runIds[0])
|
|
264
|
+
return row && row.status !== 'queued' && row.status !== 'running' ? row : null
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
242
268
|
test('status 路由:报告调度器与 timer 可用性', async (t) => {
|
|
243
269
|
// Given scheduler 桩与 timer 运行标记
|
|
244
270
|
const { api } = await makeApi(t, {
|