@fastybird/smart-panel-backend 1.1.0-alpha.3 → 1.1.0-alpha.30
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/dist/app.module.js +8 -0
- package/dist/app.module.js.map +1 -1
- package/dist/common/filters/conflict-exception.filter.d.ts +6 -0
- package/dist/common/filters/conflict-exception.filter.js +59 -0
- package/dist/common/filters/conflict-exception.filter.js.map +1 -0
- package/dist/common/filters/global-error.filter.js +1 -4
- package/dist/common/filters/global-error.filter.js.map +1 -1
- package/dist/common/filters/global-filters.d.ts +3 -0
- package/dist/common/filters/global-filters.js +22 -0
- package/dist/common/filters/global-filters.js.map +1 -0
- package/dist/common/filters/unprocessable-entity-exception.filter.d.ts +1 -0
- package/dist/common/filters/unprocessable-entity-exception.filter.js +15 -3
- package/dist/common/filters/unprocessable-entity-exception.filter.js.map +1 -1
- package/dist/dataSource.js +1 -0
- package/dist/dataSource.js.map +1 -1
- package/dist/main.js +2 -7
- package/dist/main.js.map +1 -1
- package/dist/migrations/1000000000026-AddHardwareInputCategories.d.ts +7 -0
- package/dist/migrations/1000000000026-AddHardwareInputCategories.js +364 -0
- package/dist/migrations/1000000000026-AddHardwareInputCategories.js.map +1 -0
- package/dist/modules/devices/controllers/channels.controller.d.ts +5 -1
- package/dist/modules/devices/controllers/channels.controller.js +34 -2
- package/dist/modules/devices/controllers/channels.controller.js.map +1 -1
- package/dist/modules/devices/controllers/channels.properties.controller.js +21 -4
- package/dist/modules/devices/controllers/channels.properties.controller.js.map +1 -1
- package/dist/modules/devices/controllers/devices.channels.controller.d.ts +5 -1
- package/dist/modules/devices/controllers/devices.channels.controller.js +38 -2
- package/dist/modules/devices/controllers/devices.channels.controller.js.map +1 -1
- package/dist/modules/devices/controllers/devices.channels.properties.controller.js +21 -4
- package/dist/modules/devices/controllers/devices.channels.properties.controller.js.map +1 -1
- package/dist/modules/devices/devices.constants.d.ts +6 -0
- package/dist/modules/devices/devices.constants.js +6 -0
- package/dist/modules/devices/devices.constants.js.map +1 -1
- package/dist/modules/devices/devices.module.js +20 -0
- package/dist/modules/devices/devices.module.js.map +1 -1
- package/dist/modules/devices/devices.openapi.d.ts +3 -1
- package/dist/modules/devices/devices.openapi.js +7 -0
- package/dist/modules/devices/devices.openapi.js.map +1 -1
- package/dist/modules/devices/dto/channel-input-occurrence.dto.d.ts +10 -0
- package/dist/modules/devices/dto/channel-input-occurrence.dto.js +99 -0
- package/dist/modules/devices/dto/channel-input-occurrence.dto.js.map +1 -0
- package/dist/modules/devices/models/channel-input-capabilities.model.d.ts +20 -0
- package/dist/modules/devices/models/channel-input-capabilities.model.js +111 -0
- package/dist/modules/devices/models/channel-input-capabilities.model.js.map +1 -0
- package/dist/modules/devices/models/channel-input-occurrence.model.d.ts +22 -0
- package/dist/modules/devices/models/channel-input-occurrence.model.js +154 -0
- package/dist/modules/devices/models/channel-input-occurrence.model.js.map +1 -0
- package/dist/modules/devices/services/channel-input-deduplication.service.d.ts +17 -0
- package/dist/modules/devices/services/channel-input-deduplication.service.js +88 -0
- package/dist/modules/devices/services/channel-input-deduplication.service.js.map +1 -0
- package/dist/modules/devices/services/channel-input-occurrences.service.d.ts +21 -0
- package/dist/modules/devices/services/channel-input-occurrences.service.js +163 -0
- package/dist/modules/devices/services/channel-input-occurrences.service.js.map +1 -0
- package/dist/modules/devices/services/channels.properties.service.d.ts +31 -3
- package/dist/modules/devices/services/channels.properties.service.js +346 -51
- package/dist/modules/devices/services/channels.properties.service.js.map +1 -1
- package/dist/modules/devices/services/channels.service.js +42 -38
- package/dist/modules/devices/services/channels.service.js.map +1 -1
- package/dist/modules/devices/services/command-latency-trace-collector.service.d.ts +101 -0
- package/dist/modules/devices/services/command-latency-trace-collector.service.js +451 -0
- package/dist/modules/devices/services/command-latency-trace-collector.service.js.map +1 -0
- package/dist/modules/devices/services/device-structure-lock.service.d.ts +10 -2
- package/dist/modules/devices/services/device-structure-lock.service.js +86 -7
- package/dist/modules/devices/services/device-structure-lock.service.js.map +1 -1
- package/dist/modules/devices/services/devices.service.js +37 -33
- package/dist/modules/devices/services/devices.service.js.map +1 -1
- package/dist/modules/devices/services/module-reset.service.d.ts +3 -1
- package/dist/modules/devices/services/module-reset.service.js +21 -16
- package/dist/modules/devices/services/module-reset.service.js.map +1 -1
- package/dist/modules/devices/services/property-command-dispatch.service.d.ts +45 -0
- package/dist/modules/devices/services/property-command-dispatch.service.js +227 -0
- package/dist/modules/devices/services/property-command-dispatch.service.js.map +1 -0
- package/dist/modules/devices/services/property-command-window.service.d.ts +71 -0
- package/dist/modules/devices/services/property-command-window.service.js +206 -0
- package/dist/modules/devices/services/property-command-window.service.js.map +1 -0
- package/dist/modules/devices/services/property-command.service.d.ts +20 -3
- package/dist/modules/devices/services/property-command.service.js +56 -10
- package/dist/modules/devices/services/property-command.service.js.map +1 -1
- package/dist/modules/devices/services/property-metadata.service.d.ts +31 -0
- package/dist/modules/devices/services/property-metadata.service.js +160 -0
- package/dist/modules/devices/services/property-metadata.service.js.map +1 -0
- package/dist/modules/devices/services/property-state-coordinator.service.d.ts +4 -0
- package/dist/modules/devices/services/property-state-coordinator.service.js +35 -0
- package/dist/modules/devices/services/property-state-coordinator.service.js.map +1 -0
- package/dist/modules/devices/services/property-value-lock.service.d.ts +3 -1
- package/dist/modules/devices/services/property-value-lock.service.js +13 -2
- package/dist/modules/devices/services/property-value-lock.service.js.map +1 -1
- package/dist/modules/devices/services/property-value.service.d.ts +1 -0
- package/dist/modules/devices/services/property-value.service.js +4 -0
- package/dist/modules/devices/services/property-value.service.js.map +1 -1
- package/dist/modules/devices/utils/schema.utils.js +1 -1
- package/dist/modules/devices/utils/schema.utils.js.map +1 -1
- package/dist/modules/platform/platforms/raspberry.platform.js +5 -10
- package/dist/modules/platform/platforms/raspberry.platform.js.map +1 -1
- package/dist/modules/platform/services/platform.service.d.ts +13 -4
- package/dist/modules/platform/services/platform.service.js +57 -35
- package/dist/modules/platform/services/platform.service.js.map +1 -1
- package/dist/modules/remote-access/dto/update-config.dto.js +5 -0
- package/dist/modules/remote-access/dto/update-config.dto.js.map +1 -1
- package/dist/modules/remote-access/validators/is-remote-access-url.validator.js +5 -2
- package/dist/modules/remote-access/validators/is-remote-access-url.validator.js.map +1 -1
- package/dist/modules/system/scripts/update-worker.sh +1168 -61
- package/dist/modules/system/services/privileged-worker.service.d.ts +13 -0
- package/dist/modules/system/services/privileged-worker.service.js +192 -31
- package/dist/modules/system/services/privileged-worker.service.js.map +1 -1
- package/dist/modules/system/services/system.service.d.ts +2 -0
- package/dist/modules/system/services/system.service.js +22 -1
- package/dist/modules/system/services/system.service.js.map +1 -1
- package/dist/modules/system/services/update-executor.service.d.ts +20 -2
- package/dist/modules/system/services/update-executor.service.js +272 -26
- package/dist/modules/system/services/update-executor.service.js.map +1 -1
- package/dist/modules/websocket/gateway/websocket.gateway.d.ts +4 -1
- package/dist/modules/websocket/gateway/websocket.gateway.js +28 -5
- package/dist/modules/websocket/gateway/websocket.gateway.js.map +1 -1
- package/dist/modules/websocket/services/command-acknowledgement-trace.service.d.ts +143 -0
- package/dist/modules/websocket/services/command-acknowledgement-trace.service.js +523 -0
- package/dist/modules/websocket/services/command-acknowledgement-trace.service.js.map +1 -0
- package/dist/modules/websocket/websocket.module.js +9 -1
- package/dist/modules/websocket/websocket.module.js.map +1 -1
- package/dist/plugins/devices-home-assistant/devices-home-assistant.openapi.d.ts +2 -2
- package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.d.ts +6 -1
- package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.js +16 -1
- package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.js.map +1 -1
- package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.d.ts +1 -0
- package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.js +11 -0
- package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.js.map +1 -1
- package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.d.ts +19 -0
- package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.js +51 -0
- package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.js.map +1 -0
- package/dist/plugins/devices-home-assistant/mappings/definitions/entity-mappings.yaml +56 -7
- package/dist/plugins/devices-home-assistant/models/mapping-preview.model.d.ts +1 -0
- package/dist/plugins/devices-home-assistant/models/mapping-preview.model.js +11 -0
- package/dist/plugins/devices-home-assistant/models/mapping-preview.model.js.map +1 -1
- package/dist/plugins/devices-home-assistant/services/device-adoption.service.js +17 -0
- package/dist/plugins/devices-home-assistant/services/device-adoption.service.js.map +1 -1
- package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.d.ts +27 -0
- package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.js +271 -0
- package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.js.map +1 -0
- package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.d.ts +2 -2
- package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.js +33 -14
- package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.js.map +1 -1
- package/dist/plugins/devices-home-assistant/services/mapping-preview.service.js +24 -4
- package/dist/plugins/devices-home-assistant/services/mapping-preview.service.js.map +1 -1
- package/dist/plugins/devices-home-assistant/services/state-changed.event.service.d.ts +4 -1
- package/dist/plugins/devices-home-assistant/services/state-changed.event.service.js +84 -5
- package/dist/plugins/devices-home-assistant/services/state-changed.event.service.js.map +1 -1
- package/dist/plugins/devices-home-assistant/services/wizard.service.js +1 -0
- package/dist/plugins/devices-home-assistant/services/wizard.service.js.map +1 -1
- package/dist/plugins/devices-home-assistant/utils/ha-event.utils.d.ts +2 -0
- package/dist/plugins/devices-home-assistant/utils/ha-event.utils.js +50 -0
- package/dist/plugins/devices-home-assistant/utils/ha-event.utils.js.map +1 -0
- package/dist/plugins/devices-homekit/listeners/homekit-event.listener.d.ts +4 -1
- package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js +39 -4
- package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js.map +1 -1
- package/dist/plugins/devices-homekit/mappers/base.mapper.js +30 -6
- package/dist/plugins/devices-homekit/mappers/base.mapper.js.map +1 -1
- package/dist/plugins/devices-homekit/mappers/button.mapper.d.ts +15 -0
- package/dist/plugins/devices-homekit/mappers/button.mapper.js +143 -0
- package/dist/plugins/devices-homekit/mappers/button.mapper.js.map +1 -0
- package/dist/plugins/devices-homekit/mappers/characteristic-write.d.ts +2 -0
- package/dist/plugins/devices-homekit/mappers/characteristic-write.js +17 -0
- package/dist/plugins/devices-homekit/mappers/characteristic-write.js.map +1 -0
- package/dist/plugins/devices-homekit/mappers/homekit-mapper.interface.d.ts +17 -1
- package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.d.ts +7 -0
- package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.js +96 -84
- package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.js.map +1 -1
- package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.d.ts +5 -1
- package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.js +42 -0
- package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.js.map +1 -1
- package/dist/plugins/devices-homey/mappings/definitions/channels.yaml +50 -0
- package/dist/plugins/devices-homey/mappings/definitions/devices.yaml +18 -0
- package/dist/plugins/devices-homey/mappings/definitions/properties.yaml +55 -0
- package/dist/plugins/devices-homey/mappings/device-support.js +10 -0
- package/dist/plugins/devices-homey/mappings/device-support.js.map +1 -1
- package/dist/plugins/devices-homey/mappings/mapping-loader.service.d.ts +1 -0
- package/dist/plugins/devices-homey/mappings/mapping-loader.service.js +66 -8
- package/dist/plugins/devices-homey/mappings/mapping-loader.service.js.map +1 -1
- package/dist/plugins/devices-homey/models/inventory.model.d.ts +2 -1
- package/dist/plugins/devices-homey/models/inventory.model.js +1 -0
- package/dist/plugins/devices-homey/models/inventory.model.js.map +1 -1
- package/dist/plugins/devices-homey/models/mapping-preview.model.d.ts +1 -0
- package/dist/plugins/devices-homey/models/mapping-preview.model.js +1 -0
- package/dist/plugins/devices-homey/models/mapping-preview.model.js.map +1 -1
- package/dist/plugins/devices-homey/services/homey-mapping-preview.service.js +7 -0
- package/dist/plugins/devices-homey/services/homey-mapping-preview.service.js.map +1 -1
- package/dist/plugins/devices-homey/services/homey-synchronizer.service.d.ts +3 -1
- package/dist/plugins/devices-homey/services/homey-synchronizer.service.js +37 -6
- package/dist/plugins/devices-homey/services/homey-synchronizer.service.js.map +1 -1
- package/dist/plugins/devices-reterminal/services/reterminal-button.service.d.ts +7 -3
- package/dist/plugins/devices-reterminal/services/reterminal-button.service.js +63 -17
- package/dist/plugins/devices-reterminal/services/reterminal-button.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.d.ts +36 -3
- package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.js +707 -42
- package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.d.ts +11 -1
- package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.js +100 -10
- package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/devices-shelly-ng.constants.js +9 -11
- package/dist/plugins/devices-shelly-ng/devices-shelly-ng.constants.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/devices-shelly-ng.plugin.js +2 -0
- package/dist/plugins/devices-shelly-ng/devices-shelly-ng.plugin.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/mappings/definitions/inputs.yaml +80 -0
- package/dist/plugins/devices-shelly-ng/mappings/mapping-loader.service.js +7 -1
- package/dist/plugins/devices-shelly-ng/mappings/mapping-loader.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/mappings/mapping.types.d.ts +6 -3
- package/dist/plugins/devices-shelly-ng/mappings/schema/mapping-schema.json +5 -0
- package/dist/plugins/devices-shelly-ng/services/device-manager.service.js +153 -8
- package/dist/plugins/devices-shelly-ng/services/device-manager.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.d.ts +51 -0
- package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.js +549 -0
- package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.js.map +1 -0
- package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.d.ts +14 -2
- package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.js +134 -35
- package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/services/shelly-rpc-client.service.js +3 -0
- package/dist/plugins/devices-shelly-ng/services/shelly-rpc-client.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.d.ts +1 -0
- package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js +14 -4
- package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js.map +1 -1
- package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.d.ts +2 -0
- package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.js +66 -4
- package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.js.map +1 -1
- package/dist/plugins/devices-shelly-v1/devices-shelly-v1.plugin.js +2 -0
- package/dist/plugins/devices-shelly-v1/devices-shelly-v1.plugin.js.map +1 -1
- package/dist/plugins/devices-shelly-v1/platforms/shelly-v1.device.platform.js +7 -0
- package/dist/plugins/devices-shelly-v1/platforms/shelly-v1.device.platform.js.map +1 -1
- package/dist/plugins/devices-shelly-v1/services/device-mapper.service.d.ts +3 -1
- package/dist/plugins/devices-shelly-v1/services/device-mapper.service.js +20 -2
- package/dist/plugins/devices-shelly-v1/services/device-mapper.service.js.map +1 -1
- package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.d.ts +16 -0
- package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.js +107 -0
- package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.js.map +1 -0
- package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.d.ts +7 -1
- package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.js +98 -3
- package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.js.map +1 -1
- package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.d.ts +17 -0
- package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.js +205 -0
- package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.js.map +1 -0
- package/dist/plugins/devices-third-party/devices-third-party.openapi.d.ts +1 -1
- package/dist/plugins/devices-third-party/devices-third-party.plugin.js +2 -1
- package/dist/plugins/devices-third-party/devices-third-party.plugin.js.map +1 -1
- package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.d.ts +5 -0
- package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.js +50 -0
- package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.js.map +1 -0
- package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.d.ts +8 -0
- package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.js +81 -0
- package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.js.map +1 -0
- package/dist/plugins/devices-third-party/platforms/third-party-device.platform.js +19 -4
- package/dist/plugins/devices-third-party/platforms/third-party-device.platform.js.map +1 -1
- package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.d.ts +2 -0
- package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.js +57 -0
- package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.d.ts +3 -0
- package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.js +41 -5
- package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/interfaces/zigbee2mqtt.interface.d.ts +7 -1
- package/dist/plugins/devices-zigbee2mqtt/mappings/config-driven.converter.js +16 -15
- package/dist/plugins/devices-zigbee2mqtt/mappings/config-driven.converter.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/mappings/definitions/remotes.yaml +505 -74
- package/dist/plugins/devices-zigbee2mqtt/mappings/schema/mapping-schema.json +3 -3
- package/dist/plugins/devices-zigbee2mqtt/mappings/transformers/transformer.types.d.ts +1 -1
- package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.d.ts +8 -2
- package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.js +6 -5
- package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/services/device-adoption.service.js +18 -11
- package/dist/plugins/devices-zigbee2mqtt/services/device-adoption.service.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.d.ts +9 -3
- package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.js +166 -3
- package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/services/mqtt-client-adapter.service.js +5 -2
- package/dist/plugins/devices-zigbee2mqtt/services/mqtt-client-adapter.service.js.map +1 -1
- package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.d.ts +1 -0
- package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.js +44 -4
- package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.js.map +1 -1
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.d.ts +17 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js +147 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.d.ts +15 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js +98 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.d.ts +9 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.js +94 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.d.ts +9 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.js +92 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.d.ts +7 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.js +48 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.d.ts +42 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js +352 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.d.ts +24 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.js +27 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.d.ts +8 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.js +19 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.d.ts +4 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.js +20 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.d.ts +21 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.js +135 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/scripts/cloudflared-setup.sh +185 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.d.ts +82 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js +507 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.d.ts +10 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js +37 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.d.ts +38 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.js +147 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.d.ts +15 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js +67 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.d.ts +8 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js +46 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js.map +1 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.d.ts +27 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.js +139 -0
- package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.js.map +1 -0
- package/dist/plugins/remote-access-tailscale/controllers/setup.controller.d.ts +1 -0
- package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js +26 -10
- package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/controllers/status.controller.d.ts +6 -1
- package/dist/plugins/remote-access-tailscale/controllers/status.controller.js +28 -3
- package/dist/plugins/remote-access-tailscale/controllers/status.controller.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/models/status.model.d.ts +19 -0
- package/dist/plugins/remote-access-tailscale/models/status.model.js +158 -1
- package/dist/plugins/remote-access-tailscale/models/status.model.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.d.ts +3 -0
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js +4 -1
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.d.ts +13 -0
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js +27 -0
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js.map +1 -0
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.d.ts +2 -2
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js +2 -0
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/remote-access-tailscale.plugin.js +1 -1
- package/dist/plugins/remote-access-tailscale/scripts/tailscale-setup.sh +61 -0
- package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.d.ts +9 -0
- package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js +12 -0
- package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.d.ts +1 -0
- package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js +27 -2
- package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.d.ts +31 -1
- package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js +316 -30
- package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.d.ts +8 -3
- package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js +39 -12
- package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js.map +1 -1
- package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.d.ts +14 -5
- package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js +51 -16
- package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js.map +1 -1
- package/dist/plugins/scenes-local/platforms/local-scene.platform.d.ts +3 -1
- package/dist/plugins/scenes-local/platforms/local-scene.platform.js +12 -7
- package/dist/plugins/scenes-local/platforms/local-scene.platform.js.map +1 -1
- package/dist/plugins/simulator/controllers/simulator.controller.d.ts +8 -2
- package/dist/plugins/simulator/controllers/simulator.controller.js +102 -2
- package/dist/plugins/simulator/controllers/simulator.controller.js.map +1 -1
- package/dist/plugins/simulator/dto/simulate-occurrence.dto.d.ts +12 -0
- package/dist/plugins/simulator/dto/simulate-occurrence.dto.js +122 -0
- package/dist/plugins/simulator/dto/simulate-occurrence.dto.js.map +1 -0
- package/dist/plugins/simulator/models/simulator-response.model.d.ts +12 -0
- package/dist/plugins/simulator/models/simulator-response.model.js +84 -1
- package/dist/plugins/simulator/models/simulator-response.model.js.map +1 -1
- package/dist/plugins/simulator/scenarios/definitions/small-apartment.yaml +78 -0
- package/dist/plugins/simulator/services/scenario-executor.service.js +3 -1
- package/dist/plugins/simulator/services/scenario-executor.service.js.map +1 -1
- package/dist/plugins/simulator/services/simulation.service.js +10 -1
- package/dist/plugins/simulator/services/simulation.service.js.map +1 -1
- package/dist/plugins/simulator/simulator.openapi.d.ts +3 -2
- package/dist/plugins/simulator/simulator.openapi.js +5 -0
- package/dist/plugins/simulator/simulator.openapi.js.map +1 -1
- package/dist/plugins/simulator/simulators/device-simulator.interface.d.ts +2 -0
- package/dist/plugins/simulator/simulators/device-simulator.interface.js +83 -0
- package/dist/plugins/simulator/simulators/device-simulator.interface.js.map +1 -1
- package/dist/plugins/simulator/simulators/index.d.ts +1 -0
- package/dist/plugins/simulator/simulators/index.js +1 -0
- package/dist/plugins/simulator/simulators/index.js.map +1 -1
- package/dist/plugins/simulator/simulators/input-controller.simulator.d.ts +10 -0
- package/dist/plugins/simulator/simulators/input-controller.simulator.js +57 -0
- package/dist/plugins/simulator/simulators/input-controller.simulator.js.map +1 -0
- package/dist/plugins/simulator/simulators/lighting.simulator.js +1 -0
- package/dist/plugins/simulator/simulators/lighting.simulator.js.map +1 -1
- package/dist/plugins/spaces-home-control/services/climate-intent.service.d.ts +3 -1
- package/dist/plugins/spaces-home-control/services/climate-intent.service.js +6 -3
- package/dist/plugins/spaces-home-control/services/climate-intent.service.js.map +1 -1
- package/dist/plugins/spaces-home-control/services/covers-intent.service.d.ts +3 -1
- package/dist/plugins/spaces-home-control/services/covers-intent.service.js +6 -3
- package/dist/plugins/spaces-home-control/services/covers-intent.service.js.map +1 -1
- package/dist/plugins/spaces-home-control/services/lighting-intent.service.d.ts +3 -1
- package/dist/plugins/spaces-home-control/services/lighting-intent.service.js +7 -4
- package/dist/plugins/spaces-home-control/services/lighting-intent.service.js.map +1 -1
- package/dist/plugins/spaces-home-control/services/space-media-activity.service.d.ts +3 -1
- package/dist/plugins/spaces-home-control/services/space-media-activity.service.js +11 -5
- package/dist/plugins/spaces-home-control/services/space-media-activity.service.js.map +1 -1
- package/dist/plugins/spaces-home-control/services/space-undo-history.service.d.ts +3 -1
- package/dist/plugins/spaces-home-control/services/space-undo-history.service.js +8 -5
- package/dist/plugins/spaces-home-control/services/space-undo-history.service.js.map +1 -1
- package/dist/spec/channels.d.ts +51 -1
- package/dist/spec/channels.js +64 -1
- package/dist/spec/channels.js.map +1 -1
- package/dist/spec/devices.d.ts +155 -2
- package/dist/spec/devices.js +161 -2
- package/dist/spec/devices.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -6,6 +6,839 @@ STATUS_FILE="${STATUS_FILE:-/var/lib/smart-panel/update-status.json}"
|
|
|
6
6
|
INSTALL_TYPE="${INSTALL_TYPE:-npm}"
|
|
7
7
|
IMAGE_BASE_DIR="${IMAGE_BASE_DIR:-/opt/smart-panel}"
|
|
8
8
|
DOWNLOAD_URL="${DOWNLOAD_URL:-}"
|
|
9
|
+
HEALTH_URL="${HEALTH_URL:-}"
|
|
10
|
+
HEALTH_EXPECTED_VERSION="${HEALTH_EXPECTED_VERSION:-$VERSION}"
|
|
11
|
+
UPDATE_START_MODE="${UPDATE_START_MODE:-running-service}"
|
|
12
|
+
|
|
13
|
+
# `stopped-maintenance` is an operator-only mode. The API always supplies the default
|
|
14
|
+
# running-service mode; this mode is intentionally guarded by a complete, explicit identity
|
|
15
|
+
# packet and is never inferred from a stale status file or an ambient process environment.
|
|
16
|
+
if [ "$UPDATE_START_MODE" != "running-service" ] && [ "$UPDATE_START_MODE" != "stopped-maintenance" ]; then
|
|
17
|
+
printf 'Unsupported update start mode: %s\n' "$UPDATE_START_MODE" >&2
|
|
18
|
+
exit 64
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# The attempt record and lock live beside the public status file, never inside a versioned image
|
|
22
|
+
# directory. This lets the record survive a backend restart, target-directory cleanup or a
|
|
23
|
+
# failed migration, while keeping it outside the release tarball and private to the service user.
|
|
24
|
+
ATTEMPT_DIR="${ATTEMPT_DIR:-$(dirname "$STATUS_FILE")/update-attempt}"
|
|
25
|
+
ATTEMPT_FILE="${ATTEMPT_DIR}/attempt.json"
|
|
26
|
+
ATTEMPT_LOCK="${ATTEMPT_DIR}/lock"
|
|
27
|
+
# Only the internal executor supplies its own group; callers cannot select it through the API.
|
|
28
|
+
# Legacy/operator invocations without a reader retain owner-only permissions.
|
|
29
|
+
UPDATE_OBSERVER_GID="${UPDATE_OBSERVER_GID:-}"
|
|
30
|
+
case "$UPDATE_OBSERVER_GID" in
|
|
31
|
+
*[!0-9]*) printf 'Invalid update observer group\n' >&2; exit 64 ;;
|
|
32
|
+
esac
|
|
33
|
+
ATTEMPT_ID=""
|
|
34
|
+
MIGRATION_ENTERED="false"
|
|
35
|
+
FINALIZED="false"
|
|
36
|
+
MIGRATION_LOG=""
|
|
37
|
+
CURRENT_PHASE="preparing"
|
|
38
|
+
SERVICE_NAME="smart-panel"
|
|
39
|
+
SERVICE_UNIT="${SERVICE_UNIT:-${SERVICE_NAME}.service}"
|
|
40
|
+
QUIESCENCE_TIMEOUT_SECONDS="${QUIESCENCE_TIMEOUT_SECONDS:-30}"
|
|
41
|
+
START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-60}"
|
|
42
|
+
CAPTURED_SERVICE_CGROUP_PROCS=""
|
|
43
|
+
CAPTURED_SERVICE_CGROUP_DIR=""
|
|
44
|
+
CAPTURED_SERVICE_CGROUP_EVENTS=""
|
|
45
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES=""
|
|
46
|
+
CGROUP_MEMBERS=""
|
|
47
|
+
CGROUP_POPULATED=""
|
|
48
|
+
MAINTENANCE_DIR=""
|
|
49
|
+
DB_BACKUP_PATH="${DB_BACKUP_PATH:-}"
|
|
50
|
+
BACKUP_TMP_PATH=""
|
|
51
|
+
SOURCE_DB_PATH=""
|
|
52
|
+
STOPPED_SERVICE_BASELINE=""
|
|
53
|
+
CGROUP_PROBE_MOUNT_ID=""
|
|
54
|
+
CGROUP_PROBE_PARENT_ID=""
|
|
55
|
+
CGROUP_PROBE_DIR_ID=""
|
|
56
|
+
CGROUP_PROBE_EVENTS_ID=""
|
|
57
|
+
CGROUP_PROBE_NAMESPACE_ID=""
|
|
58
|
+
CGROUP_PROBE_ABSENT="false"
|
|
59
|
+
|
|
60
|
+
json_escape() {
|
|
61
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/[[:cntrl:]]//g'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
source_database_is_valid() {
|
|
65
|
+
[ -n "${SOURCE_DB_PATH:-}" ] || return 1
|
|
66
|
+
[ ! -L "$SOURCE_DB_PATH" ] && [ -f "$SOURCE_DB_PATH" ] && [ -s "$SOURCE_DB_PATH" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
write_attempt() {
|
|
70
|
+
local state="$1"
|
|
71
|
+
local phase="$2"
|
|
72
|
+
local error="${3:-}"
|
|
73
|
+
local recovery_required="${4:-false}"
|
|
74
|
+
local migration_entered="${5:-$MIGRATION_ENTERED}"
|
|
75
|
+
local tmp_file="${ATTEMPT_FILE}.tmp.$$"
|
|
76
|
+
CURRENT_PHASE="$phase"
|
|
77
|
+
|
|
78
|
+
mkdir -p "$ATTEMPT_DIR"
|
|
79
|
+
if [ -n "$UPDATE_OBSERVER_GID" ]; then
|
|
80
|
+
chgrp "$UPDATE_OBSERVER_GID" "$ATTEMPT_DIR" || return 1
|
|
81
|
+
# Traverse to the known attempt file without exposing directory listings or writes.
|
|
82
|
+
chmod 710 "$ATTEMPT_DIR" || return 1
|
|
83
|
+
else
|
|
84
|
+
chmod 700 "$ATTEMPT_DIR"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
printf '{\n'
|
|
89
|
+
printf '\t"attemptId": "%s",\n' "$(json_escape "$ATTEMPT_ID")"
|
|
90
|
+
printf '\t"ownerPid": %s,\n' "$$"
|
|
91
|
+
printf '\t"targetVersion": "%s",\n' "$(json_escape "$VERSION")"
|
|
92
|
+
printf '\t"state": "%s",\n' "$(json_escape "$state")"
|
|
93
|
+
printf '\t"phase": "%s",\n' "$(json_escape "$phase")"
|
|
94
|
+
printf '\t"migrationEntered": %s,\n' "$migration_entered"
|
|
95
|
+
printf '\t"recoveryRequired": %s,\n' "$recovery_required"
|
|
96
|
+
printf '\t"migrationLog": "%s",\n' "$(json_escape "$MIGRATION_LOG")"
|
|
97
|
+
printf '\t"updatedAt": "%s",\n' "$(date -Iseconds)"
|
|
98
|
+
printf '\t"error": "%s"\n' "$(json_escape "$error")"
|
|
99
|
+
printf '}\n'
|
|
100
|
+
} > "$tmp_file"
|
|
101
|
+
|
|
102
|
+
chmod 600 "$tmp_file"
|
|
103
|
+
if [ -n "$UPDATE_OBSERVER_GID" ]; then
|
|
104
|
+
chgrp "$UPDATE_OBSERVER_GID" "$tmp_file" || return 1
|
|
105
|
+
chmod 640 "$tmp_file" || return 1
|
|
106
|
+
fi
|
|
107
|
+
mv -f "$tmp_file" "$ATTEMPT_FILE"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
attempt_is_unresolved() {
|
|
111
|
+
if [ -f "$ATTEMPT_FILE" ] && grep -q '"state": "complete"' "$ATTEMPT_FILE"; then
|
|
112
|
+
# A crash after the durable completion record but before rmdir must not strand a clean
|
|
113
|
+
# attempt forever; completion is the evidence that makes this lock removable.
|
|
114
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
115
|
+
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if [ -d "$ATTEMPT_LOCK" ]; then
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [ ! -f "$ATTEMPT_FILE" ]; then
|
|
124
|
+
return 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
grep -q '"state": "active"' "$ATTEMPT_FILE" || grep -q '"recoveryRequired": true' "$ATTEMPT_FILE"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
acquire_attempt() {
|
|
131
|
+
mkdir -p "$ATTEMPT_DIR"
|
|
132
|
+
chmod 700 "$ATTEMPT_DIR"
|
|
133
|
+
|
|
134
|
+
if attempt_is_unresolved; then
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# mkdir is the portable atomic OS-level lock used by the legacy launcher. An orphaned lock is
|
|
139
|
+
# deliberately retained; an expired timestamp cannot prove that a migration did not commit.
|
|
140
|
+
if ! mkdir "$ATTEMPT_LOCK" 2>/dev/null; then
|
|
141
|
+
return 1
|
|
142
|
+
fi
|
|
143
|
+
chmod 700 "$ATTEMPT_LOCK"
|
|
144
|
+
|
|
145
|
+
ATTEMPT_ID="$(date +%s)-$$"
|
|
146
|
+
write_attempt "active" "preparing" "" "false" "false"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
release_attempt() {
|
|
150
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
151
|
+
write_attempt "complete" "complete" "" "false" "$MIGRATION_ENTERED"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
hold_attempt() {
|
|
158
|
+
local phase="$1"
|
|
159
|
+
local error="$2"
|
|
160
|
+
|
|
161
|
+
write_attempt "recovery_required" "$phase" "$error" "true" "$MIGRATION_ENTERED"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
finish_before_migration() {
|
|
165
|
+
local phase="$1"
|
|
166
|
+
local error="$2"
|
|
167
|
+
|
|
168
|
+
if [ -n "${BACKUP_TMP_PATH:-}" ]; then
|
|
169
|
+
rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
|
|
170
|
+
BACKUP_TMP_PATH=""
|
|
171
|
+
fi
|
|
172
|
+
if [ -n "${TMP_TARBALL:-}" ]; then
|
|
173
|
+
rm -f "$TMP_TARBALL" 2>/dev/null || true
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
write_attempt "failed_preflight" "$phase" "$error" "false" "false"
|
|
177
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
phase_requires_hold() {
|
|
181
|
+
case "$CURRENT_PHASE" in
|
|
182
|
+
stopping|quiesced|switching) return 0 ;;
|
|
183
|
+
*) return 1 ;;
|
|
184
|
+
esac
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fail_before_migration() {
|
|
188
|
+
local phase="$1"
|
|
189
|
+
local error="$2"
|
|
190
|
+
|
|
191
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
192
|
+
finish_before_migration "$phase" "$error"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
update_status "failed" "failed" "$error"
|
|
196
|
+
FINALIZED="true"
|
|
197
|
+
exit 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
restore_before_migration() {
|
|
201
|
+
local previous_target="$1"
|
|
202
|
+
local current_link="$2"
|
|
203
|
+
local new_version_dir="$3"
|
|
204
|
+
local resolved_previous resolved_current
|
|
205
|
+
|
|
206
|
+
[ -n "$previous_target" ] || return 1
|
|
207
|
+
sudo -n ln -sfn "$previous_target" "$current_link" || return 1
|
|
208
|
+
|
|
209
|
+
resolved_previous="$(realpath "$previous_target" 2>/dev/null || true)"
|
|
210
|
+
resolved_current="$(realpath "$current_link" 2>/dev/null || true)"
|
|
211
|
+
[ -n "$resolved_previous" ] && [ "$resolved_previous" = "$resolved_current" ] || return 1
|
|
212
|
+
|
|
213
|
+
rm -rf "$new_version_dir" || return 1
|
|
214
|
+
sudo -n systemctl start "$SERVICE_NAME" 2>/dev/null || return 1
|
|
215
|
+
wait_for_started_service
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
service_property() {
|
|
219
|
+
local property="$1"
|
|
220
|
+
|
|
221
|
+
systemctl show "$SERVICE_NAME" --property="$property" --value
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
manager_processes_are_empty() {
|
|
225
|
+
local output
|
|
226
|
+
|
|
227
|
+
command -v busctl >/dev/null 2>&1 || return 2
|
|
228
|
+
output="$(busctl --system --json=short call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
|
|
229
|
+
org.freedesktop.systemd1.Manager GetUnitProcesses s "$SERVICE_UNIT" 2>/dev/null)" || return 2
|
|
230
|
+
|
|
231
|
+
# busctl's JSON preserves the a(sus) signature and tuple boundaries. Never infer an empty
|
|
232
|
+
# process set from omitted digits, a truncated reply, or a successful command with no payload.
|
|
233
|
+
[ "${#output}" -le 65536 ] || return 2
|
|
234
|
+
printf '%s' "$output" | MANAGER_REPLY=true node -e '
|
|
235
|
+
let input = "";
|
|
236
|
+
process.stdin.setEncoding("utf8");
|
|
237
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
238
|
+
process.stdin.on("end", () => {
|
|
239
|
+
try {
|
|
240
|
+
const reply = JSON.parse(input);
|
|
241
|
+
if (reply?.type !== "a(sus)" || !Array.isArray(reply.data) ||
|
|
242
|
+
reply.data.length !== 1 || !Array.isArray(reply.data[0])) process.exit(2);
|
|
243
|
+
const members = reply.data[0];
|
|
244
|
+
if (members.length > 4096) process.exit(2);
|
|
245
|
+
for (const tuple of members) {
|
|
246
|
+
if (!Array.isArray(tuple) || tuple.length !== 3 ||
|
|
247
|
+
typeof tuple[0] !== "string" || typeof tuple[2] !== "string" ||
|
|
248
|
+
!Number.isInteger(tuple[1]) || tuple[1] < 1 || tuple[1] > 4294967295) process.exit(2);
|
|
249
|
+
}
|
|
250
|
+
process.exit(members.length === 0 ? 0 : 1);
|
|
251
|
+
} catch {
|
|
252
|
+
process.exit(2);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
service_unit_object() {
|
|
259
|
+
local output
|
|
260
|
+
command -v busctl >/dev/null 2>&1 || return 2
|
|
261
|
+
output="$(busctl --system --json=short call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
|
|
262
|
+
org.freedesktop.systemd1.Manager GetUnit s "$SERVICE_UNIT" 2>/dev/null)" || return 2
|
|
263
|
+
[ "${#output}" -le 4096 ] || return 2
|
|
264
|
+
printf '%s' "$output" | MANAGER_REPLY=true node -e '
|
|
265
|
+
let input = "";
|
|
266
|
+
process.stdin.setEncoding("utf8");
|
|
267
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
268
|
+
process.stdin.on("end", () => {
|
|
269
|
+
try {
|
|
270
|
+
const reply = JSON.parse(input);
|
|
271
|
+
const object = reply?.data?.[0];
|
|
272
|
+
if (reply.type !== "o" || reply.data.length !== 1 || typeof object !== "string" ||
|
|
273
|
+
!object.startsWith("/org/freedesktop/systemd1/unit/")) process.exit(2);
|
|
274
|
+
process.stdout.write(object);
|
|
275
|
+
} catch { process.exit(2); }
|
|
276
|
+
});
|
|
277
|
+
'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
systemd_manager_identity() {
|
|
281
|
+
local stat_line start_time
|
|
282
|
+
if [ "${SERVICE_PROCESS_ROOT:-/proc}" != "/proc" ]; then
|
|
283
|
+
process_identity 1
|
|
284
|
+
return $?
|
|
285
|
+
fi
|
|
286
|
+
stat_line="$(cat /proc/1/stat 2>/dev/null)" || return 2
|
|
287
|
+
case "$stat_line" in '1 (systemd) '*) ;; *) return 2 ;; esac
|
|
288
|
+
start_time="$(printf '%s\n' "$stat_line" | awk '{ sub(/^.*\) /, ""); print $20 }')" || return 2
|
|
289
|
+
case "$start_time" in ''|*[!0-9]*) return 2 ;; esac
|
|
290
|
+
[ "$start_time" -gt 0 ] 2>/dev/null || return 2
|
|
291
|
+
printf '1|%s\n' "$start_time"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
stopped_service_identity() {
|
|
295
|
+
local boot_id manager_identity unit_object property value fragment_path exec_start slice delegate kill_mode unit_id load_state
|
|
296
|
+
|
|
297
|
+
boot_id="$(cat "${SERVICE_BOOT_ID_FILE:-/proc/sys/kernel/random/boot_id}" 2>/dev/null)" || return 2
|
|
298
|
+
[ -n "$boot_id" ] || return 2
|
|
299
|
+
manager_identity="$(systemd_manager_identity)" || return 2
|
|
300
|
+
unit_object="$(service_unit_object)" || return 2
|
|
301
|
+
unit_id="$(service_property Id 2>/dev/null)" || return 2
|
|
302
|
+
load_state="$(service_property LoadState 2>/dev/null)" || return 2
|
|
303
|
+
fragment_path="$(service_property FragmentPath 2>/dev/null)" || return 2
|
|
304
|
+
exec_start="$(service_property ExecStart 2>/dev/null)" || return 2
|
|
305
|
+
slice="$(service_property Slice 2>/dev/null)" || return 2
|
|
306
|
+
delegate="$(service_property Delegate 2>/dev/null)" || return 2
|
|
307
|
+
kill_mode="$(service_property KillMode 2>/dev/null)" || return 2
|
|
308
|
+
[ "$unit_id" = "$SERVICE_UNIT" ] && [ "$load_state" = "loaded" ] || return 2
|
|
309
|
+
[ "$fragment_path" = "/etc/systemd/system/${SERVICE_UNIT}" ] || return 2
|
|
310
|
+
[[ "$exec_start" == *"${IMAGE_BASE_DIR}/current"* ]] || return 2
|
|
311
|
+
[ "$slice" = "system.slice" ] && [ "$delegate" = "no" ] && [ "$kill_mode" = "process" ] || return 2
|
|
312
|
+
printf 'boot=%s\nmanager=%s\nobject=%s\n' "$boot_id" "$manager_identity" "$unit_object"
|
|
313
|
+
for property in FragmentPath ExecStart Slice Delegate KillMode InvocationID Result; do
|
|
314
|
+
value="$(service_property "$property" 2>/dev/null)" || return 2
|
|
315
|
+
[ -n "$value" ] || return 2
|
|
316
|
+
printf '%s=%s\n' "$property" "$value"
|
|
317
|
+
done
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
stopped_service_quiesced() {
|
|
321
|
+
local active_state main_pid control_pid job_id control_group cgroup_dir cgroup_procs cgroup_events
|
|
322
|
+
local member_status population_status probe_status members subtree_file baseline current_baseline cgroup_mount_type
|
|
323
|
+
|
|
324
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
325
|
+
if [ -n "${SERVICE_CGROUP_MOUNT_TYPE:-}" ]; then
|
|
326
|
+
[ "$SERVICE_CGROUP_MOUNT_TYPE" = "cgroup2fs" ] || return 2
|
|
327
|
+
else
|
|
328
|
+
cgroup_mount_type="$(LC_ALL=C stat -f -c '%T' "${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" 2>/dev/null || true)"
|
|
329
|
+
[ "$cgroup_mount_type" = "cgroup2fs" ] || return 2
|
|
330
|
+
fi
|
|
331
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
332
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
333
|
+
job_id="$(service_property Job 2>/dev/null)" || return 2
|
|
334
|
+
case "$active_state" in inactive|failed) ;; *) return 1 ;; esac
|
|
335
|
+
[ "$main_pid" = "0" ] || return 1
|
|
336
|
+
[ "$control_pid" = "0" ] || return 1
|
|
337
|
+
# systemctl --value prints an empty value when the unit has no pending job.
|
|
338
|
+
[ -z "$job_id" ] || [ "$job_id" = "0" ] || return 1
|
|
339
|
+
|
|
340
|
+
manager_processes_are_empty || return $?
|
|
341
|
+
baseline="$(stopped_service_identity)" || return 2
|
|
342
|
+
if [ -n "$STOPPED_SERVICE_BASELINE" ]; then
|
|
343
|
+
[ "$baseline" = "$STOPPED_SERVICE_BASELINE" ] || return 1
|
|
344
|
+
else
|
|
345
|
+
STOPPED_SERVICE_BASELINE="$baseline"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
control_group="$(service_property ControlGroup 2>/dev/null)" || return 2
|
|
349
|
+
# systemd may clear ControlGroup after a stopped unit is reaped. The canonical unit cgroup is
|
|
350
|
+
# still the only acceptable fallback; it is checked against the cgroup-v2 mount below.
|
|
351
|
+
if [ -z "$control_group" ]; then
|
|
352
|
+
[ "$(service_property Slice 2>/dev/null)" = "system.slice" ] || return 2
|
|
353
|
+
[ "$(service_property Delegate 2>/dev/null)" = "no" ] || return 2
|
|
354
|
+
control_group="/system.slice/${SERVICE_UNIT}"
|
|
355
|
+
fi
|
|
356
|
+
[ "$control_group" = "/system.slice/${SERVICE_UNIT}" ] || return 2
|
|
357
|
+
cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
|
|
358
|
+
cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
|
|
359
|
+
cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
|
|
360
|
+
|
|
361
|
+
if probe_cgroup_directory "$cgroup_dir"; then
|
|
362
|
+
[ -r "$cgroup_procs" ] || return 2
|
|
363
|
+
[ -r "$cgroup_events" ] || return 2
|
|
364
|
+
read_cgroup_members "$cgroup_procs" || return $?
|
|
365
|
+
members="$CGROUP_MEMBERS"
|
|
366
|
+
[ -z "$members" ] || return 1
|
|
367
|
+
read_cgroup_population "$cgroup_events" || return $?
|
|
368
|
+
[ "$CGROUP_POPULATED" = "0" ] || return 1
|
|
369
|
+
# Check descendants as well as the unit's direct cgroup. A delegated child subtree is not
|
|
370
|
+
# quiescent merely because the parent cgroup.procs is empty.
|
|
371
|
+
local subtree_files symlinks
|
|
372
|
+
symlinks="$(find "$cgroup_dir" -type l -print 2>/dev/null)" || return 2
|
|
373
|
+
[ -z "$symlinks" ] || return 2
|
|
374
|
+
subtree_files="$(find "$cgroup_dir" -type f -name cgroup.procs -print 2>/dev/null)" || return 2
|
|
375
|
+
while IFS= read -r subtree_file; do
|
|
376
|
+
[ -n "$subtree_file" ] || continue
|
|
377
|
+
[ "$subtree_file" = "$cgroup_procs" ] && continue
|
|
378
|
+
read_cgroup_members "$subtree_file" || return $?
|
|
379
|
+
[ -z "$CGROUP_MEMBERS" ] || return 1
|
|
380
|
+
done <<< "$subtree_files"
|
|
381
|
+
current_baseline="$(stopped_service_identity)" || return 2
|
|
382
|
+
[ "$current_baseline" = "$baseline" ] || return 1
|
|
383
|
+
manager_processes_are_empty || return $?
|
|
384
|
+
return 0
|
|
385
|
+
else
|
|
386
|
+
probe_status=$?
|
|
387
|
+
fi
|
|
388
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
389
|
+
|
|
390
|
+
# A removed original cgroup is valid only when the cgroup-v2 mount and its stable parent remain
|
|
391
|
+
# readable; manager enumeration above is the independent proof that no unit process survived.
|
|
392
|
+
current_baseline="$(stopped_service_identity)" || return 2
|
|
393
|
+
[ "$current_baseline" = "$baseline" ] || return 1
|
|
394
|
+
manager_processes_are_empty || return $?
|
|
395
|
+
return 0
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
wait_for_stopped_service() {
|
|
399
|
+
local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS)) result
|
|
400
|
+
|
|
401
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
402
|
+
if stopped_service_quiesced; then return 0; else result=$?; fi
|
|
403
|
+
[ "$result" -eq 2 ] || sleep 1
|
|
404
|
+
[ "$result" -eq 2 ] && return 2
|
|
405
|
+
done
|
|
406
|
+
return 1
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
target_health_is_valid() {
|
|
410
|
+
local payload
|
|
411
|
+
|
|
412
|
+
[ -n "$HEALTH_URL" ] || return 1
|
|
413
|
+
payload="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null)" || return 1
|
|
414
|
+
|
|
415
|
+
printf '%s' "$payload" | HEALTH_EXPECTED_VERSION="$HEALTH_EXPECTED_VERSION" node -e '
|
|
416
|
+
let input = "";
|
|
417
|
+
process.stdin.setEncoding("utf8");
|
|
418
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
419
|
+
process.stdin.on("end", () => {
|
|
420
|
+
try {
|
|
421
|
+
const body = JSON.parse(input);
|
|
422
|
+
const data = body && body.data;
|
|
423
|
+
process.exit(data?.status === "ok" && data.version === process.env.HEALTH_EXPECTED_VERSION ? 0 : 1);
|
|
424
|
+
} catch {
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
target_process_is_stable() {
|
|
432
|
+
local state main_pid start_identity resolved_target expected_target
|
|
433
|
+
|
|
434
|
+
state="$(service_property ActiveState 2>/dev/null || true)"
|
|
435
|
+
main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
436
|
+
start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
437
|
+
resolved_target="$(readlink -f "$CURRENT_LINK" 2>/dev/null || true)"
|
|
438
|
+
expected_target="$(realpath "$NEW_VERSION_DIR" 2>/dev/null || true)"
|
|
439
|
+
|
|
440
|
+
[ "$state" = "active" ] || return 1
|
|
441
|
+
[ "$main_pid" != "0" ] && [ -n "$main_pid" ] || return 1
|
|
442
|
+
[ -n "$start_identity" ] || return 1
|
|
443
|
+
[ -n "$CURRENT_LINK" ] && [ -n "$NEW_VERSION_DIR" ] || return 1
|
|
444
|
+
[ -n "$expected_target" ] && [ "$resolved_target" = "$expected_target" ] || return 1
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
process_identity() {
|
|
448
|
+
local pid="$1"
|
|
449
|
+
local proc_root="${SERVICE_PROCESS_ROOT:-/proc}"
|
|
450
|
+
local proc_dir="${proc_root}/${pid}"
|
|
451
|
+
local stat_line start_time executable
|
|
452
|
+
|
|
453
|
+
[ "$pid" != "0" ] && [ -n "$pid" ] || return 1
|
|
454
|
+
|
|
455
|
+
# SERVICE_PROCESS_ROOT is a private fixture hook; the production default is /proc.
|
|
456
|
+
if [ -r "${proc_dir}/identity" ]; then
|
|
457
|
+
printf '%s|%s\n' "$pid" "$(cat "${proc_dir}/identity")"
|
|
458
|
+
return 0
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
if [ -r "${proc_dir}/stat" ]; then
|
|
462
|
+
stat_line="$(cat "${proc_dir}/stat")" || return 2
|
|
463
|
+
start_time="$(printf '%s\n' "$stat_line" | awk '{ sub(/^.*\) /, ""); print $20 }')" || return 2
|
|
464
|
+
case "$start_time" in ''|*[!0-9]*) return 2 ;; esac
|
|
465
|
+
[ "$start_time" -gt 0 ] 2>/dev/null || return 2
|
|
466
|
+
executable="$(readlink "${proc_dir}/exe" 2>/dev/null)" || return 2
|
|
467
|
+
[ -n "$executable" ] || return 2
|
|
468
|
+
printf '%s|%s|%s\n' "$pid" "$start_time" "$executable"
|
|
469
|
+
return 0
|
|
470
|
+
fi
|
|
471
|
+
[ "$proc_root" = "/proc" ] || return 1
|
|
472
|
+
|
|
473
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
474
|
+
return 1
|
|
475
|
+
fi
|
|
476
|
+
# A live process without readable /proc start ticks and executable has unknown identity.
|
|
477
|
+
return 2
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
read_cgroup_file() {
|
|
481
|
+
CGROUP_FILE_PATH="$1" node -e '
|
|
482
|
+
const fs = require("node:fs");
|
|
483
|
+
let fd;
|
|
484
|
+
try {
|
|
485
|
+
fd = fs.openSync(process.env.CGROUP_FILE_PATH, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
486
|
+
if (!fs.fstatSync(fd).isFile()) process.exit(2);
|
|
487
|
+
const chunks = [];
|
|
488
|
+
let size = 0;
|
|
489
|
+
while (true) {
|
|
490
|
+
const buffer = Buffer.allocUnsafe(4096);
|
|
491
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
492
|
+
if (count === 0) break;
|
|
493
|
+
size += count;
|
|
494
|
+
if (size > 65536) process.exit(2);
|
|
495
|
+
chunks.push(buffer.subarray(0, count));
|
|
496
|
+
}
|
|
497
|
+
process.stdout.write(Buffer.concat(chunks));
|
|
498
|
+
} catch { process.exitCode = 2; }
|
|
499
|
+
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
500
|
+
'
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
read_cgroup_members() {
|
|
504
|
+
local cgroup_procs="$1"
|
|
505
|
+
local contents members
|
|
506
|
+
|
|
507
|
+
CGROUP_MEMBERS=""
|
|
508
|
+
if ! contents="$(read_cgroup_file "$cgroup_procs" 2>/dev/null)"; then
|
|
509
|
+
return 2
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
if ! members="$(printf '%s\n' "$contents" | awk '
|
|
513
|
+
length($0) > 0 {
|
|
514
|
+
if ($0 !~ /^[0-9]+$/) {
|
|
515
|
+
exit 2
|
|
516
|
+
}
|
|
517
|
+
print
|
|
518
|
+
}
|
|
519
|
+
')"; then
|
|
520
|
+
return 2
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
CGROUP_MEMBERS="$members"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
read_cgroup_population() {
|
|
527
|
+
local cgroup_events="$1"
|
|
528
|
+
local contents populated
|
|
529
|
+
|
|
530
|
+
CGROUP_POPULATED=""
|
|
531
|
+
if ! contents="$(read_cgroup_file "$cgroup_events" 2>/dev/null)"; then
|
|
532
|
+
return 2
|
|
533
|
+
fi
|
|
534
|
+
|
|
535
|
+
if ! populated="$(printf '%s\n' "$contents" | awk '
|
|
536
|
+
$1 == "populated" {
|
|
537
|
+
count++
|
|
538
|
+
if (NF != 2 || $2 !~ /^[01]$/) {
|
|
539
|
+
invalid=1
|
|
540
|
+
} else {
|
|
541
|
+
value=$2
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
END {
|
|
545
|
+
if (count != 1 || invalid) {
|
|
546
|
+
exit 2
|
|
547
|
+
}
|
|
548
|
+
print value
|
|
549
|
+
}
|
|
550
|
+
')"; then
|
|
551
|
+
return 2
|
|
552
|
+
fi
|
|
553
|
+
|
|
554
|
+
CGROUP_POPULATED="$populated"
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
probe_cgroup_directory() {
|
|
558
|
+
local cgroup_dir="$1"
|
|
559
|
+
local output result mount_id parent_id dir_id events_id namespace_id
|
|
560
|
+
|
|
561
|
+
# lstat keeps a dangling link, permission error, and a genuinely absent cgroup distinct.
|
|
562
|
+
# The mount and parent are identified both inside the probe and across all worker checks.
|
|
563
|
+
# Compare the events node too: kernfs can reinitialize directory timestamps without replacing the cgroup.
|
|
564
|
+
# JavaScript template literals are intentionally passed verbatim.
|
|
565
|
+
# shellcheck disable=SC2016
|
|
566
|
+
if output="$(CGROUP_PROBE_PATH="$cgroup_dir" CGROUP_PROBE_EVENTS="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}" CGROUP_PROBE_MOUNT="${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" node -e '
|
|
567
|
+
const fs = require("node:fs");
|
|
568
|
+
const path = require("node:path");
|
|
569
|
+
const mount = process.env.CGROUP_PROBE_MOUNT;
|
|
570
|
+
const dir = process.env.CGROUP_PROBE_PATH;
|
|
571
|
+
if (!path.isAbsolute(mount) || !path.isAbsolute(dir) || !dir.startsWith(`${mount}/`)) process.exit(2);
|
|
572
|
+
const parent = path.dirname(dir);
|
|
573
|
+
function directoryIdentity(name) {
|
|
574
|
+
const stat = fs.lstatSync(name, { bigint: true });
|
|
575
|
+
if (!stat.isDirectory()) process.exit(2);
|
|
576
|
+
return `${stat.dev}:${stat.ino}`;
|
|
577
|
+
}
|
|
578
|
+
function fileIdentity(name) {
|
|
579
|
+
const stat = fs.lstatSync(name, { bigint: true });
|
|
580
|
+
if (!stat.isFile()) process.exit(2);
|
|
581
|
+
return `${stat.dev}:${stat.ino}`;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const mountId = directoryIdentity(mount);
|
|
585
|
+
let namespaceId;
|
|
586
|
+
try { namespaceId = `${fs.readlinkSync("/proc/self/ns/mnt")}:${fs.readlinkSync("/proc/self/ns/cgroup")}`; }
|
|
587
|
+
catch {
|
|
588
|
+
if (!process.env.SERVICE_CGROUP_MOUNT_TYPE) process.exit(2);
|
|
589
|
+
namespaceId = "fixture-namespace";
|
|
590
|
+
}
|
|
591
|
+
if (process.env.SERVICE_CGROUP_MOUNT_TYPE) {
|
|
592
|
+
if (process.env.SERVICE_CGROUP_MOUNT_TYPE !== "cgroup2fs") process.exit(2);
|
|
593
|
+
} else if (fs.statfsSync(mount, { bigint: true }).type !== 0x63677270n) process.exit(2);
|
|
594
|
+
const parentId = directoryIdentity(parent);
|
|
595
|
+
let dirId = "-";
|
|
596
|
+
let eventsId = "-";
|
|
597
|
+
let absent = false;
|
|
598
|
+
try { dirId = directoryIdentity(dir); }
|
|
599
|
+
catch (error) {
|
|
600
|
+
if (error.code !== "ENOENT") process.exit(2);
|
|
601
|
+
absent = true;
|
|
602
|
+
}
|
|
603
|
+
if (!absent) eventsId = fileIdentity(process.env.CGROUP_PROBE_EVENTS);
|
|
604
|
+
if (directoryIdentity(mount) !== mountId || directoryIdentity(parent) !== parentId) process.exit(2);
|
|
605
|
+
if (absent) {
|
|
606
|
+
try { fs.lstatSync(dir); process.exit(2); }
|
|
607
|
+
catch (error) { if (error.code !== "ENOENT") process.exit(2); }
|
|
608
|
+
}
|
|
609
|
+
process.stdout.write(`${mountId}|${parentId}|${dirId}|${eventsId}|${namespaceId}`);
|
|
610
|
+
process.exit(absent ? 1 : 0);
|
|
611
|
+
} catch { process.exit(2); }
|
|
612
|
+
' 2>/dev/null)"; then
|
|
613
|
+
result=0
|
|
614
|
+
else
|
|
615
|
+
result=$?
|
|
616
|
+
fi
|
|
617
|
+
[ "$result" -eq 0 ] || [ "$result" -eq 1 ] || return 2
|
|
618
|
+
IFS='|' read -r mount_id parent_id dir_id events_id namespace_id <<< "$output"
|
|
619
|
+
[ -n "$mount_id" ] && [ -n "$parent_id" ] && [ -n "$dir_id" ] && [ -n "$events_id" ] && [ -n "$namespace_id" ] || return 2
|
|
620
|
+
if [ -n "$CGROUP_PROBE_MOUNT_ID" ]; then
|
|
621
|
+
[ "$CGROUP_PROBE_MOUNT_ID" = "$mount_id" ] && [ "$CGROUP_PROBE_PARENT_ID" = "$parent_id" ] && \
|
|
622
|
+
[ "$CGROUP_PROBE_NAMESPACE_ID" = "$namespace_id" ] || return 2
|
|
623
|
+
else
|
|
624
|
+
CGROUP_PROBE_MOUNT_ID="$mount_id"
|
|
625
|
+
CGROUP_PROBE_PARENT_ID="$parent_id"
|
|
626
|
+
CGROUP_PROBE_NAMESPACE_ID="$namespace_id"
|
|
627
|
+
fi
|
|
628
|
+
if [ "$result" -eq 1 ]; then
|
|
629
|
+
CGROUP_PROBE_ABSENT="true"
|
|
630
|
+
return 1
|
|
631
|
+
fi
|
|
632
|
+
[ "$CGROUP_PROBE_ABSENT" = "false" ] || return 2
|
|
633
|
+
if [ -n "$CGROUP_PROBE_DIR_ID" ]; then
|
|
634
|
+
[ "$CGROUP_PROBE_DIR_ID" = "$dir_id" ] && [ "$CGROUP_PROBE_EVENTS_ID" = "$events_id" ] || return 2
|
|
635
|
+
else
|
|
636
|
+
CGROUP_PROBE_DIR_ID="$dir_id"
|
|
637
|
+
CGROUP_PROBE_EVENTS_ID="$events_id"
|
|
638
|
+
fi
|
|
639
|
+
return 0
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
capture_service_identity() {
|
|
643
|
+
local active_state main_pid control_pid control_group cgroup_procs cgroup_dir cgroup_events members pid identity member_status
|
|
644
|
+
|
|
645
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
646
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
647
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
648
|
+
control_group="$(service_property ControlGroup 2>/dev/null)" || return 2
|
|
649
|
+
|
|
650
|
+
case "$active_state" in
|
|
651
|
+
active|activating) ;;
|
|
652
|
+
*) return 1 ;;
|
|
653
|
+
esac
|
|
654
|
+
|
|
655
|
+
[ "$main_pid" -gt 0 ] 2>/dev/null || return 1
|
|
656
|
+
[ "$control_pid" = "0" ] || [ "$control_pid" -gt 0 ] 2>/dev/null || return 2
|
|
657
|
+
[ -n "$control_group" ] || return 2
|
|
658
|
+
|
|
659
|
+
cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
|
|
660
|
+
cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
|
|
661
|
+
cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
|
|
662
|
+
if probe_cgroup_directory "$cgroup_dir"; then :; else return 2; fi
|
|
663
|
+
[ -r "$cgroup_procs" ] || return 2
|
|
664
|
+
[ -r "$cgroup_events" ] || return 2
|
|
665
|
+
if read_cgroup_members "$cgroup_procs"; then
|
|
666
|
+
member_status=0
|
|
667
|
+
else
|
|
668
|
+
member_status=$?
|
|
669
|
+
fi
|
|
670
|
+
[ "$member_status" -eq 0 ] || return "$member_status"
|
|
671
|
+
members="$CGROUP_MEMBERS"
|
|
672
|
+
[ -n "$members" ] || return 1
|
|
673
|
+
|
|
674
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES=""
|
|
675
|
+
while IFS= read -r pid; do
|
|
676
|
+
[ -n "$pid" ] || continue
|
|
677
|
+
identity="$(process_identity "$pid")" || return 2
|
|
678
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES="${CAPTURED_SERVICE_MEMBER_IDENTITIES}${identity}\n"
|
|
679
|
+
done <<EOF
|
|
680
|
+
$members
|
|
681
|
+
EOF
|
|
682
|
+
|
|
683
|
+
printf '%s\n' "$members" | grep -qx "$main_pid" || return 1
|
|
684
|
+
CAPTURED_SERVICE_CGROUP_PROCS="$cgroup_procs"
|
|
685
|
+
CAPTURED_SERVICE_CGROUP_DIR="$cgroup_dir"
|
|
686
|
+
CAPTURED_SERVICE_CGROUP_EVENTS="$cgroup_events"
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
service_quiesced() {
|
|
690
|
+
local active_state main_pid control_pid cgroup_dir_status member_status population_status probe_status pid captured_identity current_identity identity_status
|
|
691
|
+
|
|
692
|
+
[ -n "$CAPTURED_SERVICE_CGROUP_PROCS" ] || return 2
|
|
693
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
694
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
695
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
696
|
+
|
|
697
|
+
case "$active_state" in
|
|
698
|
+
inactive|failed) ;;
|
|
699
|
+
*) return 1 ;;
|
|
700
|
+
esac
|
|
701
|
+
|
|
702
|
+
[ "$main_pid" = "0" ] || return 1
|
|
703
|
+
[ "$control_pid" = "0" ] || return 1
|
|
704
|
+
|
|
705
|
+
# Check every member captured before stop independently. This catches KillMode=process
|
|
706
|
+
# survivors even when systemd has already removed the original cgroup directory.
|
|
707
|
+
while IFS= read -r captured_identity; do
|
|
708
|
+
[ -n "$captured_identity" ] || continue
|
|
709
|
+
pid="${captured_identity%%|*}"
|
|
710
|
+
current_identity=""
|
|
711
|
+
if current_identity="$(process_identity "$pid")"; then
|
|
712
|
+
identity_status=0
|
|
713
|
+
else
|
|
714
|
+
identity_status=$?
|
|
715
|
+
fi
|
|
716
|
+
case "$identity_status" in
|
|
717
|
+
0) [ "$current_identity" != "$captured_identity" ] || return 1 ;;
|
|
718
|
+
1) ;;
|
|
719
|
+
*) return 2 ;;
|
|
720
|
+
esac
|
|
721
|
+
done <<EOF
|
|
722
|
+
$(printf '%b' "$CAPTURED_SERVICE_MEMBER_IDENTITIES")
|
|
723
|
+
EOF
|
|
724
|
+
|
|
725
|
+
# The original cgroup may be empty or removed after the service reaches inactive/failed with
|
|
726
|
+
# zero MainPID/ControlPID. A missing membership file while its directory remains is an
|
|
727
|
+
# unavailable probe, not proof that the subtree is empty.
|
|
728
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
729
|
+
if read_cgroup_members "$CAPTURED_SERVICE_CGROUP_PROCS"; then
|
|
730
|
+
member_status=0
|
|
731
|
+
else
|
|
732
|
+
member_status=$?
|
|
733
|
+
fi
|
|
734
|
+
if [ "$member_status" -eq 2 ]; then
|
|
735
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
736
|
+
return 2
|
|
737
|
+
else
|
|
738
|
+
probe_status=$?
|
|
739
|
+
fi
|
|
740
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
741
|
+
return 0
|
|
742
|
+
fi
|
|
743
|
+
[ "$member_status" -eq 0 ] || return "$member_status"
|
|
744
|
+
[ -z "$CGROUP_MEMBERS" ] || return 1
|
|
745
|
+
|
|
746
|
+
if read_cgroup_population "$CAPTURED_SERVICE_CGROUP_EVENTS"; then
|
|
747
|
+
population_status=0
|
|
748
|
+
else
|
|
749
|
+
population_status=$?
|
|
750
|
+
fi
|
|
751
|
+
if [ "$population_status" -eq 2 ]; then
|
|
752
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
753
|
+
return 2
|
|
754
|
+
else
|
|
755
|
+
probe_status=$?
|
|
756
|
+
fi
|
|
757
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
758
|
+
return 0
|
|
759
|
+
fi
|
|
760
|
+
[ "$population_status" -eq 0 ] || return "$population_status"
|
|
761
|
+
[ "$CGROUP_POPULATED" = "0" ] || return 1
|
|
762
|
+
else
|
|
763
|
+
cgroup_dir_status=$?
|
|
764
|
+
case "$cgroup_dir_status" in
|
|
765
|
+
1) ;;
|
|
766
|
+
*) return 2 ;;
|
|
767
|
+
esac
|
|
768
|
+
fi
|
|
769
|
+
|
|
770
|
+
return 0
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
wait_for_quiescence() {
|
|
774
|
+
local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS))
|
|
775
|
+
local result
|
|
776
|
+
|
|
777
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
778
|
+
if service_quiesced; then
|
|
779
|
+
return 0
|
|
780
|
+
else
|
|
781
|
+
result=$?
|
|
782
|
+
fi
|
|
783
|
+
if [ "$result" -eq 2 ]; then
|
|
784
|
+
return 2
|
|
785
|
+
fi
|
|
786
|
+
|
|
787
|
+
sleep 1
|
|
788
|
+
done
|
|
789
|
+
|
|
790
|
+
return 1
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
wait_for_started_service() {
|
|
794
|
+
local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
|
|
795
|
+
local previous_main_pid=""
|
|
796
|
+
local previous_start_identity=""
|
|
797
|
+
local before_main_pid before_start_identity main_pid start_identity
|
|
798
|
+
|
|
799
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
800
|
+
if target_process_is_stable; then
|
|
801
|
+
before_main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
802
|
+
before_start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
803
|
+
|
|
804
|
+
if [ "$before_main_pid" = "0" ] || [ -z "$before_main_pid" ] || [ -z "$before_start_identity" ]; then
|
|
805
|
+
previous_main_pid=""
|
|
806
|
+
previous_start_identity=""
|
|
807
|
+
sleep 1
|
|
808
|
+
continue
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
if target_health_is_valid && target_process_is_stable; then
|
|
812
|
+
main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
813
|
+
start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
814
|
+
|
|
815
|
+
if [ "$before_main_pid" = "$main_pid" ] &&
|
|
816
|
+
[ "$before_start_identity" = "$start_identity" ]; then
|
|
817
|
+
if [ "$main_pid" = "$previous_main_pid" ] &&
|
|
818
|
+
[ "$start_identity" = "$previous_start_identity" ]; then
|
|
819
|
+
return 0
|
|
820
|
+
fi
|
|
821
|
+
|
|
822
|
+
previous_main_pid="$main_pid"
|
|
823
|
+
previous_start_identity="$start_identity"
|
|
824
|
+
else
|
|
825
|
+
previous_main_pid=""
|
|
826
|
+
previous_start_identity=""
|
|
827
|
+
fi
|
|
828
|
+
else
|
|
829
|
+
previous_main_pid=""
|
|
830
|
+
previous_start_identity=""
|
|
831
|
+
fi
|
|
832
|
+
else
|
|
833
|
+
previous_main_pid=""
|
|
834
|
+
previous_start_identity=""
|
|
835
|
+
fi
|
|
836
|
+
|
|
837
|
+
sleep 1
|
|
838
|
+
done
|
|
839
|
+
|
|
840
|
+
return 1
|
|
841
|
+
}
|
|
9
842
|
|
|
10
843
|
# Capture once — used for all status writes so timeout detection works correctly
|
|
11
844
|
STARTED_AT="$(date -Iseconds)"
|
|
@@ -45,7 +878,17 @@ update_status() {
|
|
|
45
878
|
cleanup() {
|
|
46
879
|
local exit_code=$?
|
|
47
880
|
|
|
48
|
-
if [
|
|
881
|
+
if [ -n "${BACKUP_TMP_PATH:-}" ]; then
|
|
882
|
+
rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
|
|
883
|
+
BACKUP_TMP_PATH=""
|
|
884
|
+
fi
|
|
885
|
+
if [ "$exit_code" -ne 0 ] && [ "$FINALIZED" != "true" ]; then
|
|
886
|
+
if [ -n "$ATTEMPT_ID" ] && { [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; }; then
|
|
887
|
+
hold_attempt "interrupted" "Update worker exited with code $exit_code during a state-changing phase"
|
|
888
|
+
elif [ -n "$ATTEMPT_ID" ]; then
|
|
889
|
+
finish_before_migration "failed" "Update process exited with code $exit_code"
|
|
890
|
+
fi
|
|
891
|
+
|
|
49
892
|
# Only write a generic failure if a specific error wasn't already recorded
|
|
50
893
|
if [ -f "$STATUS_FILE" ] && grep -q '"status": "failed"' "$STATUS_FILE" 2>/dev/null; then
|
|
51
894
|
return
|
|
@@ -56,6 +899,27 @@ cleanup() {
|
|
|
56
899
|
|
|
57
900
|
trap cleanup EXIT
|
|
58
901
|
|
|
902
|
+
handle_signal() {
|
|
903
|
+
local signal_name="$1"
|
|
904
|
+
local signal_code="$2"
|
|
905
|
+
|
|
906
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
907
|
+
if [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; then
|
|
908
|
+
hold_attempt "interrupted" "Update worker received SIG${signal_name} after migration entry"
|
|
909
|
+
else
|
|
910
|
+
finish_before_migration "interrupted" "Update worker received SIG${signal_name} before migration entry"
|
|
911
|
+
fi
|
|
912
|
+
fi
|
|
913
|
+
|
|
914
|
+
update_status "failed" "failed" "Update worker received SIG${signal_name}"
|
|
915
|
+
FINALIZED="true"
|
|
916
|
+
exit "$signal_code"
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
trap 'handle_signal TERM 143' TERM
|
|
920
|
+
trap 'handle_signal INT 130' INT
|
|
921
|
+
trap 'handle_signal HUP 129' HUP
|
|
922
|
+
|
|
59
923
|
# ──────────────────────────────────────────────────────────────
|
|
60
924
|
# Image-based update (Raspbian image installs)
|
|
61
925
|
# ──────────────────────────────────────────────────────────────
|
|
@@ -64,6 +928,85 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
64
928
|
CLEAN_VERSION="${VERSION#v}"
|
|
65
929
|
NEW_VERSION_DIR="${IMAGE_BASE_DIR}/v${CLEAN_VERSION}"
|
|
66
930
|
CURRENT_LINK="${IMAGE_BASE_DIR}/current"
|
|
931
|
+
ENV_FILE="${ENV_FILE:-/etc/smart-panel/environment}"
|
|
932
|
+
|
|
933
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
934
|
+
case "$VERSION" in latest|vlatest|'')
|
|
935
|
+
update_status "failed" "failed" "Stopped maintenance requires an exact target version"
|
|
936
|
+
FINALIZED="true"
|
|
937
|
+
exit 1
|
|
938
|
+
;; esac
|
|
939
|
+
for required in EXPECTED_CURRENT_VERSION TARGET_ARCHIVE_SHA256 TARGET_WORKER_SHA256 EXPECTED_IMAGE_BASE_DIR \
|
|
940
|
+
EXPECTED_STATUS_FILE EXPECTED_ATTEMPT_DIR EXPECTED_DB_PATH DB_BACKUP_PATH; do
|
|
941
|
+
if [ -z "${!required:-}" ]; then
|
|
942
|
+
update_status "failed" "failed" "Stopped maintenance identity is missing: ${required}"
|
|
943
|
+
FINALIZED="true"
|
|
944
|
+
exit 1
|
|
945
|
+
fi
|
|
946
|
+
done
|
|
947
|
+
TARGET_ARCHIVE_SHA256="$(printf '%s' "$TARGET_ARCHIVE_SHA256" | tr '[:upper:]' '[:lower:]')"
|
|
948
|
+
TARGET_WORKER_SHA256="$(printf '%s' "$TARGET_WORKER_SHA256" | tr '[:upper:]' '[:lower:]')"
|
|
949
|
+
case "$TARGET_ARCHIVE_SHA256$TARGET_WORKER_SHA256" in *[!0123456789abcdef]*)
|
|
950
|
+
update_status "failed" "failed" "Stopped maintenance checksum is invalid"
|
|
951
|
+
FINALIZED="true"
|
|
952
|
+
exit 1
|
|
953
|
+
;; esac
|
|
954
|
+
[ "${#TARGET_ARCHIVE_SHA256}" -eq 64 ] && [ "${#TARGET_WORKER_SHA256}" -eq 64 ] || {
|
|
955
|
+
update_status "failed" "failed" "Stopped maintenance checksum must be SHA-256"
|
|
956
|
+
FINALIZED="true"
|
|
957
|
+
exit 1
|
|
958
|
+
}
|
|
959
|
+
[ "$EXPECTED_IMAGE_BASE_DIR" = "$IMAGE_BASE_DIR" ] || {
|
|
960
|
+
update_status "failed" "failed" "Stopped maintenance image base identity mismatch"
|
|
961
|
+
FINALIZED="true"
|
|
962
|
+
exit 1
|
|
963
|
+
}
|
|
964
|
+
[ "$EXPECTED_STATUS_FILE" = "$STATUS_FILE" ] || {
|
|
965
|
+
update_status "failed" "failed" "Stopped maintenance status identity mismatch"
|
|
966
|
+
FINALIZED="true"
|
|
967
|
+
exit 1
|
|
968
|
+
}
|
|
969
|
+
[ "$EXPECTED_ATTEMPT_DIR" = "$ATTEMPT_DIR" ] || {
|
|
970
|
+
update_status "failed" "failed" "Stopped maintenance attempt identity mismatch"
|
|
971
|
+
FINALIZED="true"
|
|
972
|
+
exit 1
|
|
973
|
+
}
|
|
974
|
+
[ "$EXPECTED_DB_PATH" = "${FB_DB_PATH:-}" ] || {
|
|
975
|
+
update_status "failed" "failed" "Stopped maintenance database identity mismatch"
|
|
976
|
+
FINALIZED="true"
|
|
977
|
+
exit 1
|
|
978
|
+
}
|
|
979
|
+
if [ -f "$ENV_FILE" ]; then
|
|
980
|
+
configured_db_path="$(
|
|
981
|
+
set -a
|
|
982
|
+
# shellcheck source=/dev/null
|
|
983
|
+
. "$ENV_FILE"
|
|
984
|
+
printf '%s' "${FB_DB_PATH:-}"
|
|
985
|
+
)"
|
|
986
|
+
[ "$configured_db_path" = "$EXPECTED_DB_PATH" ] || \
|
|
987
|
+
fail_before_migration "preflight" "Configured database identity does not match stopped-maintenance packet"
|
|
988
|
+
fi
|
|
989
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
990
|
+
fail_before_migration "preflight" "Stopped maintenance backup path already exists"
|
|
991
|
+
fi
|
|
992
|
+
SOURCE_DB_PATH="${FB_DB_PATH%/}/database.sqlite"
|
|
993
|
+
source_database_is_valid || fail_before_migration "preflight" "Stopped maintenance source database is missing, symlinked, or empty"
|
|
994
|
+
fi
|
|
995
|
+
|
|
996
|
+
# Image updates must verify the target release through the existing local health route. A
|
|
997
|
+
# missing route would otherwise silently reduce readiness to a PID check and reintroduce the
|
|
998
|
+
# startup/worker circular wait that the updater is designed to avoid.
|
|
999
|
+
if [ -z "$HEALTH_URL" ]; then
|
|
1000
|
+
update_status "failed" "failed" "No local health URL configured for image update"
|
|
1001
|
+
FINALIZED="true"
|
|
1002
|
+
exit 1
|
|
1003
|
+
fi
|
|
1004
|
+
|
|
1005
|
+
if ! acquire_attempt; then
|
|
1006
|
+
update_status "failed" "failed" "An unresolved update attempt already owns the updater"
|
|
1007
|
+
FINALIZED="true"
|
|
1008
|
+
exit 1
|
|
1009
|
+
fi
|
|
67
1010
|
|
|
68
1011
|
# Verify passwordless sudo is available for all required commands.
|
|
69
1012
|
# Without -n, sudo may hang waiting for a password on a detached process.
|
|
@@ -77,8 +1020,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
77
1020
|
|
|
78
1021
|
for cmd in "${SUDO_CMDS[@]}"; do
|
|
79
1022
|
if ! sudo -n -l $cmd >/dev/null 2>&1; then
|
|
80
|
-
|
|
81
|
-
exit 1
|
|
1023
|
+
fail_before_migration "preflight" "Passwordless sudo not available for: $cmd"
|
|
82
1024
|
fi
|
|
83
1025
|
done
|
|
84
1026
|
PREVIOUS_TARGET=""
|
|
@@ -86,6 +1028,24 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
86
1028
|
# Save the current version for rollback
|
|
87
1029
|
if [ -L "$CURRENT_LINK" ]; then
|
|
88
1030
|
PREVIOUS_TARGET=$(readlink "$CURRENT_LINK")
|
|
1031
|
+
case "$PREVIOUS_TARGET" in
|
|
1032
|
+
/*) ;;
|
|
1033
|
+
*) PREVIOUS_TARGET="${IMAGE_BASE_DIR}/${PREVIOUS_TARGET}" ;;
|
|
1034
|
+
esac
|
|
1035
|
+
fi
|
|
1036
|
+
|
|
1037
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1038
|
+
EXPECTED_CURRENT_LINK="${IMAGE_BASE_DIR}/v${EXPECTED_CURRENT_VERSION#v}"
|
|
1039
|
+
RESOLVED_CURRENT="$(realpath "$CURRENT_LINK" 2>/dev/null || true)"
|
|
1040
|
+
RESOLVED_EXPECTED="$(realpath "$EXPECTED_CURRENT_LINK" 2>/dev/null || true)"
|
|
1041
|
+
[ -n "$RESOLVED_CURRENT" ] && [ "$RESOLVED_CURRENT" = "$RESOLVED_EXPECTED" ] || \
|
|
1042
|
+
fail_before_migration "preflight" "Current release/link identity does not match stopped-maintenance packet"
|
|
1043
|
+
if ! wait_for_stopped_service; then
|
|
1044
|
+
hold_attempt "stopping" "Stopped service did not reach verified quiescence"
|
|
1045
|
+
update_status "failed" "failed" "Stopped service quiescence could not be verified; update is held for recovery"
|
|
1046
|
+
FINALIZED="true"
|
|
1047
|
+
exit 1
|
|
1048
|
+
fi
|
|
89
1049
|
fi
|
|
90
1050
|
|
|
91
1051
|
# Guard: refuse to overwrite the currently running version
|
|
@@ -94,25 +1054,28 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
94
1054
|
RESOLVED_NEW=$(cd "$IMAGE_BASE_DIR" && realpath "$NEW_VERSION_DIR" 2>/dev/null || true)
|
|
95
1055
|
|
|
96
1056
|
if [ -n "$PREVIOUS_TARGET" ] && [ -n "$RESOLVED_PREV" ] && [ -n "$RESOLVED_NEW" ] && [ "$RESOLVED_PREV" = "$RESOLVED_NEW" ]; then
|
|
97
|
-
|
|
98
|
-
exit 1
|
|
1057
|
+
fail_before_migration "preflight" "Target version v${CLEAN_VERSION} is already the active version"
|
|
99
1058
|
fi
|
|
100
1059
|
|
|
101
1060
|
# ── Download ──
|
|
102
1061
|
update_status "downloading" "downloading"
|
|
103
1062
|
|
|
104
1063
|
if [ -z "$DOWNLOAD_URL" ]; then
|
|
105
|
-
|
|
106
|
-
exit 1
|
|
1064
|
+
fail_before_migration "downloading" "No download URL provided for image update"
|
|
107
1065
|
fi
|
|
108
1066
|
|
|
109
1067
|
TMP_TARBALL="/tmp/smart-panel-backend-v${CLEAN_VERSION}.tar.gz"
|
|
110
1068
|
|
|
111
1069
|
curl -fSL -o "$TMP_TARBALL" "$DOWNLOAD_URL" 2>&1 || {
|
|
112
|
-
|
|
113
|
-
exit 1
|
|
1070
|
+
fail_before_migration "downloading" "Download failed from ${DOWNLOAD_URL}"
|
|
114
1071
|
}
|
|
115
1072
|
|
|
1073
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1074
|
+
actual_archive_sha256="$(sha256sum "$TMP_TARBALL" 2>/dev/null | awk '{print $1}')"
|
|
1075
|
+
[ "$actual_archive_sha256" = "$TARGET_ARCHIVE_SHA256" ] || \
|
|
1076
|
+
fail_before_migration "downloading" "Downloaded archive checksum does not match maintenance packet"
|
|
1077
|
+
fi
|
|
1078
|
+
|
|
116
1079
|
# ── Extract ──
|
|
117
1080
|
update_status "installing" "installing"
|
|
118
1081
|
|
|
@@ -121,17 +1084,36 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
121
1084
|
tar -xzf "$TMP_TARBALL" -C "$NEW_VERSION_DIR" 2>&1 || {
|
|
122
1085
|
rm -rf "$NEW_VERSION_DIR"
|
|
123
1086
|
rm -f "$TMP_TARBALL"
|
|
124
|
-
|
|
125
|
-
exit 1
|
|
1087
|
+
fail_before_migration "installing" "Failed to extract update archive"
|
|
126
1088
|
}
|
|
127
1089
|
|
|
128
1090
|
rm -f "$TMP_TARBALL"
|
|
129
1091
|
|
|
1092
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1093
|
+
target_worker="${NEW_VERSION_DIR}/dist/modules/system/scripts/update-worker.sh"
|
|
1094
|
+
[ -f "$target_worker" ] || {
|
|
1095
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1096
|
+
fail_before_migration "installing" "Target release worker is missing"
|
|
1097
|
+
}
|
|
1098
|
+
actual_worker_sha256="$(sha256sum "$target_worker" 2>/dev/null | awk '{print $1}')"
|
|
1099
|
+
[ "$actual_worker_sha256" = "$TARGET_WORKER_SHA256" ] || {
|
|
1100
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1101
|
+
fail_before_migration "installing" "Target release worker checksum does not match maintenance packet"
|
|
1102
|
+
}
|
|
1103
|
+
MAINTENANCE_DIR="${MAINTENANCE_DIR:-${ATTEMPT_DIR}/maintenance-${ATTEMPT_ID}}"
|
|
1104
|
+
case "$MAINTENANCE_DIR" in
|
|
1105
|
+
"$IMAGE_BASE_DIR"/*|"$NEW_VERSION_DIR"/*) fail_before_migration "installing" "Maintenance directory is inside a release tree" ;;
|
|
1106
|
+
esac
|
|
1107
|
+
mkdir -p "$MAINTENANCE_DIR"
|
|
1108
|
+
chmod 700 "$MAINTENANCE_DIR"
|
|
1109
|
+
cp "$target_worker" "$MAINTENANCE_DIR/update-worker.sh"
|
|
1110
|
+
chmod 700 "$MAINTENANCE_DIR/update-worker.sh"
|
|
1111
|
+
fi
|
|
1112
|
+
|
|
130
1113
|
# Create the image-install marker in the new version
|
|
131
1114
|
touch "${NEW_VERSION_DIR}/.image-install" || {
|
|
132
1115
|
rm -rf "$NEW_VERSION_DIR"
|
|
133
|
-
|
|
134
|
-
exit 1
|
|
1116
|
+
fail_before_migration "installing" "Failed to create image-install marker"
|
|
135
1117
|
}
|
|
136
1118
|
|
|
137
1119
|
# ── Verify dependencies ──
|
|
@@ -143,8 +1125,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
143
1125
|
if [ ! -d "node_modules" ]; then
|
|
144
1126
|
npm install --omit=dev 2>&1 || {
|
|
145
1127
|
rm -rf "$NEW_VERSION_DIR"
|
|
146
|
-
|
|
147
|
-
exit 1
|
|
1128
|
+
fail_before_migration "installing" "npm install failed"
|
|
148
1129
|
}
|
|
149
1130
|
fi
|
|
150
1131
|
|
|
@@ -153,72 +1134,196 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
153
1134
|
# hanging on password prompt when running as a detached process.
|
|
154
1135
|
sudo -n chown -R smart-panel:smart-panel "$NEW_VERSION_DIR" || {
|
|
155
1136
|
rm -rf "$NEW_VERSION_DIR"
|
|
156
|
-
|
|
157
|
-
exit 1
|
|
1137
|
+
fail_before_migration "installing" "Failed to set ownership on ${NEW_VERSION_DIR}"
|
|
158
1138
|
}
|
|
159
1139
|
|
|
160
|
-
# ── Stop service ──
|
|
1140
|
+
# ── Stop/qualify service ──
|
|
161
1141
|
update_status "stopping" "stopping"
|
|
162
|
-
|
|
1142
|
+
write_attempt "active" "stopping" "" "false" "$MIGRATION_ENTERED"
|
|
1143
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1144
|
+
if ! wait_for_stopped_service; then
|
|
1145
|
+
hold_attempt "stopping" "Stopped service changed or failed quiescence recheck"
|
|
1146
|
+
update_status "failed" "failed" "Stopped service quiescence could not be rechecked; update is held for recovery"
|
|
1147
|
+
FINALIZED="true"
|
|
1148
|
+
exit 1
|
|
1149
|
+
fi
|
|
1150
|
+
else
|
|
1151
|
+
if ! capture_service_identity; then
|
|
1152
|
+
hold_attempt "stopping" "Could not capture service process/cgroup identity before stop"
|
|
1153
|
+
update_status "failed" "failed" "Service process/cgroup identity could not be captured; update is held for recovery"
|
|
1154
|
+
FINALIZED="true"
|
|
1155
|
+
exit 1
|
|
1156
|
+
fi
|
|
1157
|
+
|
|
1158
|
+
if ! sudo -n systemctl stop "$SERVICE_NAME" 2>/dev/null; then
|
|
1159
|
+
hold_attempt "stopping" "Service stop failed; writer quiescence is unknown"
|
|
1160
|
+
update_status "failed" "failed" "Could not stop ${SERVICE_NAME}; update is held for recovery"
|
|
1161
|
+
FINALIZED="true"
|
|
1162
|
+
exit 1
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
if ! wait_for_quiescence; then
|
|
1166
|
+
hold_attempt "stopping" "Service did not reach verified quiescence before migration"
|
|
1167
|
+
update_status "failed" "failed" "Service quiescence could not be verified; update is held for recovery"
|
|
1168
|
+
FINALIZED="true"
|
|
1169
|
+
exit 1
|
|
1170
|
+
fi
|
|
1171
|
+
|
|
1172
|
+
write_attempt "active" "quiesced" "" "false" "$MIGRATION_ENTERED"
|
|
1173
|
+
fi
|
|
1174
|
+
|
|
1175
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1176
|
+
# A consistent backup is an input safeguard, never a rollback mechanism. SQLite's own
|
|
1177
|
+
# backup API is required so a copied WAL database cannot be mistaken for a snapshot.
|
|
1178
|
+
command -v sqlite3 >/dev/null 2>&1 || {
|
|
1179
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1180
|
+
fail_before_migration "quiesced" "sqlite3 is required for stopped-maintenance backup"
|
|
1181
|
+
}
|
|
1182
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
1183
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1184
|
+
fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
|
|
1185
|
+
fi
|
|
1186
|
+
source_database_is_valid || {
|
|
1187
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1188
|
+
fail_before_migration "quiesced" "Stopped maintenance source database became unavailable"
|
|
1189
|
+
}
|
|
1190
|
+
mkdir -p "$(dirname "$DB_BACKUP_PATH")" || {
|
|
1191
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1192
|
+
fail_before_migration "quiesced" "Could not create SQLite backup directory"
|
|
1193
|
+
}
|
|
1194
|
+
BACKUP_TMP_PATH="$(mktemp "${DB_BACKUP_PATH}.tmp-${ATTEMPT_ID}.XXXXXX")" || {
|
|
1195
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1196
|
+
fail_before_migration "quiesced" "Could not create temporary SQLite backup"
|
|
1197
|
+
}
|
|
1198
|
+
if ! sqlite3 "${SOURCE_DB_PATH}" ".backup '${BACKUP_TMP_PATH}'"; then
|
|
1199
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1200
|
+
BACKUP_TMP_PATH=""
|
|
1201
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1202
|
+
fail_before_migration "quiesced" "Consistent SQLite backup failed"
|
|
1203
|
+
fi
|
|
1204
|
+
if [ ! -s "$BACKUP_TMP_PATH" ]; then
|
|
1205
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1206
|
+
BACKUP_TMP_PATH=""
|
|
1207
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1208
|
+
fail_before_migration "quiesced" "Consistent SQLite backup is empty"
|
|
1209
|
+
fi
|
|
1210
|
+
if ! ln "$BACKUP_TMP_PATH" "$DB_BACKUP_PATH" 2>/dev/null; then
|
|
1211
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1212
|
+
BACKUP_TMP_PATH=""
|
|
1213
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1214
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
1215
|
+
fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
|
|
1216
|
+
fi
|
|
1217
|
+
fail_before_migration "quiesced" "Consistent SQLite backup publish failed"
|
|
1218
|
+
fi
|
|
1219
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1220
|
+
BACKUP_TMP_PATH=""
|
|
1221
|
+
fi
|
|
1222
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ] && ! stopped_service_quiesced; then
|
|
1223
|
+
hold_attempt "quiesced" "Stopped service changed after backup before switch"
|
|
1224
|
+
update_status "failed" "failed" "Stopped service changed after backup; update is held for recovery"
|
|
1225
|
+
FINALIZED="true"
|
|
1226
|
+
exit 1
|
|
1227
|
+
fi
|
|
163
1228
|
|
|
164
1229
|
# ── Switch symlink (atomic on same filesystem) ──
|
|
165
1230
|
sudo -n ln -sfn "$NEW_VERSION_DIR" "$CURRENT_LINK" || {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1231
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1232
|
+
hold_attempt "switching" "Failed to switch version symlink in stopped maintenance"
|
|
1233
|
+
update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
|
|
1234
|
+
FINALIZED="true"
|
|
1235
|
+
exit 1
|
|
169
1236
|
fi
|
|
170
|
-
|
|
171
|
-
|
|
1237
|
+
# Revert only when the old link, target cleanup and old-service health are all verified.
|
|
1238
|
+
if restore_before_migration "$PREVIOUS_TARGET" "$CURRENT_LINK" "$NEW_VERSION_DIR"; then
|
|
1239
|
+
fail_before_migration "switching" "Failed to switch version symlink"
|
|
1240
|
+
fi
|
|
1241
|
+
|
|
1242
|
+
hold_attempt "switching" "Failed to switch version symlink and recovery start was not verified"
|
|
1243
|
+
update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
|
|
1244
|
+
FINALIZED="true"
|
|
172
1245
|
exit 1
|
|
173
1246
|
}
|
|
174
1247
|
|
|
175
1248
|
# ── Run database migrations ──
|
|
176
1249
|
update_status "migrating" "migrating"
|
|
177
1250
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
update_status "stopping" "stopping"
|
|
191
|
-
sudo -n systemctl stop smart-panel 2>/dev/null || true
|
|
192
|
-
|
|
193
|
-
if [ -n "$PREVIOUS_TARGET" ]; then
|
|
194
|
-
if sudo -n ln -sfn "$PREVIOUS_TARGET" "$CURRENT_LINK"; then
|
|
195
|
-
rm -rf "$NEW_VERSION_DIR"
|
|
196
|
-
update_status "failed" "failed" "Database migration failed — reverted to previous version"
|
|
197
|
-
else
|
|
198
|
-
# Symlink revert failed — keep new version so system stays bootable
|
|
199
|
-
update_status "failed" "failed" "Database migration failed — symlink revert also failed"
|
|
200
|
-
fi
|
|
201
|
-
else
|
|
202
|
-
# No previous version — keep current in place so system stays bootable
|
|
203
|
-
update_status "failed" "failed" "Database migration failed — no previous version to revert to"
|
|
204
|
-
fi
|
|
1251
|
+
if [ ! -f "${NEW_VERSION_DIR}/dist/dataSource.js" ]; then
|
|
1252
|
+
hold_attempt "migrating" "Target data source is missing; update is held for recovery"
|
|
1253
|
+
update_status "failed" "failed" "Target data source is missing; update is held for recovery"
|
|
1254
|
+
FINALIZED="true"
|
|
1255
|
+
exit 1
|
|
1256
|
+
fi
|
|
1257
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ] && ! stopped_service_quiesced; then
|
|
1258
|
+
hold_attempt "migrating" "Stopped service changed before migration entry"
|
|
1259
|
+
update_status "failed" "failed" "Stopped service changed before migration entry; update is held for recovery"
|
|
1260
|
+
FINALIZED="true"
|
|
1261
|
+
exit 1
|
|
1262
|
+
fi
|
|
205
1263
|
|
|
206
|
-
|
|
207
|
-
|
|
1264
|
+
MIGRATION_ENTERED="true"
|
|
1265
|
+
if ! write_attempt "active" "migrating" "" "false" "true"; then
|
|
1266
|
+
hold_attempt "migrating" "Could not persist migration-entry checkpoint"
|
|
1267
|
+
update_status "failed" "failed" "Could not persist migration-entry checkpoint; update is held for recovery"
|
|
1268
|
+
FINALIZED="true"
|
|
1269
|
+
exit 1
|
|
1270
|
+
fi
|
|
1271
|
+
|
|
1272
|
+
MIGRATION_LOG="${ATTEMPT_DIR}/${ATTEMPT_ID}.migration.log"
|
|
1273
|
+
: > "$MIGRATION_LOG"
|
|
1274
|
+
chmod 600 "$MIGRATION_LOG"
|
|
1275
|
+
write_attempt "active" "migrating" "" "false" "true"
|
|
1276
|
+
|
|
1277
|
+
set +e
|
|
1278
|
+
(
|
|
1279
|
+
set -a
|
|
1280
|
+
# shellcheck source=/dev/null
|
|
1281
|
+
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
|
1282
|
+
set +a
|
|
1283
|
+
cd "$NEW_VERSION_DIR"
|
|
1284
|
+
node node_modules/typeorm/cli.js migration:run -d dist/dataSource.js
|
|
1285
|
+
) 2>&1 | awk -v output="$MIGRATION_LOG" '
|
|
1286
|
+
BEGIN { limit = 65536; bytes = 0 }
|
|
1287
|
+
{
|
|
1288
|
+
if (bytes < limit) {
|
|
1289
|
+
line = $0 ORS;
|
|
1290
|
+
remaining = limit - bytes;
|
|
1291
|
+
if (length(line) > remaining) line = substr(line, 1, remaining);
|
|
1292
|
+
printf "%s", line >> output;
|
|
1293
|
+
bytes += length(line);
|
|
1294
|
+
}
|
|
208
1295
|
}
|
|
1296
|
+
'
|
|
1297
|
+
pipeline_status=("${PIPESTATUS[@]}")
|
|
1298
|
+
migration_exit_code="${pipeline_status[0]}"
|
|
1299
|
+
capture_exit_code="${pipeline_status[1]}"
|
|
1300
|
+
set -e
|
|
1301
|
+
|
|
1302
|
+
if [ "$capture_exit_code" -ne 0 ]; then
|
|
1303
|
+
hold_attempt "migration_capture_failed" "Could not retain bounded migration diagnostics"
|
|
1304
|
+
update_status "failed" "failed" "Migration diagnostics could not be retained; update is held for recovery"
|
|
1305
|
+
FINALIZED="true"
|
|
1306
|
+
exit 1
|
|
1307
|
+
fi
|
|
1308
|
+
|
|
1309
|
+
if [ "$migration_exit_code" -eq 0 ]; then
|
|
1310
|
+
write_attempt "active" "migrated" "" "false" "true"
|
|
1311
|
+
else
|
|
1312
|
+
hold_attempt "migration_failed" "Database migration failed with exit code ${migration_exit_code}"
|
|
1313
|
+
update_status "failed" "failed" "Database migration failed; update is held for recovery"
|
|
1314
|
+
FINALIZED="true"
|
|
1315
|
+
exit "$migration_exit_code"
|
|
209
1316
|
fi
|
|
210
1317
|
|
|
211
1318
|
# ── Start service ──
|
|
212
1319
|
update_status "starting" "starting"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
fi
|
|
219
|
-
update_status "failed" "failed" "Failed to start service after update"
|
|
1320
|
+
write_attempt "active" "starting" "" "false" "true"
|
|
1321
|
+
if ! sudo -n systemctl start "$SERVICE_NAME" 2>&1 || ! wait_for_started_service; then
|
|
1322
|
+
hold_attempt "start_failed" "Target service did not reach verified health after migration"
|
|
1323
|
+
update_status "failed" "failed" "Target service failed to start or become healthy; update is held for recovery"
|
|
1324
|
+
FINALIZED="true"
|
|
220
1325
|
exit 1
|
|
221
|
-
|
|
1326
|
+
fi
|
|
222
1327
|
|
|
223
1328
|
# ── Cleanup old versions (keep max 2 previous) ──
|
|
224
1329
|
cd "$IMAGE_BASE_DIR"
|
|
@@ -239,7 +1344,9 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
239
1344
|
fi
|
|
240
1345
|
|
|
241
1346
|
# ── Mark complete ──
|
|
1347
|
+
release_attempt
|
|
242
1348
|
update_status "complete" "complete"
|
|
1349
|
+
FINALIZED="true"
|
|
243
1350
|
trap - EXIT
|
|
244
1351
|
exit 0
|
|
245
1352
|
fi
|