@whyour/qinglong 2.20.2-3 → 2.21.0-10

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.
Files changed (238) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +43 -0
  3. package/README-en.md +11 -0
  4. package/README.md +11 -0
  5. package/back/protos/api.proto +3 -0
  6. package/deploy/kubernetes/README.md +81 -0
  7. package/deploy/kubernetes/base/kustomization.yaml +5 -0
  8. package/deploy/kubernetes/base/qinglong.yaml +95 -0
  9. package/deploy/kubernetes/overlays/example/kustomization.yaml +10 -0
  10. package/docker/Dockerfile +51 -69
  11. package/docker/Dockerfile.310 +94 -0
  12. package/docker/Dockerfile.debian +120 -0
  13. package/docker/{310.Dockerfile → Dockerfile.debian310} +13 -5
  14. package/docker/docker-entrypoint.sh +95 -3
  15. package/docs/PROJECT_ARCHITECTURE.md +550 -0
  16. package/package.json +29 -28
  17. package/sample/config.sample.sh +19 -3
  18. package/sample/notify.js +145 -9
  19. package/sample/notify.py +110 -11
  20. package/shell/api.sh +51 -21
  21. package/shell/bot.sh +30 -16
  22. package/shell/check.sh +21 -19
  23. package/shell/lang/en.sh +105 -0
  24. package/shell/lang/zh.sh +105 -0
  25. package/shell/otask.sh +117 -28
  26. package/shell/preload/client.js +20 -2
  27. package/shell/preload/esm-loader.mjs +41 -0
  28. package/shell/preload/sitecustomize.js +36 -0
  29. package/shell/pub.sh +8 -8
  30. package/shell/rmlog.sh +5 -5
  31. package/shell/share.sh +50 -31
  32. package/shell/start.sh +40 -27
  33. package/shell/task.sh +10 -2
  34. package/shell/update.sh +22 -22
  35. package/static/build/api/config.js +17 -7
  36. package/static/build/api/cron.js +40 -3
  37. package/static/build/api/dashboard.js +344 -0
  38. package/static/build/api/env.js +40 -2
  39. package/static/build/api/index.js +2 -0
  40. package/static/build/api/log.js +15 -5
  41. package/static/build/api/script.js +34 -9
  42. package/static/build/api/system.js +22 -8
  43. package/static/build/api/user.js +6 -5
  44. package/static/build/app.js +22 -19
  45. package/static/build/config/const.js +8 -6
  46. package/static/build/config/container.js +12 -0
  47. package/static/build/config/grpcCerts.js +150 -0
  48. package/static/build/config/index.js +6 -4
  49. package/static/build/config/util.js +69 -18
  50. package/static/build/data/cron.js +3 -1
  51. package/static/build/data/cronStats.js +59 -0
  52. package/static/build/data/env.js +2 -0
  53. package/static/build/data/notify.js +18 -1
  54. package/static/build/data/runningInstance.js +57 -0
  55. package/static/build/data/sock.js +1 -0
  56. package/static/build/loaders/app.js +7 -7
  57. package/static/build/loaders/db.js +8 -2
  58. package/static/build/loaders/deps.js +1 -22
  59. package/static/build/loaders/express.js +19 -15
  60. package/static/build/loaders/initData.js +20 -2
  61. package/static/build/loaders/initFile.js +3 -1
  62. package/static/build/loaders/initTask.js +3 -2
  63. package/static/build/loaders/server.js +1 -1
  64. package/static/build/protos/api.js +23 -1
  65. package/static/build/schedule/addCron.js +67 -10
  66. package/static/build/schedule/api.js +1 -1
  67. package/static/build/schedule/client.js +9 -1
  68. package/static/build/schedule/delCron.js +11 -1
  69. package/static/build/schedule/health.js +1 -1
  70. package/static/build/services/config.js +19 -5
  71. package/static/build/services/cron.js +198 -38
  72. package/static/build/services/dependence.js +19 -18
  73. package/static/build/services/env.js +31 -3
  74. package/static/build/services/grpc.js +32 -6
  75. package/static/build/services/http.js +33 -18
  76. package/static/build/services/notify.js +87 -8
  77. package/static/build/services/open.js +2 -1
  78. package/static/build/services/sshKey.js +6 -3
  79. package/static/build/services/subscription.js +8 -5
  80. package/static/build/services/system.js +34 -9
  81. package/static/build/services/user.js +39 -13
  82. package/static/build/shared/auth.js +7 -1
  83. package/static/build/shared/i18n.js +160 -0
  84. package/static/build/shared/pLimit.js +15 -3
  85. package/static/build/shared/runCron.js +5 -0
  86. package/static/build/shared/store.js +7 -0
  87. package/static/build/validation/schedule.js +10 -0
  88. package/static/dist/1089.0ecc5383.async.js +1 -0
  89. package/static/dist/1147.62793604.async.js +1 -0
  90. package/static/dist/1192.4e5740f2.async.js +1 -0
  91. package/static/dist/1214.c6469b53.async.js +1 -0
  92. package/static/dist/134.54df9e38.async.js +1 -0
  93. package/static/dist/1352.96a77e1d.async.js +1 -0
  94. package/static/dist/{1765.d8e002d7.async.js → 1765.5ac01a93.async.js} +1 -1
  95. package/static/dist/1836.f0b74cf1.async.js +1 -0
  96. package/static/dist/{1885.e0d00d2d.async.js → 1885.e1ea09f6.async.js} +1 -1
  97. package/static/dist/1967.3f3945d0.async.js +1 -0
  98. package/static/dist/{2096.383c1047.async.js → 2096.6d98fd12.async.js} +1 -1
  99. package/static/dist/2208.85285476.async.js +1 -0
  100. package/static/dist/2286.f371c743.async.js +1 -0
  101. package/static/dist/2476.4e9b0992.async.js +1 -0
  102. package/static/dist/2537.2b262ee0.async.js +1 -0
  103. package/static/dist/{255.12f03ab2.async.js → 255.8a80b983.async.js} +1 -1
  104. package/static/dist/2635.d4c59d23.async.js +1 -0
  105. package/static/dist/2808.cdc0995c.async.js +1 -0
  106. package/static/dist/{2821.be3dc88e.async.js → 2821.651f31e5.async.js} +1 -1
  107. package/static/dist/2986.4c49eef7.async.js +1 -0
  108. package/static/dist/300.08ac9875.async.js +1 -0
  109. package/static/dist/3191.545ab873.async.js +1 -0
  110. package/static/dist/3714.8d6dba9e.async.js +1 -0
  111. package/static/dist/3906.a5eee612.async.js +1 -0
  112. package/static/dist/{6541.a6d499de.async.js → 3948.4fe809fd.async.js} +1 -1
  113. package/static/dist/{5171.7fc6d0a2.async.js → 4573.16f19278.async.js} +1 -1
  114. package/static/dist/{4859.93e63ea6.async.js → 4859.6592cebb.async.js} +1 -1
  115. package/static/dist/4887.8e3ae573.async.js +1 -0
  116. package/static/dist/{4902.54ecbdb5.async.js → 4902.555f92ab.async.js} +1 -1
  117. package/static/dist/{4934.1ca6b6b0.async.js → 4934.f3539d08.async.js} +1 -1
  118. package/static/dist/5077.cbd111cd.async.js +1 -0
  119. package/static/dist/{6247.b550d996.async.js → 5157.7c5af144.async.js} +1 -1
  120. package/static/dist/{540.481d4708.async.js → 540.19ddf020.async.js} +1 -1
  121. package/static/dist/{5970.10ac4f16.async.js → 5970.d3a6e7bd.async.js} +1 -1
  122. package/static/dist/{6013.2d7bb12a.async.js → 6013.4e38de36.async.js} +1 -1
  123. package/static/dist/{6016.9c379049.async.js → 6016.1e6574b6.async.js} +1 -1
  124. package/static/dist/{6035.5889ddc7.async.js → 6035.32ebfad9.async.js} +1 -1
  125. package/static/dist/6339.ab305e96.async.js +1 -0
  126. package/static/dist/6569.55177f6a.async.js +1 -0
  127. package/static/dist/6610.c111af7e.async.js +1 -0
  128. package/static/dist/{3034.6413c38e.async.js → 6638.87a163d2.async.js} +1 -1
  129. package/static/dist/{6646.5fc37228.async.js → 6646.95e92533.async.js} +1 -1
  130. package/static/dist/6665.1d099ad1.async.js +1 -0
  131. package/static/dist/{7355.b7c0562f.async.js → 7355.f9f263d8.async.js} +1 -1
  132. package/static/dist/{7384.065ccae2.async.js → 7384.1bb449d5.async.js} +1 -1
  133. package/static/dist/7441.ee1bf2bb.async.js +1 -0
  134. package/static/dist/{7508.a31662a3.async.js → 7508.7dcee91c.async.js} +1 -1
  135. package/static/dist/{7802.6b73f16a.async.js → 7802.34255805.async.js} +1 -1
  136. package/static/dist/{7984.e6bb9378.async.js → 7984.594296a5.async.js} +1 -1
  137. package/static/dist/{5653.4fce7ce8.async.js → 8033.5cb31493.async.js} +1 -1
  138. package/static/dist/8147.2fb55202.async.js +1 -0
  139. package/static/dist/8187.48c130d6.async.js +1 -0
  140. package/static/dist/8495.d53e15ca.async.js +1 -0
  141. package/static/dist/8587.315b06c0.async.js +1 -0
  142. package/static/dist/8826.db34ad8d.async.js +1 -0
  143. package/static/dist/{2742.4852aac8.async.js → 8865.ed665d31.async.js} +1 -1
  144. package/static/dist/901.7ee5c6d3.async.js +1 -0
  145. package/static/dist/9271.231db2ce.async.js +1 -0
  146. package/static/dist/9323.a33f47da.async.js +1 -0
  147. package/static/dist/{9730.30083c91.async.js → 9730.801665a3.async.js} +1 -1
  148. package/static/dist/{9761.627ca3b5.async.js → 9761.360a19d8.async.js} +1 -1
  149. package/static/dist/index.html +2 -2
  150. package/static/dist/layouts__index.a7a2bfe0.async.js +1 -0
  151. package/static/dist/layouts__index.adf0692f.chunk.css +1 -0
  152. package/static/dist/preload_helper.3048fe5b.js +1 -0
  153. package/static/dist/{src__pages__config__index.622b6ee8.async.js → src__pages__config__index.a22ff7dc.async.js} +1 -1
  154. package/static/dist/{src__pages__crontab__const.323d5124.async.js → src__pages__crontab__const.aba07deb.async.js} +1 -1
  155. package/static/dist/src__pages__crontab__detail.b98f0084.async.js +1 -0
  156. package/static/dist/src__pages__crontab__index.68e78a33.async.js +1 -0
  157. package/static/dist/{src__pages__crontab__logModal.5e6a4bf2.async.js → src__pages__crontab__logModal.831d5e4b.async.js} +1 -1
  158. package/static/dist/src__pages__crontab__modal.f7041c7c.async.js +1 -0
  159. package/static/dist/src__pages__crontab__type.d7af36e5.async.js +1 -0
  160. package/static/dist/{src__pages__crontab__viewCreateModal.ffcf7a24.async.js → src__pages__crontab__viewCreateModal.4d589f66.async.js} +1 -1
  161. package/static/dist/{src__pages__crontab__viewManageModal.c2724575.async.js → src__pages__crontab__viewManageModal.0e317746.async.js} +1 -1
  162. package/static/dist/src__pages__dashboard__index.58177357.async.js +1 -0
  163. package/static/dist/src__pages__dependence__index.b7bbc795.async.js +1 -0
  164. package/static/dist/{src__pages__dependence__logModal.f123e2ac.async.js → src__pages__dependence__logModal.0681830b.async.js} +1 -1
  165. package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
  166. package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
  167. package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
  168. package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
  169. package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
  170. package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
  171. package/static/dist/src__pages__log__index.9c66639f.async.js +1 -0
  172. package/static/dist/{src__pages__login__index.cd6e3152.async.js → src__pages__login__index.8c813eb1.async.js} +1 -1
  173. package/static/dist/{src__pages__script__components__UnsupportedFilePreview__index.39074c68.async.js → src__pages__script__components__UnsupportedFilePreview__index.c347a13a.async.js} +1 -1
  174. package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
  175. package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
  176. package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
  177. package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
  178. package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
  179. package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
  180. package/static/dist/src__pages__setting__checkUpdate.34d773f2.async.js +1 -0
  181. package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
  182. package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
  183. package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
  184. package/static/dist/src__pages__setting__other.707cfafe.async.js +1 -0
  185. package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
  186. package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
  187. package/static/dist/{src__pages__subscription__logModal.0caa7283.async.js → src__pages__subscription__logModal.3864b37f.async.js} +1 -1
  188. package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
  189. package/static/dist/{umi.ef8199a4.js → umi.a54a59f6.js} +1 -1
  190. package/version.yaml +33 -4
  191. package/sample/notify.py.save +0 -1010
  192. package/static/dist/105.85a5c47a.async.js +0 -1
  193. package/static/dist/1083.f86ce804.async.js +0 -1
  194. package/static/dist/1147.32f41a88.async.js +0 -1
  195. package/static/dist/1352.ab6da08e.async.js +0 -1
  196. package/static/dist/1690.f0290540.async.js +0 -1
  197. package/static/dist/1742.6cbe5aca.async.js +0 -1
  198. package/static/dist/2208.8e3a7325.async.js +0 -1
  199. package/static/dist/3191.70bc19db.async.js +0 -1
  200. package/static/dist/5312.74b95311.async.js +0 -1
  201. package/static/dist/5691.931f59c5.async.js +0 -1
  202. package/static/dist/6159.55cb068a.async.js +0 -1
  203. package/static/dist/7025.f4080d63.async.js +0 -1
  204. package/static/dist/739.6be5552a.async.js +0 -1
  205. package/static/dist/7571.4f6240b1.async.js +0 -1
  206. package/static/dist/786.59fc381c.async.js +0 -1
  207. package/static/dist/8317.c44c1ebd.async.js +0 -1
  208. package/static/dist/8826.0291edfd.async.js +0 -1
  209. package/static/dist/955.3c9481f7.async.js +0 -1
  210. package/static/dist/layouts__index.1fce90e0.chunk.css +0 -1
  211. package/static/dist/layouts__index.8dcf1576.async.js +0 -1
  212. package/static/dist/preload_helper.116a62f6.js +0 -1
  213. package/static/dist/src__pages__crontab__detail.b07f0c0a.async.js +0 -1
  214. package/static/dist/src__pages__crontab__index.2e2e1096.async.js +0 -1
  215. package/static/dist/src__pages__crontab__modal.4d8c2a22.async.js +0 -1
  216. package/static/dist/src__pages__crontab__type.db7c1858.async.js +0 -1
  217. package/static/dist/src__pages__dependence__index.3d87ceac.async.js +0 -1
  218. package/static/dist/src__pages__dependence__modal.631ffb5b.async.js +0 -1
  219. package/static/dist/src__pages__env__editNameModal.ff85ef8c.async.js +0 -1
  220. package/static/dist/src__pages__env__index.a0a2fece.async.js +0 -1
  221. package/static/dist/src__pages__env__modal.d1004662.async.js +0 -1
  222. package/static/dist/src__pages__error__index.1bc3c90b.async.js +0 -1
  223. package/static/dist/src__pages__initialization__index.8b1cbaf9.async.js +0 -1
  224. package/static/dist/src__pages__log__index.cf00c9af.async.js +0 -1
  225. package/static/dist/src__pages__script__editNameModal.53424d49.async.js +0 -1
  226. package/static/dist/src__pages__script__index.e65df827.async.js +0 -1
  227. package/static/dist/src__pages__script__renameModal.4bbe7fb1.async.js +0 -1
  228. package/static/dist/src__pages__script__saveModal.cf449f3c.async.js +0 -1
  229. package/static/dist/src__pages__script__setting.b345d59a.async.js +0 -1
  230. package/static/dist/src__pages__setting__appModal.03faec89.async.js +0 -1
  231. package/static/dist/src__pages__setting__checkUpdate.46efe057.async.js +0 -1
  232. package/static/dist/src__pages__setting__dependence.4495c7b6.async.js +0 -1
  233. package/static/dist/src__pages__setting__index.6919c399.async.js +0 -1
  234. package/static/dist/src__pages__setting__notification.d6a3884f.async.js +0 -1
  235. package/static/dist/src__pages__setting__other.0d931d6f.async.js +0 -1
  236. package/static/dist/src__pages__setting__security.91cb545f.async.js +0 -1
  237. package/static/dist/src__pages__setting__systemLog.1d433a48.async.js +0 -1
  238. package/static/dist/src__pages__subscription__modal.0b84f6cf.async.js +0 -1
