container manager update and restart stuffs

This commit is contained in:
legop3
2026-09-15 13:06:50 -04:00
parent 5d4f7fe5cc
commit 55ee6e05bb
10 changed files with 968 additions and 21 deletions
+363
View File
@@ -13,6 +13,7 @@
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.11.1",
"discord.js": "^14.25.1",
"dockerode": "^5.0.1",
"express": "^4.19.2",
"fuse.js": "^7.4.2",
"home-assistant-js-websocket": "^3.1.2",
@@ -35,6 +36,12 @@
"nodemon": "^3.1.0"
}
},
"node_modules/@balena/dockerignore": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
"license": "Apache-2.0"
},
"node_modules/@discordjs/builders": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
@@ -185,6 +192,55 @@
"tslib": "^2.4.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz",
"integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz",
"integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.5.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@huggingface/jinja": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
@@ -1088,6 +1144,16 @@
"node": ">=18.0.0"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1276,6 +1342,30 @@
}
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -1302,6 +1392,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -1373,6 +1472,15 @@
"node": ">= 18"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/better-sqlite3": {
"version": "12.11.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz",
@@ -1500,6 +1608,15 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buildcheck": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1572,6 +1689,20 @@
"node": ">=18"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -1666,6 +1797,20 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1803,6 +1948,61 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/docker-modem": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz",
"integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.15.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/docker-modem/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/docker-modem/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/dockerode": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz",
"integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==",
"license": "Apache-2.0",
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"docker-modem": "^5.0.7",
"protobufjs": "^7.3.2",
"tar-fs": "^2.1.4"
},
"engines": {
"node": ">= 14.17"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1823,6 +2023,12 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -1951,6 +2157,15 @@
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -2184,6 +2399,15 @@
"url": "https://github.com/sponsors/krisk"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2505,6 +2729,15 @@
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -2585,6 +2818,12 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
@@ -2796,6 +3035,13 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/nan": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz",
"integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==",
"license": "MIT",
"optional": true
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
@@ -3264,6 +3510,15 @@
"node": ">=20.18.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -3724,12 +3979,35 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
"license": "ISC"
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/ssh2": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.10",
"nan": "^2.23.0"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -3748,6 +4026,32 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -3875,6 +4179,12 @@
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
@@ -3975,6 +4285,23 @@
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -4024,6 +4351,15 @@
"node": ">=4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -4032,6 +4368,33 @@
"engines": {
"node": ">=18"
}
},
"node_modules/yargs": {
"version": "17.7.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
}
}
}
+1
View File
@@ -14,6 +14,7 @@
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.11.1",
"discord.js": "^14.25.1",
"dockerode": "^5.0.1",
"express": "^4.19.2",
"fuse.js": "^7.4.2",
"home-assistant-js-websocket": "^3.1.2",
@@ -0,0 +1,335 @@
// Restricted Container Lifecycle Controller
// Purpose: Pulls and replaces only the fixed MultiRover application container through Docker's local API.
// Scope: Runs as the private lifecycle Compose service; it exposes no TCP listener and accepts no caller-selected targets.
const fs = require('fs');
const path = require('path');
const http = require('http');
const Docker = require('dockerode');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
const TARGET_CONTAINER_NAME = 'multirover';
const TARGET_IMAGE = process.env.MULTIROVER_TARGET_IMAGE || 'ghcr.io/legop3/multiroombarover:latest';
const SOCKET_PATH = process.env.MULTIROVER_LIFECYCLE_SOCKET || '/run/multirover/lifecycle.sock';
const STATUS_PATH = process.env.MULTIROVER_LIFECYCLE_STATUS || '/data/runtime/container-lifecycle/status.json';
const HEALTH_TIMEOUT_MS = 2 * 60 * 1000;
const HEALTH_POLL_MS = 1000;
let operationRunning = false;
let status = readStatus();
function readStatus() {
try {
const saved = JSON.parse(fs.readFileSync(STATUS_PATH, 'utf8'));
// A controller replacement cannot resume an in-flight Docker operation.
// Preserve its history but make the interrupted result explicit.
if (saved.state === 'running') {
return {
...saved,
state: 'failed',
message: 'The lifecycle controller restarted before the operation completed.',
completedAt: Date.now(),
};
}
return saved;
} catch (error) {
if (error.code !== 'ENOENT') process.stderr.write(`Unable to read lifecycle status: ${error.message}\n`);
return {
state: 'idle',
operation: null,
message: 'No lifecycle operation has run yet.',
startedAt: null,
completedAt: null,
runningImage: null,
availableImage: null,
updateAvailable: null,
rollback: null,
};
}
}
function writeStatus(patch) {
status = { ...status, ...patch };
fs.mkdirSync(path.dirname(STATUS_PATH), { recursive: true });
const temporaryPath = `${STATUS_PATH}.tmp`;
fs.writeFileSync(temporaryPath, `${JSON.stringify(status, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
fs.renameSync(temporaryPath, STATUS_PATH);
return status;
}
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function inspectTargetContainer() {
return docker.getContainer(TARGET_CONTAINER_NAME).inspect();
}
async function inspectLocalTargetImage() {
try {
return await docker.getImage(TARGET_IMAGE).inspect();
} catch (error) {
if (error.statusCode === 404) return null;
throw error;
}
}
async function pullTargetImage() {
const stream = await new Promise((resolve, reject) => {
docker.pull(TARGET_IMAGE, (error, pullStream) => (error ? reject(error) : resolve(pullStream)));
});
await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (error) => (error ? reject(error) : resolve()));
});
const image = await inspectLocalTargetImage();
if (!image) throw new Error('Docker finished pulling but the target image is unavailable.');
return image;
}
function environmentMap(values = []) {
const result = new Map();
values.forEach((entry) => {
const separator = String(entry).indexOf('=');
const key = separator >= 0 ? entry.slice(0, separator) : entry;
result.set(key, String(entry));
});
return result;
}
function mergeEnvironment({ current = [], previousDefaults = [], nextDefaults = [] }) {
const previous = environmentMap(previousDefaults);
const overrides = current.filter((entry) => previous.get(String(entry).split('=', 1)[0]) !== entry);
const merged = environmentMap(nextDefaults);
overrides.forEach((entry) => merged.set(String(entry).split('=', 1)[0], entry));
return [...merged.values()];
}
async function buildReplacementOptions({ current, imageReference }) {
const [previousImage, nextImage] = await Promise.all([
docker.getImage(current.Image).inspect(),
docker.getImage(imageReference).inspect(),
]);
const imageConfig = nextImage.Config || {};
/*
Image-owned defaults come from the replacement image so updates can change
their command, health check, or runtime environment. Only environment
values that Compose overrode on the running container survive. HostConfig
is copied intact because it contains the named volume, host networking,
device access, capability, security, and restart-policy contract.
*/
return {
name: TARGET_CONTAINER_NAME,
Image: imageReference,
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: mergeEnvironment({
current: current.Config?.Env,
previousDefaults: previousImage.Config?.Env,
nextDefaults: imageConfig.Env,
}),
Cmd: imageConfig.Cmd,
Entrypoint: imageConfig.Entrypoint,
WorkingDir: imageConfig.WorkingDir,
User: imageConfig.User,
ExposedPorts: imageConfig.ExposedPorts,
Volumes: imageConfig.Volumes,
Healthcheck: imageConfig.Healthcheck,
StopSignal: imageConfig.StopSignal,
StopTimeout: imageConfig.StopTimeout,
Labels: { ...(imageConfig.Labels || {}), ...(current.Config?.Labels || {}) },
HostConfig: current.HostConfig,
};
}
async function removeContainerIfPresent() {
const container = docker.getContainer(TARGET_CONTAINER_NAME);
try {
const current = await container.inspect();
if (current.State?.Running) {
try {
await container.stop({ t: 20 });
} catch (error) {
if (error.statusCode !== 304) throw error;
}
}
await container.remove();
} catch (error) {
if (error.statusCode !== 404) throw error;
}
}
async function replaceContainer({ template, imageReference }) {
const options = await buildReplacementOptions({ current: template, imageReference });
await removeContainerIfPresent();
const replacement = await docker.createContainer(options);
await replacement.start();
return replacement;
}
async function waitForHealthyContainer() {
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
while (Date.now() < deadline) {
const current = await inspectTargetContainer();
const health = current.State?.Health?.Status;
if (current.State?.Running && health === 'healthy') return current;
if (health === 'unhealthy') throw new Error('The replacement container failed its health check.');
await sleep(HEALTH_POLL_MS);
}
throw new Error('The replacement container did not become healthy in time.');
}
async function refreshImageStatus() {
const [container, image] = await Promise.all([
inspectTargetContainer(),
inspectLocalTargetImage(),
]);
return writeStatus({
runningImage: container.Image,
availableImage: image?.Id || null,
updateAvailable: image ? container.Image !== image.Id : null,
});
}
async function runCheck() {
writeStatus({ message: `Pulling ${TARGET_IMAGE}` });
await pullTargetImage();
const current = await refreshImageStatus();
return current.updateAvailable ? 'An application update is available.' : 'The application is current.';
}
async function runRestart() {
writeStatus({ message: 'Restarting the application container…' });
await docker.getContainer(TARGET_CONTAINER_NAME).restart({ t: 20 });
await waitForHealthyContainer();
await refreshImageStatus();
return 'The application container restarted successfully.';
}
async function runUpdate() {
writeStatus({ message: `Pulling ${TARGET_IMAGE}`, rollback: null });
const targetImage = await pullTargetImage();
const previousContainer = await inspectTargetContainer();
const previousImage = previousContainer.Image;
let replacementStarted = false;
try {
writeStatus({
message: 'Replacing the application container…',
runningImage: previousImage,
availableImage: targetImage.Id,
updateAvailable: previousImage !== targetImage.Id,
});
replacementStarted = true;
await replaceContainer({ template: previousContainer, imageReference: TARGET_IMAGE });
writeStatus({ message: 'Waiting for the updated application to become healthy…' });
const healthy = await waitForHealthyContainer();
writeStatus({
runningImage: healthy.Image,
availableImage: targetImage.Id,
updateAvailable: false,
});
return previousImage === targetImage.Id
? 'The current application image was restarted successfully.'
: 'The application was updated successfully.';
} catch (error) {
if (!replacementStarted) throw error;
writeStatus({ message: 'The update failed; restoring the previous application image…' });
try {
await replaceContainer({ template: previousContainer, imageReference: previousImage });
await waitForHealthyContainer();
writeStatus({
rollback: { status: 'succeeded', image: previousImage, completedAt: Date.now() },
runningImage: previousImage,
updateAvailable: true,
});
} catch (rollbackError) {
writeStatus({
rollback: { status: 'failed', image: previousImage, error: rollbackError.message, completedAt: Date.now() },
});
throw new Error(`${error.message} Rollback also failed: ${rollbackError.message}`);
}
throw new Error(`${error.message} The previous image was restored.`);
}
}
function queueOperation(operation, runner) {
if (operationRunning) throw new Error('Another lifecycle operation is already running.');
operationRunning = true;
writeStatus({
state: 'running',
operation,
message: `Starting ${operation}`,
startedAt: Date.now(),
completedAt: null,
});
// Respond to the application before replacement disconnects its Socket.IO
// clients. The persisted status remains available when the new process asks
// for the result after reconnecting.
setTimeout(async () => {
try {
const message = await runner();
writeStatus({ state: 'succeeded', message, completedAt: Date.now() });
} catch (error) {
writeStatus({ state: 'failed', message: error.message, completedAt: Date.now() });
} finally {
operationRunning = false;
}
}, 500);
return status;
}
function sendJson(response, statusCode, value) {
response.writeHead(statusCode, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
response.end(JSON.stringify(value));
}
const server = http.createServer((request, response) => {
try {
if (request.method === 'GET' && request.url === '/status') {
refreshImageStatus()
.catch(() => status)
.then((current) => sendJson(response, 200, { available: true, ...current }));
return;
}
if (request.method === 'POST' && request.url === '/check') {
sendJson(response, 202, { available: true, ...queueOperation('check', runCheck) });
return;
}
if (request.method === 'POST' && request.url === '/restart') {
sendJson(response, 202, { available: true, ...queueOperation('restart', runRestart) });
return;
}
if (request.method === 'POST' && request.url === '/update') {
sendJson(response, 202, { available: true, ...queueOperation('update', runUpdate) });
return;
}
sendJson(response, 404, { error: 'Lifecycle operation not found.' });
} catch (error) {
sendJson(response, 409, { error: error.message });
}
});
fs.mkdirSync(path.dirname(SOCKET_PATH), { recursive: true });
fs.rmSync(SOCKET_PATH, { force: true });
server.listen(SOCKET_PATH, () => {
// The application runs as uid 1000 while this Docker-authorized controller
// runs as root. The socket carries only the fixed API above, so permitting
// the application user to connect does not expose arbitrary Docker access.
fs.chmodSync(SOCKET_PATH, 0o666);
process.stdout.write(`Lifecycle controller listening on ${SOCKET_PATH}\n`);
});
function stop() {
server.close(() => {
fs.rmSync(SOCKET_PATH, { force: true });
process.exit(0);
});
}
process.once('SIGINT', stop);
process.once('SIGTERM', stop);
@@ -4,7 +4,8 @@
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('serverControlService');
const { getConfigurationDatabase } = require('../../configuration');
const { requireRecentPassword } = require('../adminConfigurationService');
const { requireLockdownAdministrator, requireRecentPassword } = require('../adminConfigurationService');
const lifecycleClient = require('./lifecycleClient');
const database = getConfigurationDatabase();
let restartPending = false;
@@ -45,15 +46,60 @@ function requestApplicationRestart({ actor, reason = 'administrator-requested' }
}
io.on('connection', (socket) => {
socket.on('server:lifecycleStatus', (_payload = {}, cb = () => {}) => {
Promise.resolve()
.then(() => requireLockdownAdministrator(socket))
.then(() => lifecycleClient.getLifecycleStatus())
.then((lifecycle) => cb({ success: true, lifecycle }))
.catch((error) => cb({ error: error.message, code: error.code || null }));
});
socket.on('server:checkForUpdate', (_payload = {}, cb = () => {}) => {
Promise.resolve()
.then(() => requireRecentPassword(socket))
.then(() => lifecycleClient.checkForUpdate())
.then((lifecycle) => {
database.recordAuditEvent(actorFor(socket), 'application.update-check-requested', {});
cb({ success: true, lifecycle });
})
.catch((error) => cb({ error: error.message, code: error.code || null }));
});
socket.on('server:updateApplication', (_payload = {}, cb = () => {}) => {
Promise.resolve()
.then(() => requireRecentPassword(socket))
.then(() => lifecycleClient.updateApplication())
.then((lifecycle) => {
database.recordAuditEvent(actorFor(socket), 'application.update-requested', {});
io.emit('server:restarting', { reason: 'application-update' });
cb({ success: true, lifecycle });
})
.catch((error) => cb({ error: error.message, code: error.code || null }));
});
socket.on('server:restartApplication', (_payload = {}, cb = () => {}) => {
try {
Promise.resolve().then(async () => {
requireRecentPassword(socket);
if (restartPending) throw new Error('Application restart already pending.');
const actor = actorFor(socket);
requestApplicationRestart({ actor });
cb({ success: true });
} catch (error) {
cb({ error: error.message, code: error.code || null });
}
try {
const lifecycle = await lifecycleClient.restartApplication();
restartPending = true;
database.recordAuditEvent(actor, 'application.restart-requested', { reason: 'administrator-requested' });
logger.warn('Application container restart requested', { actor });
io.emit('server:restarting', { reason: 'administrator-requested' });
cb({ success: true, lifecycle });
} catch (error) {
/*
Legacy installations intentionally have no controller socket. Keep
their existing process-signal restart during migration, but never
bypass a real controller rejection such as an operation conflict.
*/
if (!['ENOENT', 'ECONNREFUSED'].includes(error.code)) throw error;
requestApplicationRestart({ actor });
cb({ success: true, legacy: true });
}
}).catch((error) => cb({ error: error.message, code: error.code || null }));
});
});
@@ -0,0 +1,72 @@
// Container Lifecycle Client
// Purpose: Sends fixed lifecycle requests from the application to the private controller socket.
// Scope: This client cannot select images, containers, or Docker arguments; the controller owns those constants.
const http = require('http');
const LIFECYCLE_SOCKET_PATH = process.env.MULTIROVER_LIFECYCLE_SOCKET || '/run/multirover/lifecycle.sock';
const REQUEST_TIMEOUT_MS = 5000;
function requestLifecycleController({ method = 'GET', path }) {
return new Promise((resolve, reject) => {
const request = http.request({
socketPath: LIFECYCLE_SOCKET_PATH,
method,
path,
headers: { Accept: 'application/json' },
}, (response) => {
let body = '';
response.setEncoding('utf8');
response.on('data', (chunk) => {
// Controller responses contain only a small status document. Keeping a
// hard bound prevents a broken peer from growing application memory.
body += chunk;
if (body.length > 64 * 1024) request.destroy(new Error('Lifecycle controller response is too large.'));
});
response.on('end', () => {
let parsed;
try {
parsed = body ? JSON.parse(body) : {};
} catch {
reject(new Error('Lifecycle controller returned an invalid response.'));
return;
}
if (response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(parsed.error || 'Lifecycle controller request failed.'));
return;
}
resolve(parsed);
});
});
request.setTimeout(REQUEST_TIMEOUT_MS, () => request.destroy(new Error('Lifecycle controller did not respond.')));
request.on('error', reject);
request.end();
});
}
async function getLifecycleStatus() {
try {
return await requestLifecycleController({ path: '/status' });
} catch (error) {
/*
The legacy development/server installation deliberately has no lifecycle
controller. Report that as an unavailable capability so the existing
in-process restart remains usable instead of turning absence into a noisy
server error.
*/
return {
available: false,
state: 'unavailable',
message: error.message,
operation: null,
updateAvailable: null,
};
}
}
module.exports = {
checkForUpdate: () => requestLifecycleController({ method: 'POST', path: '/check' }),
getLifecycleStatus,
restartApplication: () => requestLifecycleController({ method: 'POST', path: '/restart' }),
updateApplication: () => requestLifecycleController({ method: 'POST', path: '/update' }),
};