@cartanova/qgrid-cli 2.6.10 → 2.6.11
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/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +137 -60
- package/bundle/dist/application/qgrid/qgrid.frame.js +5 -2
- package/bundle/dist/application/setting/setting.model.js +65 -3
- package/bundle/dist/application/setting/setting.types.js +13 -3
- package/bundle/dist/i18n/en.js +3 -1
- package/bundle/dist/i18n/ko.js +3 -1
- package/bundle/dist/utils/process-supervisor.js +22 -0
- package/bundle/dist/utils/server-restart.js +61 -0
- package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +7 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +113 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +172 -75
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +45 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -0
- package/bundle/src/application/setting/setting.model.test.ts +104 -0
- package/bundle/src/application/setting/setting.model.ts +75 -3
- package/bundle/src/application/setting/setting.types.ts +12 -0
- package/bundle/src/application/sonamu.generated.http +7 -0
- package/bundle/src/i18n/en.ts +3 -0
- package/bundle/src/i18n/ko.ts +3 -0
- package/bundle/src/server-error-handler.test.ts +15 -0
- package/bundle/src/utils/process-supervisor.test.ts +24 -0
- package/bundle/src/utils/process-supervisor.ts +17 -0
- package/bundle/src/utils/server-restart.test.ts +76 -0
- package/bundle/src/utils/server-restart.ts +68 -0
- package/bundle/web-dist/client/assets/index-CFvFRDk_.js +78 -0
- package/bundle/web-dist/client/assets/index-CJ9iTI_V.css +1 -0
- package/bundle/web-dist/client/assets/{logs-BI5K8oe4.js → logs-DeAK5Uyu.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-4Y-Razet.js → monit-Rj94Sz3_.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CkBXB-V4.js → routes-DVseTo6T.js} +1 -1
- package/bundle/web-dist/client/assets/{sd.generated-Bwa7rses.js → sd.generated-W6XqgWK6.js} +8 -8
- package/bundle/web-dist/client/assets/services.generated-Dz5pA5Fb.js +1 -0
- package/bundle/web-dist/client/assets/settings-DvuRDFfS.js +1 -0
- package/bundle/web-dist/client/assets/{show-CGRchJj7.js → show-C7etBj89.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-Bx5mQnoJ.js → tokens-BroPe7Tn.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Bz87RV5v.js → use-oauth-login-flow-CuLvs_CW.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Ws7I6fL-.js → lazyRouteComponent-C-N6sd5N.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-C1rxLh8H.js → logs-CVkeSr1S.js} +3 -3
- package/bundle/web-dist/server/assets/{monit-C8gx9Sar.js → monit-Bb6QfT9V.js} +3 -3
- package/bundle/web-dist/server/assets/{plus-BISXNqRU.js → plus-B8JRuCb9.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-D7duWOwf.js → react-dom-ByF9tqTB.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-ZW_Ih5Em.js → routes-Dnm-Eti6.js} +4 -4
- package/bundle/web-dist/server/assets/{sd.generated-LO-h1_16.js → sd.generated-CuyajYtc.js} +126 -116
- package/bundle/web-dist/server/assets/{services.generated-CZIChgHV.js → services.generated-BAGLi2r8.js} +9 -1
- package/bundle/web-dist/server/assets/{settings--aoatERH.js → settings-4T3nBfY8.js} +271 -39
- package/bundle/web-dist/server/assets/{show-BY27w9oi.js → show-CQw03iTP.js} +3 -3
- package/bundle/web-dist/server/assets/{tokens-DIObtTuZ.js → tokens-Di_SGPYj.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-V3k8PSqQ.js → use-oauth-login-flow-DiYCMNAQ.js} +2 -2
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/dist/cli.js +93 -38
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +34 -3
- package/bundle/web-dist/client/assets/index-BRRNJRgu.js +0 -78
- package/bundle/web-dist/client/assets/index-rdbxJinT.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +0 -1
- package/bundle/web-dist/client/assets/settings-B62_r1i4.js +0 -1
package/bundle/dist/i18n/ko.js
CHANGED
|
@@ -48,8 +48,10 @@ var ko_default = {
|
|
|
48
48
|
"error.serviceUnavailable": "서비스를 사용할 수 없습니다",
|
|
49
49
|
"setting.invalidValue": (reason) => `설정 값이 올바르지 않습니다: ${reason}`,
|
|
50
50
|
"setting.unknownKey": (key) => `알 수 없는 설정 키 ${key}`,
|
|
51
|
+
"setting.noSupervisor": () => "로컬 실행 환경에서는 재시작할 수 없습니다 — 종료하면 다시 켜줄 모니터링 도구가 없습니다",
|
|
51
52
|
"qgrid.dispatcherFailed": (provider) => `${provider} dispatcher 초기화에 실패했습니다`,
|
|
52
53
|
"qgrid.dispatcherStarting": (provider) => `${provider} dispatcher 기동 중입니다. 잠시 후 다시 시도해주세요`,
|
|
54
|
+
"qgrid.restartPending": () => "서버 재시작 중입니다. 잠시 후 다시 시도해주세요",
|
|
53
55
|
"error.unauthorized": "인증이 필요합니다",
|
|
54
56
|
"error.unknownSearchField": (field) => `구현되지 않은 검색 필드 ${field}`,
|
|
55
57
|
notFound: (name, id) => `존재하지 않는 ${name} ID ${id}`,
|
|
@@ -62,4 +64,4 @@ var ko_default = {
|
|
|
62
64
|
//#endregion
|
|
63
65
|
export { ko_default as default };
|
|
64
66
|
|
|
65
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia28uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2kxOG4va28udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgam9zYSB9IGZyb20gXCJzb25hbXUvZGljdFwiO1xuXG4vKipcbiAqIFByb2plY3QgS08gRGljdGlvbmFyeVxuICovXG5leHBvcnQgZGVmYXVsdCB7XG4gIFwiY29tbW9uLmFsbFwiOiBcIuyghOyytFwiLFxuICBcImNvbW1vbi5iYWNrVG9MaXN0XCI6IFwi66qp66Gd7Jy866GcXCIsXG4gIFwiY29tbW9uLmNhbmNlbFwiOiBcIuy3qOyGjFwiLFxuICBcImNvbW1vbi5jbG9zZVwiOiBcIuuLq+q4sFwiLFxuICBcImNvbW1vbi5jb25maXJtXCI6IFwi7ZmV7J24XCIsXG4gIFwiY29tbW9uLmNyZWF0ZVwiOiBcIuyDneyEsVwiLFxuICBcImNvbW1vbi5jcmVhdGVkQXRcIjogXCLrk7HroZ1cIixcbiAgXCJjb21tb24uZGVsZXRlXCI6IFwi7IKt7KCcXCIsXG4gIFwiY29tbW9uLmVkaXRcIjogXCLsiJjsoJVcIixcbiAgXCJjb21tb24ubG9naW5cIjogXCLroZzqt7jsnbhcIixcbiAgXCJjb21tb24ubG9nb3V0XCI6IFwi66Gc6re47JWE7JuDXCIsXG4gIFwiY29tbW9uLm1hbmFnZVwiOiBcIuq0gOumrFwiLFxuICBcImNvbW1vbi5yZXN1bHRzXCI6IChjb3VudDogbnVtYmVyKSA9PiBgJHtjb3VudH3qsJwg6rKw6rO8YCxcbiAgXCJjb21tb24uc2F2ZVwiOiBcIuyggOyepVwiLFxuICBcImNvbW1vbi5zZWFyY2hcIjogXCLqsoDsg4lcIixcbiAgXCJjb21tb24uc2VhcmNoUGxhY2Vob2xkZXJcIjogXCLqsoDsg4kuLi5cIixcbiAgXCJjb21tb24uc2VhcmNoVHlwZVwiOiBcIuqygOyDiSDsnKDtmJVcIixcbiAgXCJjb21tb24uc29ydFwiOiBcIuygleugrFwiLFxuICBcImNvbmZpcm0uZGVsZXRlXCI6IFwi7KCV66eQIOyCreygnO2VmOyLnOqyoOyKteuLiOq5jD9cIixcbiAgXCJjb25maXJtLnNhdmVcIjogXCLsoIDsnqXtlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZGFzaGJvYXJkLmFkbWluTWVudVwiOiBcIuq0gOumrCDrqZTribRcIixcbiAgXCJkYXNoYm9hcmQuY3JlYXRlZEF0XCI6IFwi6rCA7J6F7J28XCIsXG4gIFwiZGFzaGJvYXJkLmVtYWlsXCI6IFwi7J2066mU7J28XCIsXG4gIFwiZGFzaGJvYXJkLmxvZ2luUmVxdWlyZWRcIjogXCLroZzqt7jsnbjsnbQg7ZWE7JqU7ZWp64uI64ukLlwiLFxuICBcImRhc2hib2FyZC5uYW1lXCI6IFwi7J2066aEXCIsXG4gIFwiZGFzaGJvYXJkLnJvbGVcIjogXCLsl63tlaBcIixcbiAgXCJkYXNoYm9hcmQudGl0bGVcIjogXCLqtIDrpqzsnpAg64yA7Iuc67O065OcXCIsXG4gIFwiZGFzaGJvYXJkLndlbGNvbWVcIjogXCLtmZjsmIHtlanri4jri6QhXCIsXG4gIFwiZGVsZXRlLmNvbmZpcm0uZGVzY3JpcHRpb25cIjogXCLsnbQg7J6R7JeF7J2AIOy3qOyGjO2VoCDsiJgg7JeG7Iq164uI64ukLiDtla3rqqnsnbQg7JiB6rWs7KCB7Jy866GcIOyCreygnOuQqeuLiOuLpC5cIixcbiAgXCJkZWxldGUuY29uZmlybS50aXRsZVwiOiBcIuygleunkCDsgq3soJztlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZW50aXR5LmNyZWF0ZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDsg53shLFgLFxuICBcImVudGl0eS5lZGl0XCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIpID0+IGAke25hbWV9IOyImOyglSAoIyR7aWR9KWAsXG4gIFwiZW50aXR5Lmxpc3RcIjogKG5hbWU6IHN0cmluZykgPT4gYCR7bmFtZX0g66qp66GdYCxcbiAgXCJlbnRpdHkubGlzdE1hbmFnZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDrqqnroZ0g6rSA66asYCxcbiAgXCJlcnJvci5hbHJlYWR5UHJvY2Vzc2VkXCI6IFwi7J2066+4IOyymOumrOuQmOyXiOyKteuLiOuLpFwiLFxuICBcImVycm9yLmJhZFJlcXVlc3RcIjogXCLsnpjrqrvrkJwg7JqU7LKt7J6F64uI64ukXCIsXG4gIFwiZXJyb3IuZHVwbGljYXRlUm93XCI6IFwi7KSR67O165CcIOuNsOydtO2EsOyeheuLiOuLpFwiLFxuICBcImVycm9yLmVudGl0eU5vdEZvdW5kXCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIgfCBzdHJpbmcpID0+IGDsobTsnqztlZjsp4Ag7JWK64qUICR7bmFtZX0gSUQgJHtpZH1gLFxuICBcImVycm9yLmZvcmJpZGRlblwiOiBcIuq2jO2VnOydtCDsl4bsirXri4jri6RcIixcbiAgXCJlcnJvci5pbnRlcm5hbFNlcnZlckVycm9yXCI6IFwi7ISc67KEIOyYpOulmOqwgCDrsJzsg53tlojsirXri4jri6RcIixcbiAgXCJlcnJvci5ub3RGb3VuZFwiOiBcIuywvuydhCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwiZXJyb3Iuc2VydmljZVVuYXZhaWxhYmxlXCI6IFwi7ISc67mE7Iqk66W8IOyCrOyaqe2VoCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwic2V0dGluZy5pbnZhbGlkVmFsdWVcIjogKHJlYXNvbjogc3RyaW5nKSA9PiBg7ISk7KCVIOqwkuydtCDsmKzrsJTrpbTsp4Ag7JWK7Iq164uI64ukOiAke3JlYXNvbn1gLFxuICBcInNldHRpbmcudW5rbm93bktleVwiOiAoa2V5OiBzdHJpbmcpID0+
|
|
67
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia28uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2kxOG4va28udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgam9zYSB9IGZyb20gXCJzb25hbXUvZGljdFwiO1xuXG4vKipcbiAqIFByb2plY3QgS08gRGljdGlvbmFyeVxuICovXG5leHBvcnQgZGVmYXVsdCB7XG4gIFwiY29tbW9uLmFsbFwiOiBcIuyghOyytFwiLFxuICBcImNvbW1vbi5iYWNrVG9MaXN0XCI6IFwi66qp66Gd7Jy866GcXCIsXG4gIFwiY29tbW9uLmNhbmNlbFwiOiBcIuy3qOyGjFwiLFxuICBcImNvbW1vbi5jbG9zZVwiOiBcIuuLq+q4sFwiLFxuICBcImNvbW1vbi5jb25maXJtXCI6IFwi7ZmV7J24XCIsXG4gIFwiY29tbW9uLmNyZWF0ZVwiOiBcIuyDneyEsVwiLFxuICBcImNvbW1vbi5jcmVhdGVkQXRcIjogXCLrk7HroZ1cIixcbiAgXCJjb21tb24uZGVsZXRlXCI6IFwi7IKt7KCcXCIsXG4gIFwiY29tbW9uLmVkaXRcIjogXCLsiJjsoJVcIixcbiAgXCJjb21tb24ubG9naW5cIjogXCLroZzqt7jsnbhcIixcbiAgXCJjb21tb24ubG9nb3V0XCI6IFwi66Gc6re47JWE7JuDXCIsXG4gIFwiY29tbW9uLm1hbmFnZVwiOiBcIuq0gOumrFwiLFxuICBcImNvbW1vbi5yZXN1bHRzXCI6IChjb3VudDogbnVtYmVyKSA9PiBgJHtjb3VudH3qsJwg6rKw6rO8YCxcbiAgXCJjb21tb24uc2F2ZVwiOiBcIuyggOyepVwiLFxuICBcImNvbW1vbi5zZWFyY2hcIjogXCLqsoDsg4lcIixcbiAgXCJjb21tb24uc2VhcmNoUGxhY2Vob2xkZXJcIjogXCLqsoDsg4kuLi5cIixcbiAgXCJjb21tb24uc2VhcmNoVHlwZVwiOiBcIuqygOyDiSDsnKDtmJVcIixcbiAgXCJjb21tb24uc29ydFwiOiBcIuygleugrFwiLFxuICBcImNvbmZpcm0uZGVsZXRlXCI6IFwi7KCV66eQIOyCreygnO2VmOyLnOqyoOyKteuLiOq5jD9cIixcbiAgXCJjb25maXJtLnNhdmVcIjogXCLsoIDsnqXtlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZGFzaGJvYXJkLmFkbWluTWVudVwiOiBcIuq0gOumrCDrqZTribRcIixcbiAgXCJkYXNoYm9hcmQuY3JlYXRlZEF0XCI6IFwi6rCA7J6F7J28XCIsXG4gIFwiZGFzaGJvYXJkLmVtYWlsXCI6IFwi7J2066mU7J28XCIsXG4gIFwiZGFzaGJvYXJkLmxvZ2luUmVxdWlyZWRcIjogXCLroZzqt7jsnbjsnbQg7ZWE7JqU7ZWp64uI64ukLlwiLFxuICBcImRhc2hib2FyZC5uYW1lXCI6IFwi7J2066aEXCIsXG4gIFwiZGFzaGJvYXJkLnJvbGVcIjogXCLsl63tlaBcIixcbiAgXCJkYXNoYm9hcmQudGl0bGVcIjogXCLqtIDrpqzsnpAg64yA7Iuc67O065OcXCIsXG4gIFwiZGFzaGJvYXJkLndlbGNvbWVcIjogXCLtmZjsmIHtlanri4jri6QhXCIsXG4gIFwiZGVsZXRlLmNvbmZpcm0uZGVzY3JpcHRpb25cIjogXCLsnbQg7J6R7JeF7J2AIOy3qOyGjO2VoCDsiJgg7JeG7Iq164uI64ukLiDtla3rqqnsnbQg7JiB6rWs7KCB7Jy866GcIOyCreygnOuQqeuLiOuLpC5cIixcbiAgXCJkZWxldGUuY29uZmlybS50aXRsZVwiOiBcIuygleunkCDsgq3soJztlZjsi5zqsqDsirXri4jquYw/XCIsXG4gIFwiZW50aXR5LmNyZWF0ZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDsg53shLFgLFxuICBcImVudGl0eS5lZGl0XCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIpID0+IGAke25hbWV9IOyImOyglSAoIyR7aWR9KWAsXG4gIFwiZW50aXR5Lmxpc3RcIjogKG5hbWU6IHN0cmluZykgPT4gYCR7bmFtZX0g66qp66GdYCxcbiAgXCJlbnRpdHkubGlzdE1hbmFnZVwiOiAobmFtZTogc3RyaW5nKSA9PiBgJHtuYW1lfSDrqqnroZ0g6rSA66asYCxcbiAgXCJlcnJvci5hbHJlYWR5UHJvY2Vzc2VkXCI6IFwi7J2066+4IOyymOumrOuQmOyXiOyKteuLiOuLpFwiLFxuICBcImVycm9yLmJhZFJlcXVlc3RcIjogXCLsnpjrqrvrkJwg7JqU7LKt7J6F64uI64ukXCIsXG4gIFwiZXJyb3IuZHVwbGljYXRlUm93XCI6IFwi7KSR67O165CcIOuNsOydtO2EsOyeheuLiOuLpFwiLFxuICBcImVycm9yLmVudGl0eU5vdEZvdW5kXCI6IChuYW1lOiBzdHJpbmcsIGlkOiBudW1iZXIgfCBzdHJpbmcpID0+IGDsobTsnqztlZjsp4Ag7JWK64qUICR7bmFtZX0gSUQgJHtpZH1gLFxuICBcImVycm9yLmZvcmJpZGRlblwiOiBcIuq2jO2VnOydtCDsl4bsirXri4jri6RcIixcbiAgXCJlcnJvci5pbnRlcm5hbFNlcnZlckVycm9yXCI6IFwi7ISc67KEIOyYpOulmOqwgCDrsJzsg53tlojsirXri4jri6RcIixcbiAgXCJlcnJvci5ub3RGb3VuZFwiOiBcIuywvuydhCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwiZXJyb3Iuc2VydmljZVVuYXZhaWxhYmxlXCI6IFwi7ISc67mE7Iqk66W8IOyCrOyaqe2VoCDsiJgg7JeG7Iq164uI64ukXCIsXG4gIFwic2V0dGluZy5pbnZhbGlkVmFsdWVcIjogKHJlYXNvbjogc3RyaW5nKSA9PiBg7ISk7KCVIOqwkuydtCDsmKzrsJTrpbTsp4Ag7JWK7Iq164uI64ukOiAke3JlYXNvbn1gLFxuICBcInNldHRpbmcudW5rbm93bktleVwiOiAoa2V5OiBzdHJpbmcpID0+IGDslYwg7IiYIOyXhuuKlCDshKTsoJUg7YKkICR7a2V5fWAsXG4gIFwic2V0dGluZy5ub1N1cGVydmlzb3JcIjogKCkgPT5cbiAgICBcIuuhnOy7rCDsi6Ttlokg7ZmY6rK97JeQ7ISc64qUIOyerOyLnOyeke2VoCDsiJgg7JeG7Iq164uI64ukIOKAlCDsooXro4ztlZjrqbQg64uk7IucIOy8nOykhCDrqqjri4jthLDrp4Eg64+E6rWs6rCAIOyXhuyKteuLiOuLpFwiLFxuICBcInFncmlkLmRpc3BhdGNoZXJGYWlsZWRcIjogKHByb3ZpZGVyOiBzdHJpbmcpID0+IGAke3Byb3ZpZGVyfSBkaXNwYXRjaGVyIOy0iOq4sO2ZlOyXkCDsi6TtjKjtlojsirXri4jri6RgLFxuICBcInFncmlkLmRpc3BhdGNoZXJTdGFydGluZ1wiOiAocHJvdmlkZXI6IHN0cmluZykgPT5cbiAgICBgJHtwcm92aWRlcn0gZGlzcGF0Y2hlciDquLDrj5kg7KSR7J6F64uI64ukLiDsnqDsi5wg7ZuEIOuLpOyLnCDsi5zrj4TtlbTso7zshLjsmpRgLFxuICBcInFncmlkLnJlc3RhcnRQZW5kaW5nXCI6ICgpID0+IFwi7ISc67KEIOyerOyLnOyekSDspJHsnoXri4jri6QuIOyeoOyLnCDtm4Qg64uk7IucIOyLnOuPhO2VtOyjvOyEuOyalFwiLFxuICBcImVycm9yLnVuYXV0aG9yaXplZFwiOiBcIuyduOymneydtCDtlYTsmpTtlanri4jri6RcIixcbiAgXCJlcnJvci51bmtub3duU2VhcmNoRmllbGRcIjogKGZpZWxkOiBzdHJpbmcpID0+IGDqtaztmITrkJjsp4Ag7JWK7J2AIOqygOyDiSDtlYTrk5wgJHtmaWVsZH1gLFxuICBub3RGb3VuZDogKG5hbWU6IHN0cmluZywgaWQ6IG51bWJlcikgPT4gYOyhtOyerO2VmOyngCDslYrripQgJHtuYW1lfSBJRCAke2lkfWAsXG4gIFwidmFsaWRhdGlvbi5lbWFpbFwiOiBcIuyYrOuwlOuluCDsnbTrqZTsnbwg7ZiV7Iud7J20IOyVhOuLmeuLiOuLpFwiLFxuICBcInZhbGlkYXRpb24ubWF4TGVuZ3RoXCI6IChmaWVsZDogc3RyaW5nLCBtYXg6IG51bWJlcikgPT5cbiAgICBgJHtmaWVsZH0ke2pvc2EoZmllbGQsIFwi7J2A64qUXCIpfSDstZzrjIAgJHttYXh97J6Q6rmM7KeAIOyeheugpe2VoCDsiJgg7J6I7Iq164uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLm1pbkxlbmd0aFwiOiAoZmllbGQ6IHN0cmluZywgbWluOiBudW1iZXIpID0+XG4gICAgYCR7ZmllbGR9JHtqb3NhKGZpZWxkLCBcIuydgOuKlFwiKX0g7LWc7IaMICR7bWlufeyekCDsnbTsg4HsnbTslrTslbwg7ZWp64uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLnJlcXVpcmVkXCI6IChmaWVsZDogc3RyaW5nKSA9PiBgJHtqb3NhKGZpZWxkLCBcIuydgOuKlFwiKX0g7ZWE7IiY7J6F64uI64ukYCxcbiAgXCJ2YWxpZGF0aW9uLnVybFwiOiBcIuyYrOuwlOuluCBVUkwg7ZiV7Iud7J20IOyVhOuLmeuLiOuLpFwiLFxufSBhcyBjb25zdDtcbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFLQSxJQUFBLGFBQWU7Q0FDYixjQUFjO0NBQ2QscUJBQXFCO0NBQ3JCLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsa0JBQWtCO0NBQ2xCLGlCQUFpQjtDQUNqQixvQkFBb0I7Q0FDcEIsaUJBQWlCO0NBQ2pCLGVBQWU7Q0FDZixnQkFBZ0I7Q0FDaEIsaUJBQWlCO0NBQ2pCLGlCQUFpQjtDQUNqQixtQkFBbUIsVUFBa0IsR0FBRyxNQUFNO0NBQzlDLGVBQWU7Q0FDZixpQkFBaUI7Q0FDakIsNEJBQTRCO0NBQzVCLHFCQUFxQjtDQUNyQixlQUFlO0NBQ2Ysa0JBQWtCO0NBQ2xCLGdCQUFnQjtDQUNoQix1QkFBdUI7Q0FDdkIsdUJBQXVCO0NBQ3ZCLG1CQUFtQjtDQUNuQiwyQkFBMkI7Q0FDM0Isa0JBQWtCO0NBQ2xCLGtCQUFrQjtDQUNsQixtQkFBbUI7Q0FDbkIscUJBQXFCO0NBQ3JCLDhCQUE4QjtDQUM5Qix3QkFBd0I7Q0FDeEIsa0JBQWtCLFNBQWlCLEdBQUcsS0FBSztDQUMzQyxnQkFBZ0IsTUFBYyxPQUFlLEdBQUcsS0FBSyxRQUFRLEdBQUc7Q0FDaEUsZ0JBQWdCLFNBQWlCLEdBQUcsS0FBSztDQUN6QyxzQkFBc0IsU0FBaUIsR0FBRyxLQUFLO0NBQy9DLDBCQUEwQjtDQUMxQixvQkFBb0I7Q0FDcEIsc0JBQXNCO0NBQ3RCLHlCQUF5QixNQUFjLE9BQXdCLFdBQVcsS0FBSyxNQUFNO0NBQ3JGLG1CQUFtQjtDQUNuQiw2QkFBNkI7Q0FDN0Isa0JBQWtCO0NBQ2xCLDRCQUE0QjtDQUM1Qix5QkFBeUIsV0FBbUIsb0JBQW9CO0NBQ2hFLHVCQUF1QixRQUFnQixlQUFlO0NBQ3RELDhCQUNFO0NBQ0YsMkJBQTJCLGFBQXFCLEdBQUcsU0FBUztDQUM1RCw2QkFBNkIsYUFDM0IsR0FBRyxTQUFTO0NBQ2QsOEJBQThCO0NBQzlCLHNCQUFzQjtDQUN0Qiw2QkFBNkIsVUFBa0IsaUJBQWlCO0NBQ2hFLFdBQVcsTUFBYyxPQUFlLFdBQVcsS0FBSyxNQUFNO0NBQzlELG9CQUFvQjtDQUNwQix5QkFBeUIsT0FBZSxRQUN0QyxHQUFHLFFBQVEsS0FBSyxPQUFPLElBQUksRUFBRSxNQUFNLElBQUk7Q0FDekMseUJBQXlCLE9BQWUsUUFDdEMsR0FBRyxRQUFRLEtBQUssT0FBTyxJQUFJLEVBQUUsTUFBTSxJQUFJO0NBQ3pDLHdCQUF3QixVQUFrQixHQUFHLEtBQUssT0FBTyxJQUFJLEVBQUU7Q0FDL0Qsa0JBQWtCO0FBQ3BCIn0=
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/utils/process-supervisor.ts
|
|
2
|
+
/**
|
|
3
|
+
* 프로세스 모니터링 도구 감지.
|
|
4
|
+
*
|
|
5
|
+
* 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
|
|
6
|
+
* 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
|
|
7
|
+
* 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
|
|
8
|
+
*
|
|
9
|
+
* 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
|
|
10
|
+
* unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
|
|
11
|
+
*
|
|
12
|
+
* 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
|
|
13
|
+
* 판별하면 놓친다.
|
|
14
|
+
*/
|
|
15
|
+
function detectSupervisor(env = process.env) {
|
|
16
|
+
if (env.pm_id !== void 0) return "pm2";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { detectSupervisor };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1zdXBlcnZpc29yLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9wcm9jZXNzLXN1cGVydmlzb3IudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDtlITroZzshLjsiqQg66qo64uI7YSw66eBIOuPhOq1rCDqsJDsp4AuXG4gKlxuICog7ZmU66m07J2YIFwi7J6s7Iuc7J6RXCLsnYAg7Iqk7Iqk66Gc66W8IOyiheujjO2VmOuKlCDqsoPsnbTri6Qg4oCUIHBtMiDqsJnsnYAg64+E6rWs6rCAIOyjveydgCDtlITroZzshLjsiqTrpbwg64uk7IucXG4gKiDrnYTsm4zso7zripQg6rKD7J20IOqzpyDsnqzsi5zsnpHsnbTri6QuIOq3uOuemOyEnCDqt7jrn7Ag64+E6rWsIOyXhuydtCDsooXro4ztlZjrqbQg6re464OlIOyEnOuyhOqwgCDrgrTroKTqsITri6QuXG4gKiDroZzsu6wg6rCc67Cc7JeQ7IScIOuyhO2KvOydhCDriITrpbTrqbQg7JWE66y064+EIOyCtOugpOyjvOyngCDslYrsnLzrr4DroZwsIOyiheujjCDsoITsl5Ag67CY65Oc7IucIO2ZleyduO2VnOuLpC5cbiAqXG4gKiDtmITsnqwg67Cw7Y+sIOqzhOyVveydgCBwbTIg66eMIOyerOyLnOyekeydhCDrs7TsnqXtlZzri6QuIHN5c3RlbWQg7J2YIGBJTlZPQ0FUSU9OX0lEYCDripQg7ZW064u5XG4gKiB1bml0IOydtCBgUmVzdGFydD1gIOulvCDsk7Tri6TripQg65y77J20IOyVhOuLiOuvgOuhnCDslYjsoITtlZwg6rCQ64+F7J6QIOyLoO2YuOuhnCDsgqzsmqntlaAg7IiYIOyXhuuLpC5cbiAqXG4gKiDqsJLsnbQg7JWE64uI6528IOyhtOyerCDsl6zrtoDqsIAg7Iug7Zi464ukLiBwbTIg64qUIOyyqyDtlITroZzshLjsiqTsl5AgYHBtX2lkPTBgIOydhCDso7zrr4DroZwgdHJ1dGh5IOqygOyCrOuhnFxuICog7YyQ67OE7ZWY66m0IOuGk+y5nOuLpC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGRldGVjdFN1cGVydmlzb3IoZW52OiBOb2RlSlMuUHJvY2Vzc0VudiA9IHByb2Nlc3MuZW52KTogXCJwbTJcIiB8IG51bGwge1xuICBpZiAoZW52LnBtX2lkICE9PSB1bmRlZmluZWQpIHJldHVybiBcInBtMlwiO1xuICByZXR1cm4gbnVsbDtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7QUFhQSxTQUFnQixpQkFBaUIsTUFBeUIsUUFBUSxLQUFtQjtDQUNuRixJQUFJLElBQUksVUFBVSxRQUFXLE9BQU87Q0FDcEMsT0FBTztBQUNUIn0=
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/utils/server-restart.ts
|
|
2
|
+
const RESTART_EXIT_FALLBACK_MS = 5e3;
|
|
3
|
+
let restartPending = false;
|
|
4
|
+
let exitTriggered = false;
|
|
5
|
+
let exitArmed = false;
|
|
6
|
+
let fallbackTimer;
|
|
7
|
+
let armedReplyRaw;
|
|
8
|
+
let armedExit;
|
|
9
|
+
/** 재시작 요청 뒤에는 새 native provider 실행을 받지 않는다. */
|
|
10
|
+
function isRestartPending() {
|
|
11
|
+
return restartPending;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 프로세스 전체에서 최초 요청만 재시작을 시작한다. 종료 신호는 active run 정합성 처리가
|
|
15
|
+
* 끝난 뒤 별도로 arm 한다. 그래야 이미 닫힌 연결이나 fallback 타이머가 DB 정리보다 앞서
|
|
16
|
+
* 프로세스를 끝내지 않는다.
|
|
17
|
+
*/
|
|
18
|
+
function beginServerRestart() {
|
|
19
|
+
if (restartPending) return false;
|
|
20
|
+
restartPending = true;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function clearExitArm() {
|
|
24
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
25
|
+
fallbackTimer = void 0;
|
|
26
|
+
if (armedReplyRaw && armedExit) {
|
|
27
|
+
armedReplyRaw.removeListener("finish", armedExit);
|
|
28
|
+
armedReplyRaw.removeListener("close", armedExit);
|
|
29
|
+
}
|
|
30
|
+
armedReplyRaw = void 0;
|
|
31
|
+
armedExit = void 0;
|
|
32
|
+
}
|
|
33
|
+
/** 정합성 처리가 끝난 뒤 HTTP 응답 flush/연결 종료/유한 fallback 중 먼저 온 신호로 종료한다. */
|
|
34
|
+
function armServerRestartExit(replyRaw) {
|
|
35
|
+
if (!restartPending || exitArmed) return;
|
|
36
|
+
exitArmed = true;
|
|
37
|
+
const exitOnce = () => {
|
|
38
|
+
if (exitTriggered) return;
|
|
39
|
+
exitTriggered = true;
|
|
40
|
+
clearExitArm();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
armedReplyRaw = replyRaw;
|
|
44
|
+
armedExit = exitOnce;
|
|
45
|
+
replyRaw.once("finish", exitOnce);
|
|
46
|
+
replyRaw.once("close", exitOnce);
|
|
47
|
+
fallbackTimer = setTimeout(exitOnce, RESTART_EXIT_FALLBACK_MS);
|
|
48
|
+
fallbackTimer.unref();
|
|
49
|
+
if (replyRaw.destroyed && !replyRaw.writableEnded) exitOnce();
|
|
50
|
+
}
|
|
51
|
+
/** 모듈 전역 상태를 격리해야 하는 단위 테스트 전용. */
|
|
52
|
+
function resetServerRestartForTests() {
|
|
53
|
+
clearExitArm();
|
|
54
|
+
restartPending = false;
|
|
55
|
+
exitTriggered = false;
|
|
56
|
+
exitArmed = false;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { armServerRestartExit, beginServerRestart, isRestartPending, resetServerRestartForTests };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXJlc3RhcnQuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3NlcnZlci1yZXN0YXJ0LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHR5cGUgU2VydmVyUmVzcG9uc2UgfSBmcm9tIFwibm9kZTpodHRwXCI7XG5cbmNvbnN0IFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyA9IDVfMDAwO1xuXG5sZXQgcmVzdGFydFBlbmRpbmcgPSBmYWxzZTtcbmxldCBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG5sZXQgZXhpdEFybWVkID0gZmFsc2U7XG5sZXQgZmFsbGJhY2tUaW1lcjogTm9kZUpTLlRpbWVvdXQgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRSZXBseVJhdzogUmVwbHlSYXcgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRFeGl0OiAoKCkgPT4gdm9pZCkgfCB1bmRlZmluZWQ7XG5cbnR5cGUgUmVwbHlSYXcgPSBQaWNrPFNlcnZlclJlc3BvbnNlLCBcIm9uY2VcIiB8IFwicmVtb3ZlTGlzdGVuZXJcIiB8IFwiZGVzdHJveWVkXCIgfCBcIndyaXRhYmxlRW5kZWRcIj47XG5cbi8qKiDsnqzsi5zsnpEg7JqU7LKtIOuSpOyXkOuKlCDsg4ggbmF0aXZlIHByb3ZpZGVyIOyLpO2WieydhCDrsJvsp4Ag7JWK64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUmVzdGFydFBlbmRpbmcoKTogYm9vbGVhbiB7XG4gIHJldHVybiByZXN0YXJ0UGVuZGluZztcbn1cblxuLyoqXG4gKiDtlITroZzshLjsiqQg7KCE7LK07JeQ7IScIOy1nOy0iCDsmpTssq3rp4wg7J6s7Iuc7J6R7J2EIOyLnOyeke2VnOuLpC4g7KKF66OMIOyLoO2YuOuKlCBhY3RpdmUgcnVuIOygle2VqeyEsSDsspjrpqzqsIBcbiAqIOuBneuCnCDrkqQg67OE64+E66GcIGFybSDtlZzri6QuIOq3uOuemOyVvCDsnbTrr7gg64ur7Z6MIOyXsOqysOydtOuCmCBmYWxsYmFjayDtg4DsnbTrqLjqsIAgREIg7KCV66as67O064ukIOyVnuyEnFxuICog7ZSE66Gc7IS47Iqk66W8IOuBneuCtOyngCDslYrripTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBiZWdpblNlcnZlclJlc3RhcnQoKTogYm9vbGVhbiB7XG4gIGlmIChyZXN0YXJ0UGVuZGluZykgcmV0dXJuIGZhbHNlO1xuICByZXN0YXJ0UGVuZGluZyA9IHRydWU7XG4gIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBjbGVhckV4aXRBcm0oKTogdm9pZCB7XG4gIGlmIChmYWxsYmFja1RpbWVyKSBjbGVhclRpbWVvdXQoZmFsbGJhY2tUaW1lcik7XG4gIGZhbGxiYWNrVGltZXIgPSB1bmRlZmluZWQ7XG4gIGlmIChhcm1lZFJlcGx5UmF3ICYmIGFybWVkRXhpdCkge1xuICAgIGFybWVkUmVwbHlSYXcucmVtb3ZlTGlzdGVuZXIoXCJmaW5pc2hcIiwgYXJtZWRFeGl0KTtcbiAgICBhcm1lZFJlcGx5UmF3LnJlbW92ZUxpc3RlbmVyKFwiY2xvc2VcIiwgYXJtZWRFeGl0KTtcbiAgfVxuICBhcm1lZFJlcGx5UmF3ID0gdW5kZWZpbmVkO1xuICBhcm1lZEV4aXQgPSB1bmRlZmluZWQ7XG59XG5cbi8qKiDsoJXtlanshLEg7LKY66as6rCAIOuBneuCnCDrkqQgSFRUUCDsnZHri7UgZmx1c2gv7Jew6rKwIOyiheujjC/snKDtlZwgZmFsbGJhY2sg7KSRIOuovOyggCDsmKgg7Iug7Zi466GcIOyiheujjO2VnOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhcm1TZXJ2ZXJSZXN0YXJ0RXhpdChyZXBseVJhdzogUmVwbHlSYXcpOiB2b2lkIHtcbiAgaWYgKCFyZXN0YXJ0UGVuZGluZyB8fCBleGl0QXJtZWQpIHJldHVybjtcbiAgZXhpdEFybWVkID0gdHJ1ZTtcblxuICBjb25zdCBleGl0T25jZSA9ICgpID0+IHtcbiAgICBpZiAoZXhpdFRyaWdnZXJlZCkgcmV0dXJuO1xuICAgIGV4aXRUcmlnZ2VyZWQgPSB0cnVlO1xuICAgIGNsZWFyRXhpdEFybSgpO1xuICAgIHByb2Nlc3MuZXhpdCgwKTtcbiAgfTtcblxuICBhcm1lZFJlcGx5UmF3ID0gcmVwbHlSYXc7XG4gIGFybWVkRXhpdCA9IGV4aXRPbmNlO1xuICByZXBseVJhdy5vbmNlKFwiZmluaXNoXCIsIGV4aXRPbmNlKTtcbiAgcmVwbHlSYXcub25jZShcImNsb3NlXCIsIGV4aXRPbmNlKTtcbiAgZmFsbGJhY2tUaW1lciA9IHNldFRpbWVvdXQoZXhpdE9uY2UsIFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyk7XG4gIGZhbGxiYWNrVGltZXIudW5yZWYoKTtcbiAgaWYgKHJlcGx5UmF3LmRlc3Ryb3llZCAmJiAhcmVwbHlSYXcud3JpdGFibGVFbmRlZCkgZXhpdE9uY2UoKTtcbn1cblxuLyoqIOuqqOuTiCDsoITsl60g7IOB7YOc66W8IOqyqeumrO2VtOyVvCDtlZjripQg64uo7JyEIO2FjOyKpO2KuCDsoITsmqkuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzZXRTZXJ2ZXJSZXN0YXJ0Rm9yVGVzdHMoKTogdm9pZCB7XG4gIGNsZWFyRXhpdEFybSgpO1xuICByZXN0YXJ0UGVuZGluZyA9IGZhbHNlO1xuICBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG4gIGV4aXRBcm1lZCA9IGZhbHNlO1xufVxuIl0sIm1hcHBpbmdzIjoiO0FBRUEsTUFBTSwyQkFBMkI7QUFFakMsSUFBSSxpQkFBaUI7QUFDckIsSUFBSSxnQkFBZ0I7QUFDcEIsSUFBSSxZQUFZO0FBQ2hCLElBQUk7QUFDSixJQUFJO0FBQ0osSUFBSTs7QUFLSixTQUFnQixtQkFBNEI7Q0FDMUMsT0FBTztBQUNUOzs7Ozs7QUFPQSxTQUFnQixxQkFBOEI7Q0FDNUMsSUFBSSxnQkFBZ0IsT0FBTztDQUMzQixpQkFBaUI7Q0FDakIsT0FBTztBQUNUO0FBRUEsU0FBUyxlQUFxQjtDQUM1QixJQUFJLGVBQWUsYUFBYSxhQUFhO0NBQzdDLGdCQUFnQjtDQUNoQixJQUFJLGlCQUFpQixXQUFXO0VBQzlCLGNBQWMsZUFBZSxVQUFVLFNBQVM7RUFDaEQsY0FBYyxlQUFlLFNBQVMsU0FBUztDQUNqRDtDQUNBLGdCQUFnQjtDQUNoQixZQUFZO0FBQ2Q7O0FBR0EsU0FBZ0IscUJBQXFCLFVBQTBCO0NBQzdELElBQUksQ0FBQyxrQkFBa0IsV0FBVztDQUNsQyxZQUFZO0NBRVosTUFBTSxpQkFBaUI7RUFDckIsSUFBSSxlQUFlO0VBQ25CLGdCQUFnQjtFQUNoQixhQUFhO0VBQ2IsUUFBUSxLQUFLLENBQUM7Q0FDaEI7Q0FFQSxnQkFBZ0I7Q0FDaEIsWUFBWTtDQUNaLFNBQVMsS0FBSyxVQUFVLFFBQVE7Q0FDaEMsU0FBUyxLQUFLLFNBQVMsUUFBUTtDQUMvQixnQkFBZ0IsV0FBVyxVQUFVLHdCQUF3QjtDQUM3RCxjQUFjLE1BQU07Q0FDcEIsSUFBSSxTQUFTLGFBQWEsQ0FBQyxTQUFTLGVBQWUsU0FBUztBQUM5RDs7QUFHQSxTQUFnQiw2QkFBbUM7Q0FDakQsYUFBYTtDQUNiLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsWUFBWTtBQUNkIn0=
|
|
@@ -52,6 +52,13 @@ vi.mock("../token/token.model", () => ({
|
|
|
52
52
|
},
|
|
53
53
|
}));
|
|
54
54
|
|
|
55
|
+
// OAuth 리다이렉트 테스트는 token-death를 검증하지 않는다. 부분 TokenModel mock이
|
|
56
|
+
// isolate:false 워커의 token-death 전용 테스트로 새지 않게 실제 모듈 로드를 막는다.
|
|
57
|
+
vi.mock("./token-death", () => ({
|
|
58
|
+
deactivateAuthDeadToken: vi.fn(),
|
|
59
|
+
notifyTokenAdded: vi.fn(),
|
|
60
|
+
}));
|
|
61
|
+
|
|
55
62
|
function authUrlParam(authUrl: string, key: string): string | null {
|
|
56
63
|
return new URL(authUrl).searchParams.get(key);
|
|
57
64
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
beginServerRestart,
|
|
5
|
+
resetServerRestartForTests,
|
|
6
|
+
} from "../../utils/server-restart";
|
|
3
7
|
import {
|
|
4
8
|
afterQuery,
|
|
5
9
|
beforeQuery,
|
|
10
|
+
finishActiveNativeRunsForRestart,
|
|
6
11
|
finishRunAborted,
|
|
7
12
|
finishRunWithError,
|
|
13
|
+
resetNativeRunRegistryForTests,
|
|
8
14
|
} from "./qgrid-run-lifecycle";
|
|
9
15
|
import { type QueryOutput } from "./qgrid.types";
|
|
10
16
|
|
|
@@ -58,6 +64,11 @@ function queryOutput(overrides: Partial<QueryOutput> = {}): QueryOutput {
|
|
|
58
64
|
|
|
59
65
|
let lifecycleNow = Date.now();
|
|
60
66
|
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
resetServerRestartForTests();
|
|
69
|
+
resetNativeRunRegistryForTests();
|
|
70
|
+
});
|
|
71
|
+
|
|
61
72
|
describe("qgrid run lifecycle start", () => {
|
|
62
73
|
beforeEach(() => {
|
|
63
74
|
lifecycleNow += 60_001;
|
|
@@ -188,6 +199,108 @@ describe("qgrid run lifecycle start", () => {
|
|
|
188
199
|
expect(expireStaleToolWaitingRunsMock).toHaveBeenCalledTimes(1);
|
|
189
200
|
expect(createRunMock).toHaveBeenCalledTimes(2);
|
|
190
201
|
});
|
|
202
|
+
|
|
203
|
+
it("rejects new native runs while a restart is pending", async () => {
|
|
204
|
+
beginServerRestart();
|
|
205
|
+
|
|
206
|
+
await expect(beforeQuery({ prompt: "hi" })).rejects.toMatchObject({ statusCode: 503 });
|
|
207
|
+
expect(createRunMock).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("finishes a run prepared during the restart race before rejecting provider execution", async () => {
|
|
211
|
+
let resolveCreate!: (requestLogId: number) => void;
|
|
212
|
+
createRunMock.mockReturnValueOnce(
|
|
213
|
+
new Promise<number>((resolve) => {
|
|
214
|
+
resolveCreate = resolve;
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const starting = beforeQuery({ prompt: "hi" });
|
|
219
|
+
await vi.waitFor(() => expect(createRunMock).toHaveBeenCalledOnce());
|
|
220
|
+
beginServerRestart();
|
|
221
|
+
resolveCreate(22);
|
|
222
|
+
|
|
223
|
+
await expect(starting).rejects.toMatchObject({ statusCode: 503 });
|
|
224
|
+
expect(finishRunMock).toHaveBeenCalledWith(
|
|
225
|
+
22,
|
|
226
|
+
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("marks only active native runs as restarted and prevents a late success overwrite", async () => {
|
|
231
|
+
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
|
|
232
|
+
|
|
233
|
+
await finishActiveNativeRunsForRestart();
|
|
234
|
+
expect(finishRunMock).toHaveBeenCalledWith(
|
|
235
|
+
requestLogId,
|
|
236
|
+
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
appendStepMock.mockClear();
|
|
240
|
+
finishRunMock.mockClear();
|
|
241
|
+
await expect(afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput())).resolves.toEqual(
|
|
242
|
+
{},
|
|
243
|
+
);
|
|
244
|
+
expect(appendStepMock).not.toHaveBeenCalled();
|
|
245
|
+
expect(finishRunMock).not.toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("waits for an in-flight finalizer and writes the restart error last", async () => {
|
|
249
|
+
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "hi" });
|
|
250
|
+
let releaseAppend!: () => void;
|
|
251
|
+
appendStepMock.mockReturnValueOnce(
|
|
252
|
+
new Promise<void>((resolve) => {
|
|
253
|
+
releaseAppend = resolve;
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const finishing = afterQuery(requestLogId, stepIndex, { prompt: "hi" }, queryOutput());
|
|
258
|
+
await vi.waitFor(() => expect(appendStepMock).toHaveBeenCalledOnce());
|
|
259
|
+
const restarting = finishActiveNativeRunsForRestart();
|
|
260
|
+
|
|
261
|
+
await Promise.resolve();
|
|
262
|
+
expect(finishRunMock).not.toHaveBeenCalledWith(
|
|
263
|
+
requestLogId,
|
|
264
|
+
expect.objectContaining({ error_message: "server restarted" }),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
releaseAppend();
|
|
268
|
+
await Promise.all([finishing, restarting]);
|
|
269
|
+
|
|
270
|
+
const terminalStatuses = finishRunMock.mock.calls
|
|
271
|
+
.filter(([id]) => id === requestLogId)
|
|
272
|
+
.map(([, payload]) => payload.status);
|
|
273
|
+
expect(terminalStatuses.at(-1)).toBe("error");
|
|
274
|
+
expect(finishRunMock).toHaveBeenLastCalledWith(
|
|
275
|
+
requestLogId,
|
|
276
|
+
expect.objectContaining({ status: "error", error_message: "server restarted" }),
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("leaves tool-wait runs alone after their provider turn has completed", async () => {
|
|
281
|
+
const { requestLogId, stepIndex } = await beforeQuery({ prompt: "use a tool" });
|
|
282
|
+
await afterQuery(
|
|
283
|
+
requestLogId,
|
|
284
|
+
stepIndex,
|
|
285
|
+
{ prompt: "use a tool" },
|
|
286
|
+
queryOutput({
|
|
287
|
+
finishReason: "tool-calls",
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "tool-call",
|
|
291
|
+
toolCallId: "call-1",
|
|
292
|
+
toolName: "lookup",
|
|
293
|
+
input: "{}",
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
finishRunMock.mockClear();
|
|
299
|
+
|
|
300
|
+
await finishActiveNativeRunsForRestart();
|
|
301
|
+
|
|
302
|
+
expect(finishRunMock).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
191
304
|
});
|
|
192
305
|
|
|
193
306
|
describe("qgrid run lifecycle TTFT", () => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { ServiceUnavailableException } from "sonamu";
|
|
2
3
|
|
|
4
|
+
import { SD } from "../../i18n/sd.generated";
|
|
5
|
+
import { isRestartPending } from "../../utils/server-restart";
|
|
3
6
|
import { RequestLogModel } from "../request-log/request-log.model";
|
|
4
7
|
import {
|
|
5
8
|
estimateImageGenerationCostMicroUsd,
|
|
@@ -17,6 +20,43 @@ const STALE_RUN_THRESHOLD_MS = 30 * 60 * 1000;
|
|
|
17
20
|
const STALE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
18
21
|
let staleCleanupInFlight: Promise<void> | undefined;
|
|
19
22
|
let lastStaleCleanupStartedAt = 0;
|
|
23
|
+
const activeNativeRunRefs = new Map<number, number>();
|
|
24
|
+
const interruptedNativeRunIds = new Set<number>();
|
|
25
|
+
const activeNativeFinalizers = new Map<number, Set<Promise<void>>>();
|
|
26
|
+
|
|
27
|
+
function restartUnavailable(): ServiceUnavailableException {
|
|
28
|
+
return new ServiceUnavailableException(SD("qgrid.restartPending")());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function registerNativeRun(requestLogId: number): void {
|
|
32
|
+
activeNativeRunRefs.set(requestLogId, (activeNativeRunRefs.get(requestLogId) ?? 0) + 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function releaseNativeRun(requestLogId: number): void {
|
|
36
|
+
const refs = activeNativeRunRefs.get(requestLogId);
|
|
37
|
+
if (refs === undefined || refs <= 1) activeNativeRunRefs.delete(requestLogId);
|
|
38
|
+
else activeNativeRunRefs.set(requestLogId, refs - 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trackNativeFinalizer<T>(requestLogId: number, finalize: () => Promise<T>): Promise<T> {
|
|
42
|
+
const operation = finalize();
|
|
43
|
+
const settled = operation.then(
|
|
44
|
+
() => undefined,
|
|
45
|
+
() => undefined,
|
|
46
|
+
);
|
|
47
|
+
const finalizers = activeNativeFinalizers.get(requestLogId) ?? new Set<Promise<void>>();
|
|
48
|
+
finalizers.add(settled);
|
|
49
|
+
activeNativeFinalizers.set(requestLogId, finalizers);
|
|
50
|
+
void settled.then(() => {
|
|
51
|
+
finalizers.delete(settled);
|
|
52
|
+
if (finalizers.size === 0) activeNativeFinalizers.delete(requestLogId);
|
|
53
|
+
});
|
|
54
|
+
return operation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertNativeRunAdmission(): void {
|
|
58
|
+
if (isRestartPending()) throw restartUnavailable();
|
|
59
|
+
}
|
|
20
60
|
|
|
21
61
|
function withProviderPrefix(
|
|
22
62
|
routeModel: string | undefined,
|
|
@@ -61,6 +101,8 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
61
101
|
requestLogId: number;
|
|
62
102
|
stepIndex: number;
|
|
63
103
|
}> {
|
|
104
|
+
assertNativeRunAdmission();
|
|
105
|
+
|
|
64
106
|
let requestLogId: number;
|
|
65
107
|
let stepIndex = 0;
|
|
66
108
|
|
|
@@ -88,6 +130,14 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
88
130
|
});
|
|
89
131
|
}
|
|
90
132
|
|
|
133
|
+
// DB 준비 중에 재시작이 시작됐다면 provider 실행으로 넘어가지 않는다.
|
|
134
|
+
// 이 요청은 재시작 snapshot에 없었으므로 방금 준비한 run을 직접 마감한다.
|
|
135
|
+
if (isRestartPending()) {
|
|
136
|
+
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
137
|
+
throw restartUnavailable();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
registerNativeRun(requestLogId);
|
|
91
141
|
return { requestLogId, stepIndex };
|
|
92
142
|
}
|
|
93
143
|
|
|
@@ -97,87 +147,97 @@ export async function afterQuery(
|
|
|
97
147
|
args: QueryInput,
|
|
98
148
|
result: QueryOutput,
|
|
99
149
|
): Promise<RunLifecycleResult> {
|
|
100
|
-
|
|
101
|
-
const requestedModelName = withProviderPrefix(
|
|
102
|
-
args.model,
|
|
103
|
-
args.model ?? result.requestedModel ?? result.model,
|
|
104
|
-
);
|
|
105
|
-
const servedModelName = withProviderPrefix(args.model, result.model);
|
|
106
|
-
|
|
107
|
-
// generate step 기록
|
|
108
|
-
await RequestLogModel.appendStep(requestLogId, {
|
|
109
|
-
step_index: stepIndex,
|
|
110
|
-
type: "generate",
|
|
111
|
-
model_name: servedModelName,
|
|
112
|
-
requested_model_name: requestedModelName,
|
|
113
|
-
fallback_count: result.modelFallbacks?.length ?? 0,
|
|
114
|
-
input_tokens: result.usage.input_tokens,
|
|
115
|
-
output_tokens: result.usage.output_tokens,
|
|
116
|
-
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
117
|
-
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
118
|
-
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
119
|
-
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
120
|
-
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
121
|
-
cost_source: result.costSource,
|
|
122
|
-
duration_ms: result.durationMs,
|
|
123
|
-
ttft_ms: result.ttftMs,
|
|
124
|
-
finish_reason: result.finishReason,
|
|
125
|
-
});
|
|
150
|
+
if (interruptedNativeRunIds.has(requestLogId)) return {};
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
return trackNativeFinalizer(requestLogId, async () => {
|
|
153
|
+
try {
|
|
154
|
+
// provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
|
|
155
|
+
const requestedModelName = withProviderPrefix(
|
|
156
|
+
args.model,
|
|
157
|
+
args.model ?? result.requestedModel ?? result.model,
|
|
158
|
+
);
|
|
159
|
+
const servedModelName = withProviderPrefix(args.model, result.model);
|
|
135
160
|
|
|
136
|
-
|
|
137
|
-
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
138
|
-
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
139
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
140
|
-
const tc = toolCalls[i]!;
|
|
141
|
-
if (tc.type !== "tool-call") continue;
|
|
161
|
+
// generate step 기록
|
|
142
162
|
await RequestLogModel.appendStep(requestLogId, {
|
|
143
163
|
step_index: stepIndex,
|
|
144
|
-
type: "
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
type: "generate",
|
|
165
|
+
model_name: servedModelName,
|
|
166
|
+
requested_model_name: requestedModelName,
|
|
167
|
+
fallback_count: result.modelFallbacks?.length ?? 0,
|
|
168
|
+
input_tokens: result.usage.input_tokens,
|
|
169
|
+
output_tokens: result.usage.output_tokens,
|
|
170
|
+
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
171
|
+
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
172
|
+
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
173
|
+
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
174
|
+
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
175
|
+
cost_source: result.costSource,
|
|
176
|
+
duration_ms: result.durationMs,
|
|
177
|
+
ttft_ms: result.ttftMs,
|
|
178
|
+
finish_reason: result.finishReason,
|
|
149
179
|
});
|
|
150
|
-
}
|
|
151
180
|
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
const imageParts = getImageParts(result);
|
|
182
|
+
const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
|
|
183
|
+
result,
|
|
184
|
+
args.imageGenerationOptions,
|
|
185
|
+
);
|
|
186
|
+
for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
|
|
187
|
+
await RequestLogModel.appendStep(requestLogId, step);
|
|
188
|
+
}
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
if (result.finishReason === "tool-calls") {
|
|
191
|
+
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
192
|
+
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
193
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
194
|
+
const tc = toolCalls[i]!;
|
|
195
|
+
if (tc.type !== "tool-call") continue;
|
|
196
|
+
await RequestLogModel.appendStep(requestLogId, {
|
|
197
|
+
step_index: stepIndex,
|
|
198
|
+
type: "tool_call",
|
|
199
|
+
tool_call_index: imageParts.length + i,
|
|
200
|
+
tool_call_id: tc.toolCallId,
|
|
201
|
+
tool_name: tc.toolName,
|
|
202
|
+
tool_args: tc.input,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { runContext: { requestLogId } };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// finishReason === "stop" → run 종료 (전체 step usage 합산)
|
|
210
|
+
const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
211
|
+
await RequestLogModel.finishRun(requestLogId, {
|
|
212
|
+
status: "succeeded",
|
|
213
|
+
response: formatResponseForLog(result),
|
|
214
|
+
token_name: result.tokenName,
|
|
215
|
+
input_tokens: agg.input_tokens,
|
|
216
|
+
output_tokens: agg.output_tokens,
|
|
217
|
+
cache_read_tokens: agg.cache_read_tokens,
|
|
218
|
+
cache_creation_tokens: agg.cache_creation_tokens,
|
|
219
|
+
cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
|
|
220
|
+
cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
|
|
221
|
+
duration_ms: agg.duration_ms,
|
|
222
|
+
// multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
|
|
223
|
+
requested_model_name: requestedModelName,
|
|
224
|
+
model_name: servedModelName,
|
|
225
|
+
fallback_count: agg.fallback_count,
|
|
226
|
+
cost_usd: agg.cost_usd,
|
|
227
|
+
cost_source: agg.cost_source,
|
|
228
|
+
image_cost_usd: imageCostMicroUsd,
|
|
229
|
+
image_cost_method:
|
|
230
|
+
imageCostMicroUsd !== null
|
|
231
|
+
? imageGenerationCostMethod(args.imageGenerationOptions)
|
|
232
|
+
: null,
|
|
233
|
+
history: filterHistoryForStorage(args.history),
|
|
234
|
+
});
|
|
179
235
|
|
|
180
|
-
|
|
236
|
+
return {};
|
|
237
|
+
} finally {
|
|
238
|
+
releaseNativeRun(requestLogId);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
181
241
|
}
|
|
182
242
|
|
|
183
243
|
export async function finishRunWithError(
|
|
@@ -185,11 +245,48 @@ export async function finishRunWithError(
|
|
|
185
245
|
errorMessage: string,
|
|
186
246
|
args?: QueryInput,
|
|
187
247
|
): Promise<void> {
|
|
188
|
-
|
|
248
|
+
if (interruptedNativeRunIds.has(requestLogId)) return;
|
|
249
|
+
return trackNativeFinalizer(requestLogId, async () => {
|
|
250
|
+
try {
|
|
251
|
+
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
252
|
+
} finally {
|
|
253
|
+
releaseNativeRun(requestLogId);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
189
256
|
}
|
|
190
257
|
|
|
191
258
|
export async function finishRunAborted(requestLogId: number, args?: QueryInput): Promise<void> {
|
|
192
|
-
|
|
259
|
+
if (interruptedNativeRunIds.has(requestLogId)) return;
|
|
260
|
+
return trackNativeFinalizer(requestLogId, async () => {
|
|
261
|
+
try {
|
|
262
|
+
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
|
|
263
|
+
} finally {
|
|
264
|
+
releaseNativeRun(requestLogId);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 현재 이 프로세스가 provider를 실행 중인 native run만 재시작 오류로 마감한다. */
|
|
270
|
+
export async function finishActiveNativeRunsForRestart(): Promise<void> {
|
|
271
|
+
const requestLogIds = [...activeNativeRunRefs.keys()];
|
|
272
|
+
for (const requestLogId of requestLogIds) {
|
|
273
|
+
interruptedNativeRunIds.add(requestLogId);
|
|
274
|
+
activeNativeRunRefs.delete(requestLogId);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await Promise.all(
|
|
278
|
+
requestLogIds.map(async (requestLogId) => {
|
|
279
|
+
await Promise.all(activeNativeFinalizers.get(requestLogId) ?? []);
|
|
280
|
+
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 프로세스 전역 registry를 격리해야 하는 단위 테스트 전용. */
|
|
286
|
+
export function resetNativeRunRegistryForTests(): void {
|
|
287
|
+
activeNativeRunRefs.clear();
|
|
288
|
+
interruptedNativeRunIds.clear();
|
|
289
|
+
activeNativeFinalizers.clear();
|
|
193
290
|
}
|
|
194
291
|
|
|
195
292
|
async function finishTerminalRun(
|