@@ -19,6 +19,7 @@ const typedi_1 = require("typedi");
19
19
  const winston_1 = __importDefault(require("winston"));
20
20
  const config_1 = __importDefault(require("../config"));
21
21
  const cron_1 = require("../data/cron");
22
+ const runningInstance_1 = require("../data/runningInstance");
22
23
  const child_process_1 = require("child_process");
23
24
  const promises_1 = __importDefault(require("fs/promises"));
24
25
  const cron_parser_1 = __importDefault(require("cron-parser"));
@@ -33,8 +34,10 @@ const dayjs_1 = __importDefault(require("dayjs"));
33
34
  const pickBy_1 = __importDefault(require("lodash/pickBy"));
34
35
  const omit_1 = __importDefault(require("lodash/omit"));
35
36
  const utils_1 = require("../shared/utils");
37
+ const i18n_1 = require("../shared/i18n");
36
38
  const schedule_1 = require("../interface/schedule");
37
39
  const logStreamManager_1 = require("../shared/logStreamManager");
40
+ const lodash_1 = require("lodash");
38
41
  let CronService = class CronService {
39
42
  constructor(logger) {
40
43
  this.logger = logger;
@@ -46,6 +49,26 @@ let CronService = class CronService {
46
49
  }
47
50
  return false;
48
51
  }
52
+ get schedulerMode() {
53
+ const env = process.env.QL_SCHEDULER;
54
+ if (env === 'system')
55
+ return 'system';
56
+ if (env === 'node')
57
+ return 'node';
58
+ try {
59
+ (0, child_process_1.execSync)('which crond', { stdio: 'ignore' });
60
+ return 'system';
61
+ }
62
+ catch (_a) {
63
+ return 'node';
64
+ }
65
+ }
66
+ shouldUseCronClient(cron) {
67
+ if (this.schedulerMode === 'node') {
68
+ return !this.isSpecialSchedule(cron.schedule);
69
+ }
70
+ return this.isNodeCron(cron) && !this.isSpecialSchedule(cron.schedule);
71
+ }
49
72
  isOnceSchedule(schedule) {
50
73
  return schedule === null || schedule === void 0 ? void 0 : schedule.startsWith(schedule_1.ScheduleType.ONCE);
51
74
  }
@@ -81,16 +104,25 @@ let CronService = class CronService {
81
104
  if ((0, util_1.isDemoEnv)()) {
82
105
  return doc;
83
106
  }
84
- if (!this.isSpecialSchedule(doc.schedule)) {
85
- await client_1.default.addCron([
86
- {
87
- name: doc.name || '',
88
- id: String(doc.id),
89
- schedule: doc.schedule,
90
- command: this.makeCommand(doc),
91
- extra_schedules: doc.extra_schedules || [],
92
- },
93
- ]);
107
+ if (this.shouldUseCronClient(doc)) {
108
+ try {
109
+ await client_1.default.addCron([
110
+ {
111
+ name: doc.name || '',
112
+ id: String(doc.id),
113
+ schedule: doc.schedule,
114
+ command: this.makeCommand(doc),
115
+ extra_schedules: doc.extra_schedules || [],
116
+ },
117
+ ]);
118
+ }
119
+ catch (error) {
120
+ // gRPC 注册失败时回滚 DB 记录,避免产生"僵尸任务"
121
+ // (DB 和 crontab.list 有记录但调度器永远不会执行)
122
+ await cron_1.CrontabModel.destroy({ where: { id: doc.id } });
123
+ this.logger.error('[crontab] Failed to register cron job in scheduler, task creation rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
124
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务创建已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
125
+ }
94
126
  }
95
127
  await this.setCrontab();
96
128
  return doc;
@@ -107,17 +139,46 @@ let CronService = class CronService {
107
139
  if (doc.isDisabled === 1 || (0, util_1.isDemoEnv)()) {
108
140
  return newDoc;
109
141
  }
110
- await client_1.default.delCron([String(newDoc.id)]);
111
- if (!this.isSpecialSchedule(newDoc.schedule)) {
112
- await client_1.default.addCron([
113
- {
114
- name: doc.name || '',
115
- id: String(newDoc.id),
116
- schedule: newDoc.schedule,
117
- command: this.makeCommand(newDoc),
118
- extra_schedules: newDoc.extra_schedules || [],
119
- },
120
- ]);
142
+ try {
143
+ await client_1.default.delCron([String(newDoc.id)]);
144
+ }
145
+ catch (error) {
146
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
147
+ }
148
+ if (this.shouldUseCronClient(newDoc)) {
149
+ try {
150
+ await client_1.default.addCron([
151
+ {
152
+ name: doc.name || '',
153
+ id: String(newDoc.id),
154
+ schedule: newDoc.schedule,
155
+ command: this.makeCommand(newDoc),
156
+ extra_schedules: newDoc.extra_schedules || [],
157
+ },
158
+ ]);
159
+ }
160
+ catch (error) {
161
+ // gRPC 注册新任务失败 → 回滚 DB 到旧数据,并尝试恢复旧调度注册
162
+ await cron_1.CrontabModel.update(doc, { where: { id: doc.id } });
163
+ if (this.shouldUseCronClient(doc)) {
164
+ try {
165
+ await client_1.default.addCron([
166
+ {
167
+ name: doc.name || '',
168
+ id: String(doc.id),
169
+ schedule: doc.schedule,
170
+ command: this.makeCommand(doc),
171
+ extra_schedules: doc.extra_schedules || [],
172
+ },
173
+ ]);
174
+ }
175
+ catch (_recoveryError) {
176
+ this.logger.warn('[crontab] Failed to restore old cron job in scheduler after rollback:', (_recoveryError === null || _recoveryError === void 0 ? void 0 : _recoveryError.message) || _recoveryError);
177
+ }
178
+ }
179
+ this.logger.error('[crontab] Failed to register updated cron job in scheduler, update rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
180
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务更新已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
181
+ }
121
182
  }
122
183
  await this.setCrontab();
123
184
  return newDoc;
@@ -126,7 +187,7 @@ let CronService = class CronService {
126
187
  await cron_1.CrontabModel.update(payload, { where: { id: payload.id } });
127
188
  return await this.getDb({ id: payload.id });
128
189
  }
129
- async status({ ids, status, pid, log_path, last_running_time = 0, last_execution_time = 0, }) {
190
+ async status({ ids, status, pid, log_path, last_running_time = 0, last_execution_time = 0, exit_code, }) {
130
191
  let options = {
131
192
  status,
132
193
  pid,
@@ -148,12 +209,46 @@ let CronService = class CronService {
148
209
  if (status === cron_1.CrontabStatus.idle && log_path !== cron.log_path) {
149
210
  options = (0, omit_1.default)(options, ['status', 'log_path', 'pid']);
150
211
  }
212
+ // Manage RunningInstance records for status transitions from shell scripts
213
+ if (status === cron_1.CrontabStatus.running) {
214
+ // Create a new running instance record
215
+ await runningInstance_1.RunningInstanceModel.create({
216
+ cron_id: id,
217
+ pid: pid || undefined,
218
+ log_path: log_path || undefined,
219
+ started_at: last_execution_time || (0, dayjs_1.default)().unix(),
220
+ status: runningInstance_1.InstanceStatus.running,
221
+ });
222
+ }
223
+ else if (status === cron_1.CrontabStatus.idle) {
224
+ // Mark the matching running instance as finished
225
+ const finishedAt = (0, dayjs_1.default)().unix();
226
+ const instanceStatus = exit_code !== undefined && exit_code !== null && exit_code !== 0
227
+ ? runningInstance_1.InstanceStatus.error
228
+ : runningInstance_1.InstanceStatus.finished;
229
+ await runningInstance_1.RunningInstanceModel.update({
230
+ finished_at: finishedAt,
231
+ status: instanceStatus,
232
+ exit_code: exit_code !== null && exit_code !== void 0 ? exit_code : undefined,
233
+ }, {
234
+ where: {
235
+ cron_id: id,
236
+ pid: pid || undefined,
237
+ status: runningInstance_1.InstanceStatus.running,
238
+ },
239
+ });
240
+ }
151
241
  await cron_1.CrontabModel.update(Object.assign({}, (0, pickBy_1.default)(options, (v) => v === 0 || !!v)), { where: { id } });
152
242
  }
153
243
  }
154
244
  async remove(ids) {
155
245
  await cron_1.CrontabModel.destroy({ where: { id: ids } });
156
- await client_1.default.delCron(ids.map(String));
246
+ try {
247
+ await client_1.default.delCron(ids.map(String));
248
+ }
249
+ catch (error) {
250
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
251
+ }
157
252
  await this.setCrontab();
158
253
  }
159
254
  async pin(ids) {
@@ -308,7 +403,7 @@ let CronService = class CronService {
308
403
  }
309
404
  }
310
405
  formatFilterQuery(query, filterQuery) {
311
- if (filterQuery) {
406
+ if (!(0, lodash_1.isEmpty)(filterQuery)) {
312
407
  if (!query[sequelize_1.Op.and]) {
313
408
  query[sequelize_1.Op.and] = [];
314
409
  }
@@ -418,8 +513,36 @@ let CronService = class CronService {
418
513
  this.logger.error(`[panel][停止任务失败] 任务ID: ${doc.id}, 错误: ${error}`);
419
514
  }
420
515
  }
516
+ // Mark all running instances as stopped
517
+ const finishedAt = (0, dayjs_1.default)().unix();
518
+ await runningInstance_1.RunningInstanceModel.update({ status: runningInstance_1.InstanceStatus.stopped, finished_at: finishedAt }, { where: { cron_id: ids, status: runningInstance_1.InstanceStatus.running } });
421
519
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id: ids } });
422
520
  }
521
+ async stopInstance(instanceId) {
522
+ const instance = await runningInstance_1.RunningInstanceModel.findOne({
523
+ where: { id: instanceId, status: runningInstance_1.InstanceStatus.running },
524
+ });
525
+ if (!instance) {
526
+ return { code: 400, message: (0, i18n_1.t)('实例不存在或已停止') };
527
+ }
528
+ if (instance.pid) {
529
+ try {
530
+ await (0, util_1.killTask)(instance.pid);
531
+ }
532
+ catch (error) {
533
+ this.logger.error(`[panel][停止实例失败] 实例ID: ${instanceId}, PID: ${instance.pid}, 错误: ${error}`);
534
+ }
535
+ }
536
+ await runningInstance_1.RunningInstanceModel.update({ status: runningInstance_1.InstanceStatus.stopped, finished_at: (0, dayjs_1.default)().unix(), exit_code: 143 }, { where: { id: instanceId } });
537
+ // Check if there are still other running instances for this cron
538
+ const otherRunning = await runningInstance_1.RunningInstanceModel.count({
539
+ where: { cron_id: instance.cron_id, status: runningInstance_1.InstanceStatus.running },
540
+ });
541
+ if (otherRunning === 0) {
542
+ await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id: instance.cron_id } });
543
+ }
544
+ return { code: 200, message: (0, i18n_1.t)('实例已停止') };
545
+ }
423
546
  async runSingle(cronId) {
424
547
  return pLimit_1.default.manualRunWithCronLimit(() => {
425
548
  return new Promise(async (resolve) => {
@@ -459,9 +582,7 @@ let CronService = class CronService {
459
582
  });
460
583
  cp.on('exit', async (code) => {
461
584
  this.logger.info('[panel][执行任务结束] 参数: %s, 退出码: %j', JSON.stringify(params), code);
462
- // Close the stream after task completion
463
585
  await logStreamManager_1.logStreamManager.closeStream(absolutePath);
464
- await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id } });
465
586
  resolve(Object.assign(Object.assign({}, params), { pid: cp.pid, code }));
466
587
  });
467
588
  });
@@ -469,13 +590,20 @@ let CronService = class CronService {
469
590
  }
470
591
  async disabled(ids) {
471
592
  await cron_1.CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
472
- await client_1.default.delCron(ids.map(String));
593
+ try {
594
+ await client_1.default.delCron(ids.map(String));
595
+ }
596
+ catch (error) {
597
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
598
+ }
473
599
  await this.setCrontab();
474
600
  }
475
601
  async enabled(ids) {
476
602
  await cron_1.CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
477
603
  const docs = await cron_1.CrontabModel.findAll({ where: { id: ids } });
478
- const crons = docs.map((doc) => ({
604
+ const crons = docs
605
+ .filter((x) => this.shouldUseCronClient(x))
606
+ .map((doc) => ({
479
607
  name: doc.name || '',
480
608
  id: String(doc.id),
481
609
  schedule: doc.schedule,
@@ -485,27 +613,38 @@ let CronService = class CronService {
485
613
  if ((0, util_1.isDemoEnv)()) {
486
614
  return;
487
615
  }
488
- await client_1.default.addCron(crons);
616
+ try {
617
+ await client_1.default.addCron(crons);
618
+ }
619
+ catch (error) {
620
+ // gRPC 注册失败 → 回滚启用状态,避免 DB 显示已启用但调度器未注册
621
+ await cron_1.CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
622
+ this.logger.error('[crontab] Failed to register cron job in scheduler, enable rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
623
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务启用已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
624
+ }
489
625
  await this.setCrontab();
490
626
  }
491
627
  async log(id) {
492
628
  const doc = await this.getDb({ id });
493
629
  if (!doc) {
494
- return '';
630
+ return { content: '', status: 'empty' };
495
631
  }
496
632
  if (doc.log_name === '/dev/null') {
497
- return '日志设置为忽略';
633
+ return { content: (0, i18n_1.t)('日志设置为忽略'), status: 'ignored' };
498
634
  }
499
635
  const absolutePath = path_1.default.resolve(config_1.default.logPath, `${doc.log_path}`);
500
636
  const logFileExist = doc.log_path && (await (0, util_1.fileExist)(absolutePath));
501
637
  if (logFileExist) {
502
- return await (0, util_1.getFileContentByName)(`${absolutePath}`);
638
+ const content = await (0, util_1.getFileContentByName)(`${absolutePath}`);
639
+ const isRunning = typeof doc.status === 'number' &&
640
+ [cron_1.CrontabStatus.running, cron_1.CrontabStatus.queued].includes(doc.status);
641
+ return { content, status: isRunning ? 'running' : 'completed' };
503
642
  }
504
643
  else {
505
644
  return typeof doc.status === 'number' &&
506
645
  [cron_1.CrontabStatus.queued, cron_1.CrontabStatus.running].includes(doc.status)
507
- ? '运行中...'
508
- : '日志不存在...';
646
+ ? { content: (0, i18n_1.t)('运行中...'), status: 'running' }
647
+ : { content: (0, i18n_1.t)('日志不存在...'), status: 'notFound' };
509
648
  }
510
649
  }
511
650
  async logs(id) {
@@ -550,6 +689,9 @@ let CronService = class CronService {
550
689
  .replace(/;? *\n/g, ';')
551
690
  .trim()}' `;
552
691
  }
692
+ if (tab.work_dir) {
693
+ commandVariable += `work_dir='${tab.work_dir.replace(/'/g, "'\\''")}' `;
694
+ }
553
695
  const crontab_job_string = `${commandVariable}${command}`;
554
696
  return crontab_job_string;
555
697
  }
@@ -574,6 +716,15 @@ let CronService = class CronService {
574
716
  }
575
717
  });
576
718
  await (0, utils_1.writeFileWithLock)(config_1.default.crontabFile, crontab_string);
719
+ if (this.schedulerMode === 'system') {
720
+ try {
721
+ (0, child_process_1.execSync)(`crontab ${config_1.default.crontabFile}`);
722
+ }
723
+ catch (error) {
724
+ const errorMsg = error.message || String(error);
725
+ this.logger.error('[crontab] Failed to update system crontab:', errorMsg);
726
+ }
727
+ }
577
728
  await cron_1.CrontabModel.update({ saved: true }, { where: {} });
578
729
  }
579
730
  importCrontab() {
@@ -611,7 +762,8 @@ let CronService = class CronService {
611
762
  async autosave_crontab() {
612
763
  const tabs = await this.crontabs();
613
764
  const regularCrons = tabs.data
614
- .filter((x) => x.isDisabled !== 1 && !this.isSpecialSchedule(x.schedule))
765
+ .filter((x) => x.isDisabled !== 1 &&
766
+ this.shouldUseCronClient(x))
615
767
  .map((doc) => ({
616
768
  name: doc.name || '',
617
769
  id: String(doc.id),
@@ -623,8 +775,16 @@ let CronService = class CronService {
623
775
  await (0, utils_1.writeFileWithLock)(config_1.default.crontabFile, '');
624
776
  return;
625
777
  }
626
- await client_1.default.addCron(regularCrons);
627
- this.setCrontab(tabs);
778
+ // 先同步 crontab.list 与系统 crontab,确保其始终反映数据库真实状态。
779
+ // gRPC 调度注册为尽力而为:失败时不阻断文件同步,调度器重启后会重新注册。
780
+ // 这避免了因调度器短暂不可用导致 crontab.list 与数据库脱节(订阅更新误判任务已存在)。
781
+ await this.setCrontab(tabs);
782
+ try {
783
+ await client_1.default.addCron(regularCrons);
784
+ }
785
+ catch (error) {
786
+ this.logger.warn('[crontab] Failed to register cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
787
+ }
628
788
  }
629
789
  async bootTask() {
630
790
  const tabs = await this.crontabs();
@@ -632,7 +792,7 @@ let CronService = class CronService {
632
792
  if (bootTasks.length > 0) {
633
793
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.queued }, { where: { id: bootTasks.map((t) => t.id) } });
634
794
  for (const task of bootTasks) {
635
- await this.runSingle(task.id);
795
+ this.runSingle(task.id);
636
796
  }
637
797
  }
638
798
  }
@@ -38,6 +38,7 @@ const dayjs_1 = __importDefault(require("dayjs"));
38
38
  const pLimit_1 = __importDefault(require("../shared/pLimit"));
39
39
  const util_2 = require("../config/util");
40
40
  const const_1 = require("../config/const");
41
+ const i18n_1 = require("../shared/i18n");
41
42
  let DependenceService = class DependenceService {
42
43
  constructor(logger, sockService) {
43
44
  this.logger = logger;
@@ -183,7 +184,7 @@ let DependenceService = class DependenceService {
183
184
  }
184
185
  const depIds = [dependency.id];
185
186
  let depName = dependency.name.trim();
186
- const actionText = isInstall ? '安装' : '删除';
187
+ const actionText = isInstall ? (0, i18n_1.t)('安装') : (0, i18n_1.t)('删除');
187
188
  const socketMessageType = isInstall
188
189
  ? 'installDependence'
189
190
  : 'uninstallDependence';
@@ -197,11 +198,12 @@ let DependenceService = class DependenceService {
197
198
  if (!osType) {
198
199
  await dependence_1.DependenceModel.update({ status: dependence_1.DependenceStatus.installFailed }, { where: { id: depIds } });
199
200
  const startTime = (0, dayjs_1.default)();
200
- const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n\n当前系统不支持\n\n依赖${actionText}失败,结束时间 ${startTime.format('YYYY-MM-DD HH:mm:ss')},耗时 ${startTime.diff(startTime, 'second')} 秒`;
201
+ const message = (0, i18n_1.tf)('开始%s依赖 %s,开始时间 %s\n\n当前系统不支持\n\n依赖%s失败,结束时间 %s,耗时 %s 秒', actionText, depName, startTime.format('YYYY-MM-DD HH:mm:ss'), actionText, startTime.format('YYYY-MM-DD HH:mm:ss'), String(startTime.diff(startTime, 'second')));
201
202
  this.sockService.sendMessage({
202
203
  type: socketMessageType,
203
204
  message,
204
205
  references: depIds,
206
+ status: dependence_1.DependenceStatus.installFailed,
205
207
  });
206
208
  this.updateLog(depIds, message);
207
209
  return resolve(null);
@@ -221,11 +223,12 @@ let DependenceService = class DependenceService {
221
223
  : `${linuxCommand.uninstall} ${depName.trim()}`;
222
224
  }
223
225
  const startTime = (0, dayjs_1.default)();
224
- const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n\n`;
226
+ const message = (0, i18n_1.tf)('开始%s依赖 %s,开始时间 %s\n\n', actionText, depName, startTime.format('YYYY-MM-DD HH:mm:ss'));
225
227
  this.sockService.sendMessage({
226
228
  type: socketMessageType,
227
229
  message,
228
230
  references: depIds,
231
+ status,
229
232
  });
230
233
  this.updateLog(depIds, message);
231
234
  // 判断是否已经安装过依赖
@@ -254,11 +257,12 @@ let DependenceService = class DependenceService {
254
257
  isPythonDependence) &&
255
258
  (!depVersion || depInfo.includes(depVersion))) {
256
259
  const endTime = (0, dayjs_1.default)();
257
- const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format('YYYY-MM-DD HH:mm:ss')},耗时 ${endTime.diff(startTime, 'second')} 秒`;
260
+ const _message = (0, i18n_1.tf)('检测到已经安装 %s\n\n%s\n\n跳过安装\n\n依赖%s成功,结束时间 %s,耗时 %s 秒', depName, depInfo, actionText, endTime.format('YYYY-MM-DD HH:mm:ss'), String(endTime.diff(startTime, 'second')));
258
261
  this.sockService.sendMessage({
259
262
  type: socketMessageType,
260
263
  message: _message,
261
264
  references: depIds,
265
+ status: dependence_1.DependenceStatus.installed,
262
266
  });
263
267
  this.updateLog(depIds, _message);
264
268
  await dependence_1.DependenceModel.update({ status: dependence_1.DependenceStatus.installed }, { where: { id: depIds } });
@@ -277,6 +281,7 @@ let DependenceService = class DependenceService {
277
281
  type: socketMessageType,
278
282
  message: data.toString(),
279
283
  references: depIds,
284
+ status,
280
285
  });
281
286
  this.updateLog(depIds, data.toString());
282
287
  });
@@ -285,6 +290,7 @@ let DependenceService = class DependenceService {
285
290
  type: socketMessageType,
286
291
  message: data.toString(),
287
292
  references: depIds,
293
+ status,
288
294
  });
289
295
  this.updateLog(depIds, data.toString());
290
296
  });
@@ -293,37 +299,32 @@ let DependenceService = class DependenceService {
293
299
  type: socketMessageType,
294
300
  message: JSON.stringify(err),
295
301
  references: depIds,
302
+ status,
296
303
  });
297
304
  this.updateLog(depIds, JSON.stringify(err));
298
305
  });
299
306
  cp.on('exit', async (code) => {
300
307
  const endTime = (0, dayjs_1.default)();
301
308
  const isSucceed = code === 0;
302
- const resultText = isSucceed ? '成功' : '失败';
303
- const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format('YYYY-MM-DD HH:mm:ss')},耗时 ${endTime.diff(startTime, 'second')} 秒`;
309
+ const resultText = isSucceed ? (0, i18n_1.t)('成功') : (0, i18n_1.t)('失败');
310
+ const message = '\n' +
311
+ (0, i18n_1.tf)('依赖%s%s,结束时间 %s,耗时 %s 秒', actionText, resultText, endTime.format('YYYY-MM-DD HH:mm:ss'), String(endTime.diff(startTime, 'second')));
312
+ const exitStatus = isSucceed
313
+ ? (isInstall ? dependence_1.DependenceStatus.installed : dependence_1.DependenceStatus.removed)
314
+ : (isInstall ? dependence_1.DependenceStatus.installFailed : dependence_1.DependenceStatus.removeFailed);
304
315
  this.sockService.sendMessage({
305
316
  type: socketMessageType,
306
317
  message,
307
318
  references: depIds,
319
+ status: exitStatus,
308
320
  });
309
321
  this.updateLog(depIds, message);
310
- let status;
311
- if (isSucceed) {
312
- status = isInstall
313
- ? dependence_1.DependenceStatus.installed
314
- : dependence_1.DependenceStatus.removed;
315
- }
316
- else {
317
- status = isInstall
318
- ? dependence_1.DependenceStatus.installFailed
319
- : dependence_1.DependenceStatus.removeFailed;
320
- }
321
322
  const docs = await dependence_1.DependenceModel.findAll({ where: { id: depIds } });
322
323
  const _docIds = docs
323
324
  .filter((x) => x.status !== dependence_1.DependenceStatus.cancelled)
324
325
  .map((x) => x.id);
325
326
  if (_docIds.length > 0) {
326
- await dependence_1.DependenceModel.update({ status }, { where: { id: _docIds } });
327
+ await dependence_1.DependenceModel.update({ status: exitStatus }, { where: { id: _docIds } });
327
328
  }
328
329
  // 如果删除依赖成功或者强制删除
329
330
  if ((isSucceed || force) && !isInstall) {
@@ -33,7 +33,7 @@ let EnvService = class EnvService {
33
33
  if (envs &&
34
34
  envs.length > 0 &&
35
35
  typeof envs[envs.length - 1].position === 'number') {
36
- position = envs[envs.length - 1].position;
36
+ position = this.getPrecisionPosition(envs[envs.length - 1].position);
37
37
  }
38
38
  const tabs = payloads.map((x) => {
39
39
  position = position - env_1.stepPosition;
@@ -90,7 +90,7 @@ let EnvService = class EnvService {
90
90
  return newDoc;
91
91
  }
92
92
  async checkPosition(position, edge = 0) {
93
- const precisionPosition = parseFloat(position.toPrecision(16));
93
+ const precisionPosition = this.getPrecisionPosition(position);
94
94
  if (precisionPosition < env_1.minPosition ||
95
95
  precisionPosition > env_1.maxPosition ||
96
96
  Math.abs(precisionPosition - edge) < env_1.minPosition) {
@@ -103,7 +103,7 @@ let EnvService = class EnvService {
103
103
  }
104
104
  }
105
105
  getPrecisionPosition(position) {
106
- return parseFloat(position.toPrecision(16));
106
+ return Math.trunc(parseFloat(position.toPrecision(16)));
107
107
  }
108
108
  async envs(searchText = '', query = {}) {
109
109
  let condition = Object.assign({}, query);
@@ -171,6 +171,34 @@ let EnvService = class EnvService {
171
171
  async unPin(ids) {
172
172
  await env_1.EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
173
173
  }
174
+ async addLabels(ids, labels) {
175
+ await data_1.sequelize.transaction(async (transaction) => {
176
+ const docs = await env_1.EnvModel.findAll({
177
+ where: { id: ids },
178
+ transaction,
179
+ });
180
+ for (const doc of docs) {
181
+ const env = doc.get({ plain: true });
182
+ await env_1.EnvModel.update({ labels: Array.from(new Set([...(env.labels || []), ...labels])) }, { where: { id: env.id }, transaction });
183
+ }
184
+ });
185
+ return await this.find({ id: ids });
186
+ }
187
+ async removeLabels(ids, labels) {
188
+ await data_1.sequelize.transaction(async (transaction) => {
189
+ const docs = await env_1.EnvModel.findAll({
190
+ where: { id: ids },
191
+ transaction,
192
+ });
193
+ for (const doc of docs) {
194
+ const env = doc.get({ plain: true });
195
+ await env_1.EnvModel.update({
196
+ labels: (env.labels || []).filter((label) => !labels.includes(label)),
197
+ }, { where: { id: env.id }, transaction });
198
+ }
199
+ });
200
+ return await this.find({ id: ids });
201
+ }
174
202
  async set_envs() {
175
203
  const envs = await this.envs('', {
176
204
  name: { [sequelize_1.Op.not]: null },
@@ -46,23 +46,49 @@ const util_1 = require("util");
46
46
  const config_1 = __importDefault(require("../config"));
47
47
  const metrics_1 = require("./metrics");
48
48
  const typedi_1 = require("typedi");
49
+ const grpcCerts_1 = require("../config/grpcCerts");
49
50
  let GrpcServerService = class GrpcServerService {
50
51
  constructor() {
51
52
  this.server = new grpc_js_1.Server({ 'grpc.enable_http_proxy': 0 });
52
53
  }
54
+ formatGrpcAddress(host, port) {
55
+ if (host === '::') {
56
+ return `[::]:${port}`;
57
+ }
58
+ return `${host}:${port}`;
59
+ }
53
60
  async initialize() {
54
61
  try {
55
62
  this.server.addService(health_1.HealthService, { check: health_2.check });
56
63
  this.server.addService(cron_1.CronService, { addCron: addCron_1.addCron, delCron: delCron_1.delCron });
57
64
  this.server.addService(api_1.ApiService, Api);
65
+ const tlsConfig = await (0, grpcCerts_1.initGrpcCerts)();
66
+ const credentials = grpc_js_1.ServerCredentials.createSsl(Buffer.from(tlsConfig.caCert), [{ cert_chain: Buffer.from(tlsConfig.serverCert), private_key: Buffer.from(tlsConfig.serverKey) }], true);
58
67
  const grpcPort = config_1.default.grpcPort;
68
+ const hostsToTry = [
69
+ config_1.default.bindHostGrpc,
70
+ ...(config_1.default.bindHostGrpc !== '0.0.0.0' ? ['0.0.0.0'] : [])
71
+ ];
59
72
  const bindAsync = (0, util_1.promisify)(this.server.bindAsync).bind(this.server);
60
- await bindAsync(`0.0.0.0:${grpcPort}`, grpc_js_1.ServerCredentials.createInsecure());
61
- logger_1.default.debug(`✌️ gRPC service started successfully`);
62
- metrics_1.metricsService.record('grpc_service_start', 1, {
63
- port: grpcPort.toString(),
64
- });
65
- return grpcPort;
73
+ let lastError = null;
74
+ for (const host of hostsToTry) {
75
+ try {
76
+ const address = this.formatGrpcAddress(host, grpcPort);
77
+ await bindAsync(address, credentials);
78
+ logger_1.default.debug(`[boot] gRPC service started successfully on ${address}`);
79
+ metrics_1.metricsService.record('grpc_service_start', 1, {
80
+ port: grpcPort.toString(),
81
+ host
82
+ });
83
+ return grpcPort;
84
+ }
85
+ catch (err) {
86
+ lastError = err;
87
+ logger_1.default.warn(`Failed to bind gRPC on ${host}:${grpcPort}, trying next...`, err);
88
+ }
89
+ }
90
+ logger_1.default.error('Failed to start gRPC service on all hosts');
91
+ throw lastError || new Error('Failed to start gRPC service');
66
92
  }
67
93
  catch (err) {
68
94
  logger_1.default.error('Failed to start gRPC service:', err);