@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 CHANGED
@@ -42,7 +42,7 @@
42
42
  ## 依赖
43
43
 
44
44
  - `croner`:cron 表达式解析与触发点计算
45
- - 宿主服务:settings / timer(软依赖)、webServer、sessionController(硬依赖,cordis 门控——缺失时插件整体不激活,含脚本任务)
45
+ - 宿主服务:settings / timer(软依赖)、webServer;sessionController 在 apply 内探测(缺失时打日志干净禁用,插件整体不激活,含脚本任务;0.1.1-rc.2 及更早版本无此服务)
46
46
 
47
47
  ## 开发
48
48
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-cron-board",
3
3
  "description": "轻量级青龙面板:环境变量集中管理与定时任务调度,执行体支持本地脚本与 dsh 会话任务",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
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
- // 会话任务与路由/定时同为本体能力:sessionController 硬依赖声明交 cordis 门控,
22
- // 服务未就绪/缺失时 fiber 不激活即整体干净禁用(apply 内同步软探测会撞异步挂载时序,mce 同款事故)
23
- export const inject = ['webServer', 'sessionController']
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
- const sessionController = ctx.sessionController
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
@@ -181,6 +181,8 @@ export async function createStore({ dir }) {
181
181
  return {
182
182
  envs,
183
183
  jobs,
184
+ // 写链排空:等待已提交 mutate 全部结束(不报告写失败);测试清理数据目录前调用以缩小与删除的竞态面
185
+ flush: () => link.chain.catch(() => {}),
184
186
  runs: {
185
187
  get rows() {
186
188
  return runs.rows
@@ -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 属性桩满足 inject 门控后环境)
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
- sessionController: { create: async () => ({ sessionId: 's-x' }), prompt: async () => ({ accepted: true }) },
30
- get: () => undefined,
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 声明 webServer+sessionController(cordis 门控整体禁用的唯一形态)', async () => {
118
- // Given 宿主会话服务走 cordis 硬依赖门控:缺失时 fiber 不激活,apply 不执行
119
- // Then 静态锁定 inject 声明(修复 mce 同款「apply 内同步探测误判缺失」事故形态)
120
- assert.deepEqual(mod.inject, ['webServer', 'sessionController'])
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 () => {
@@ -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, {