@fastybird/smart-panel-backend 1.1.0-alpha.2 → 1.1.0-alpha.21
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/common/utils/transform.utils.js +3 -0
- package/dist/common/utils/transform.utils.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 +18 -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 +27 -3
- package/dist/modules/devices/services/channels.properties.service.js +304 -28
- 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 +204 -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-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.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/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 +983 -61
- package/dist/modules/system/services/privileged-worker.service.d.ts +10 -0
- package/dist/modules/system/services/privileged-worker.service.js +137 -11
- package/dist/modules/system/services/privileged-worker.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 +257 -25
- 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 +2 -0
- package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js +28 -1
- package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js.map +1 -1
- package/dist/plugins/devices-homekit/mappers/base.mapper.js +27 -4
- 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/homekit-mapper.interface.d.ts +16 -0
- package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.d.ts +7 -0
- package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.js +84 -73
- 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 +38 -3
- package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.js +840 -134
- 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 +15 -1
- package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.js +117 -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 +11 -13
- 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 +11 -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 +130 -7
- 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 +48 -0
- package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.js +457 -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 +12 -2
- package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.js +86 -28
- 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 -1
- package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js +18 -13
- package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js.map +1 -1
- package/dist/plugins/devices-shelly-ng/utils/transform.utils.d.ts +2 -0
- package/dist/plugins/devices-shelly-ng/utils/transform.utils.js +14 -1
- package/dist/plugins/devices-shelly-ng/utils/transform.utils.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 +2 -2
|
@@ -6,6 +6,666 @@ 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
|
+
ATTEMPT_ID=""
|
|
28
|
+
MIGRATION_ENTERED="false"
|
|
29
|
+
FINALIZED="false"
|
|
30
|
+
MIGRATION_LOG=""
|
|
31
|
+
CURRENT_PHASE="preparing"
|
|
32
|
+
SERVICE_NAME="smart-panel"
|
|
33
|
+
SERVICE_UNIT="${SERVICE_UNIT:-${SERVICE_NAME}.service}"
|
|
34
|
+
QUIESCENCE_TIMEOUT_SECONDS="${QUIESCENCE_TIMEOUT_SECONDS:-30}"
|
|
35
|
+
START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-60}"
|
|
36
|
+
CAPTURED_SERVICE_CGROUP_PROCS=""
|
|
37
|
+
CAPTURED_SERVICE_CGROUP_DIR=""
|
|
38
|
+
CAPTURED_SERVICE_CGROUP_EVENTS=""
|
|
39
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES=""
|
|
40
|
+
CGROUP_MEMBERS=""
|
|
41
|
+
CGROUP_POPULATED=""
|
|
42
|
+
MAINTENANCE_DIR=""
|
|
43
|
+
DB_BACKUP_PATH="${DB_BACKUP_PATH:-}"
|
|
44
|
+
BACKUP_TMP_PATH=""
|
|
45
|
+
SOURCE_DB_PATH=""
|
|
46
|
+
STOPPED_SERVICE_BASELINE=""
|
|
47
|
+
|
|
48
|
+
json_escape() {
|
|
49
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/[[:cntrl:]]//g'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
source_database_is_valid() {
|
|
53
|
+
[ -n "${SOURCE_DB_PATH:-}" ] || return 1
|
|
54
|
+
[ ! -L "$SOURCE_DB_PATH" ] && [ -f "$SOURCE_DB_PATH" ] && [ -s "$SOURCE_DB_PATH" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
write_attempt() {
|
|
58
|
+
local state="$1"
|
|
59
|
+
local phase="$2"
|
|
60
|
+
local error="${3:-}"
|
|
61
|
+
local recovery_required="${4:-false}"
|
|
62
|
+
local migration_entered="${5:-$MIGRATION_ENTERED}"
|
|
63
|
+
local tmp_file="${ATTEMPT_FILE}.tmp.$$"
|
|
64
|
+
CURRENT_PHASE="$phase"
|
|
65
|
+
|
|
66
|
+
mkdir -p "$ATTEMPT_DIR"
|
|
67
|
+
chmod 700 "$ATTEMPT_DIR"
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
printf '{\n'
|
|
71
|
+
printf '\t"attemptId": "%s",\n' "$(json_escape "$ATTEMPT_ID")"
|
|
72
|
+
printf '\t"ownerPid": %s,\n' "$$"
|
|
73
|
+
printf '\t"targetVersion": "%s",\n' "$(json_escape "$VERSION")"
|
|
74
|
+
printf '\t"state": "%s",\n' "$(json_escape "$state")"
|
|
75
|
+
printf '\t"phase": "%s",\n' "$(json_escape "$phase")"
|
|
76
|
+
printf '\t"migrationEntered": %s,\n' "$migration_entered"
|
|
77
|
+
printf '\t"recoveryRequired": %s,\n' "$recovery_required"
|
|
78
|
+
printf '\t"migrationLog": "%s",\n' "$(json_escape "$MIGRATION_LOG")"
|
|
79
|
+
printf '\t"updatedAt": "%s",\n' "$(date -Iseconds)"
|
|
80
|
+
printf '\t"error": "%s"\n' "$(json_escape "$error")"
|
|
81
|
+
printf '}\n'
|
|
82
|
+
} > "$tmp_file"
|
|
83
|
+
|
|
84
|
+
chmod 600 "$tmp_file"
|
|
85
|
+
mv -f "$tmp_file" "$ATTEMPT_FILE"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
attempt_is_unresolved() {
|
|
89
|
+
if [ -f "$ATTEMPT_FILE" ] && grep -q '"state": "complete"' "$ATTEMPT_FILE"; then
|
|
90
|
+
# A crash after the durable completion record but before rmdir must not strand a clean
|
|
91
|
+
# attempt forever; completion is the evidence that makes this lock removable.
|
|
92
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
93
|
+
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if [ -d "$ATTEMPT_LOCK" ]; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ ! -f "$ATTEMPT_FILE" ]; then
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
grep -q '"state": "active"' "$ATTEMPT_FILE" || grep -q '"recoveryRequired": true' "$ATTEMPT_FILE"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
acquire_attempt() {
|
|
109
|
+
mkdir -p "$ATTEMPT_DIR"
|
|
110
|
+
chmod 700 "$ATTEMPT_DIR"
|
|
111
|
+
|
|
112
|
+
if attempt_is_unresolved; then
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# mkdir is the portable atomic OS-level lock used by the legacy launcher. An orphaned lock is
|
|
117
|
+
# deliberately retained; an expired timestamp cannot prove that a migration did not commit.
|
|
118
|
+
if ! mkdir "$ATTEMPT_LOCK" 2>/dev/null; then
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
chmod 700 "$ATTEMPT_LOCK"
|
|
122
|
+
|
|
123
|
+
ATTEMPT_ID="$(date +%s)-$$"
|
|
124
|
+
write_attempt "active" "preparing" "" "false" "false"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
release_attempt() {
|
|
128
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
129
|
+
write_attempt "complete" "complete" "" "false" "$MIGRATION_ENTERED"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
hold_attempt() {
|
|
136
|
+
local phase="$1"
|
|
137
|
+
local error="$2"
|
|
138
|
+
|
|
139
|
+
write_attempt "recovery_required" "$phase" "$error" "true" "$MIGRATION_ENTERED"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
finish_before_migration() {
|
|
143
|
+
local phase="$1"
|
|
144
|
+
local error="$2"
|
|
145
|
+
|
|
146
|
+
if [ -n "${BACKUP_TMP_PATH:-}" ]; then
|
|
147
|
+
rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
|
|
148
|
+
BACKUP_TMP_PATH=""
|
|
149
|
+
fi
|
|
150
|
+
if [ -n "${TMP_TARBALL:-}" ]; then
|
|
151
|
+
rm -f "$TMP_TARBALL" 2>/dev/null || true
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
write_attempt "failed_preflight" "$phase" "$error" "false" "false"
|
|
155
|
+
rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
phase_requires_hold() {
|
|
159
|
+
case "$CURRENT_PHASE" in
|
|
160
|
+
stopping|quiesced|switching) return 0 ;;
|
|
161
|
+
*) return 1 ;;
|
|
162
|
+
esac
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fail_before_migration() {
|
|
166
|
+
local phase="$1"
|
|
167
|
+
local error="$2"
|
|
168
|
+
|
|
169
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
170
|
+
finish_before_migration "$phase" "$error"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
update_status "failed" "failed" "$error"
|
|
174
|
+
FINALIZED="true"
|
|
175
|
+
exit 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
restore_before_migration() {
|
|
179
|
+
local previous_target="$1"
|
|
180
|
+
local current_link="$2"
|
|
181
|
+
local new_version_dir="$3"
|
|
182
|
+
local resolved_previous resolved_current
|
|
183
|
+
|
|
184
|
+
[ -n "$previous_target" ] || return 1
|
|
185
|
+
sudo -n ln -sfn "$previous_target" "$current_link" || return 1
|
|
186
|
+
|
|
187
|
+
resolved_previous="$(realpath "$previous_target" 2>/dev/null || true)"
|
|
188
|
+
resolved_current="$(realpath "$current_link" 2>/dev/null || true)"
|
|
189
|
+
[ -n "$resolved_previous" ] && [ "$resolved_previous" = "$resolved_current" ] || return 1
|
|
190
|
+
|
|
191
|
+
rm -rf "$new_version_dir" || return 1
|
|
192
|
+
sudo -n systemctl start "$SERVICE_NAME" 2>/dev/null || return 1
|
|
193
|
+
wait_for_started_service
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
service_property() {
|
|
197
|
+
local property="$1"
|
|
198
|
+
|
|
199
|
+
systemctl show "$SERVICE_NAME" --property="$property" --value
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
manager_processes_are_empty() {
|
|
203
|
+
local output token
|
|
204
|
+
|
|
205
|
+
# Tests may provide a deterministic manager enumeration. Production uses the systemd Manager
|
|
206
|
+
# D-Bus method instead of treating MainPID=0 as an independent process proof.
|
|
207
|
+
if [ -n "${SERVICE_MANAGER_PROCS_FILE:-}" ]; then
|
|
208
|
+
[ -r "$SERVICE_MANAGER_PROCS_FILE" ] || return 2
|
|
209
|
+
output="$(cat "$SERVICE_MANAGER_PROCS_FILE" 2>/dev/null)" || return 2
|
|
210
|
+
else
|
|
211
|
+
command -v busctl >/dev/null 2>&1 || return 2
|
|
212
|
+
output="$(busctl --system call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
|
|
213
|
+
org.freedesktop.systemd1.Manager GetUnitProcesses s "$SERVICE_UNIT" 2>/dev/null)" || return 2
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# busctl returns an array header followed by tuples. Any positive integer in the tuple stream
|
|
217
|
+
# is a process owned by the unit; zero is the empty-array count/fixture sentinel.
|
|
218
|
+
while IFS= read -r token; do
|
|
219
|
+
case "$token" in
|
|
220
|
+
''|0) ;;
|
|
221
|
+
*[!0-9]*) ;;
|
|
222
|
+
*) [ "$token" -eq 0 ] || return 1 ;;
|
|
223
|
+
esac
|
|
224
|
+
done <<EOF
|
|
225
|
+
$(printf '%s\n' "$output" | grep -Eo '[0-9]+' || true)
|
|
226
|
+
EOF
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
stopped_service_quiesced() {
|
|
230
|
+
local active_state main_pid control_pid job_id control_group cgroup_dir cgroup_procs cgroup_events
|
|
231
|
+
local member_status population_status probe_status members subtree_file baseline current_baseline property value cgroup_mount_type
|
|
232
|
+
|
|
233
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
234
|
+
if [ -n "${SERVICE_CGROUP_MOUNT_TYPE:-}" ]; then
|
|
235
|
+
[ "$SERVICE_CGROUP_MOUNT_TYPE" = "cgroup2fs" ] || return 2
|
|
236
|
+
else
|
|
237
|
+
cgroup_mount_type="$(LC_ALL=C stat -f -c '%T' "${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" 2>/dev/null || true)"
|
|
238
|
+
[ "$cgroup_mount_type" = "cgroup2fs" ] || return 2
|
|
239
|
+
fi
|
|
240
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
241
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
242
|
+
job_id="$(service_property Job 2>/dev/null)" || return 2
|
|
243
|
+
case "$active_state" in inactive|failed) ;; *) return 1 ;; esac
|
|
244
|
+
[ "$main_pid" = "0" ] || return 1
|
|
245
|
+
[ "$control_pid" = "0" ] || return 1
|
|
246
|
+
# systemctl --value prints an empty value when the unit has no pending job.
|
|
247
|
+
[ -z "$job_id" ] || [ "$job_id" = "0" ] || return 1
|
|
248
|
+
|
|
249
|
+
manager_processes_are_empty || return $?
|
|
250
|
+
baseline=""
|
|
251
|
+
for property in FragmentPath ExecStart Slice Delegate KillMode InvocationID Result; do
|
|
252
|
+
value="$(service_property "$property" 2>/dev/null || true)"
|
|
253
|
+
[ -n "$value" ] || return 2
|
|
254
|
+
baseline="${baseline}${property}=${value}\n"
|
|
255
|
+
done
|
|
256
|
+
if [ -n "$STOPPED_SERVICE_BASELINE" ]; then
|
|
257
|
+
current_baseline="$baseline"
|
|
258
|
+
[ "$current_baseline" = "$STOPPED_SERVICE_BASELINE" ] || return 1
|
|
259
|
+
else
|
|
260
|
+
STOPPED_SERVICE_BASELINE="$baseline"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
control_group="$(service_property ControlGroup 2>/dev/null || true)"
|
|
264
|
+
# systemd may clear ControlGroup after a stopped unit is reaped. The canonical unit cgroup is
|
|
265
|
+
# still the only acceptable fallback; it is checked against the cgroup-v2 mount below.
|
|
266
|
+
[ -n "$control_group" ] || control_group="/system.slice/${SERVICE_NAME}.service"
|
|
267
|
+
case "$control_group" in
|
|
268
|
+
/*) ;;
|
|
269
|
+
*) return 2 ;;
|
|
270
|
+
esac
|
|
271
|
+
cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
|
|
272
|
+
cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
|
|
273
|
+
cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
|
|
274
|
+
|
|
275
|
+
if probe_cgroup_directory "$cgroup_dir"; then
|
|
276
|
+
[ -r "$cgroup_procs" ] || return 2
|
|
277
|
+
[ -r "$cgroup_events" ] || return 2
|
|
278
|
+
read_cgroup_members "$cgroup_procs" || return $?
|
|
279
|
+
members="$CGROUP_MEMBERS"
|
|
280
|
+
[ -z "$members" ] || return 1
|
|
281
|
+
read_cgroup_population "$cgroup_events" || return $?
|
|
282
|
+
[ "$CGROUP_POPULATED" = "0" ] || return 1
|
|
283
|
+
# Check descendants as well as the unit's direct cgroup. A delegated child subtree is not
|
|
284
|
+
# quiescent merely because the parent cgroup.procs is empty.
|
|
285
|
+
while IFS= read -r subtree_file; do
|
|
286
|
+
[ "$subtree_file" = "$cgroup_procs" ] && continue
|
|
287
|
+
read_cgroup_members "$subtree_file" || return $?
|
|
288
|
+
[ -z "$CGROUP_MEMBERS" ] || return 1
|
|
289
|
+
done < <(find "$cgroup_dir" -type f -name cgroup.procs -print 2>/dev/null)
|
|
290
|
+
return 0
|
|
291
|
+
else
|
|
292
|
+
probe_status=$?
|
|
293
|
+
fi
|
|
294
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
295
|
+
|
|
296
|
+
# A removed original cgroup is valid only when the cgroup-v2 mount and its stable parent remain
|
|
297
|
+
# readable; manager enumeration above is the independent proof that no unit process survived.
|
|
298
|
+
[ -d "${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" ] || return 2
|
|
299
|
+
[ -d "$(dirname "$cgroup_dir")" ] || return 2
|
|
300
|
+
return 0
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
wait_for_stopped_service() {
|
|
304
|
+
local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS)) result
|
|
305
|
+
|
|
306
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
307
|
+
if stopped_service_quiesced; then return 0; else result=$?; fi
|
|
308
|
+
[ "$result" -eq 2 ] || sleep 1
|
|
309
|
+
[ "$result" -eq 2 ] && return 2
|
|
310
|
+
done
|
|
311
|
+
return 1
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
target_health_is_valid() {
|
|
315
|
+
local payload
|
|
316
|
+
|
|
317
|
+
[ -n "$HEALTH_URL" ] || return 1
|
|
318
|
+
payload="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null)" || return 1
|
|
319
|
+
|
|
320
|
+
printf '%s' "$payload" | HEALTH_EXPECTED_VERSION="$HEALTH_EXPECTED_VERSION" node -e '
|
|
321
|
+
let input = "";
|
|
322
|
+
process.stdin.setEncoding("utf8");
|
|
323
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
324
|
+
process.stdin.on("end", () => {
|
|
325
|
+
try {
|
|
326
|
+
const body = JSON.parse(input);
|
|
327
|
+
const data = body && body.data;
|
|
328
|
+
process.exit(data?.status === "ok" && data.version === process.env.HEALTH_EXPECTED_VERSION ? 0 : 1);
|
|
329
|
+
} catch {
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
target_process_is_stable() {
|
|
337
|
+
local state main_pid start_identity resolved_target expected_target
|
|
338
|
+
|
|
339
|
+
state="$(service_property ActiveState 2>/dev/null || true)"
|
|
340
|
+
main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
341
|
+
start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
342
|
+
resolved_target="$(readlink -f "$CURRENT_LINK" 2>/dev/null || true)"
|
|
343
|
+
expected_target="$(realpath "$NEW_VERSION_DIR" 2>/dev/null || true)"
|
|
344
|
+
|
|
345
|
+
[ "$state" = "active" ] || return 1
|
|
346
|
+
[ "$main_pid" != "0" ] && [ -n "$main_pid" ] || return 1
|
|
347
|
+
[ -n "$start_identity" ] || return 1
|
|
348
|
+
[ -n "$CURRENT_LINK" ] && [ -n "$NEW_VERSION_DIR" ] || return 1
|
|
349
|
+
[ -n "$expected_target" ] && [ "$resolved_target" = "$expected_target" ] || return 1
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
process_identity() {
|
|
353
|
+
local pid="$1"
|
|
354
|
+
local proc_root="${SERVICE_PROCESS_ROOT:-/proc}"
|
|
355
|
+
local proc_dir="${proc_root}/${pid}"
|
|
356
|
+
local stat_line start_time executable command
|
|
357
|
+
|
|
358
|
+
[ "$pid" != "0" ] && [ -n "$pid" ] || return 1
|
|
359
|
+
|
|
360
|
+
# SERVICE_PROCESS_ROOT is a private fixture hook; the production default is /proc.
|
|
361
|
+
if [ -r "${proc_dir}/identity" ]; then
|
|
362
|
+
printf '%s|%s\n' "$pid" "$(cat "${proc_dir}/identity")"
|
|
363
|
+
return 0
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
if [ -r "${proc_dir}/stat" ]; then
|
|
367
|
+
stat_line="$(cat "${proc_dir}/stat")" || return 2
|
|
368
|
+
start_time="$(printf '%s\n' "$stat_line" | awk '{ sub(/^.*\\) /, ""); print $20 }')" || return 2
|
|
369
|
+
[ -n "$start_time" ] || return 2
|
|
370
|
+
executable="$(readlink "${proc_dir}/exe" 2>/dev/null)" || return 2
|
|
371
|
+
[ -n "$executable" ] || return 2
|
|
372
|
+
printf '%s|%s|%s\n' "$pid" "$start_time" "$executable"
|
|
373
|
+
return 0
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
377
|
+
return 1
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
command="$(ps -p "$pid" -o command= 2>/dev/null)" || return 2
|
|
381
|
+
[ -n "$command" ] || return 2
|
|
382
|
+
printf '%s|%s\n' "$pid" "$command"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
read_cgroup_members() {
|
|
386
|
+
local cgroup_procs="$1"
|
|
387
|
+
local contents members
|
|
388
|
+
|
|
389
|
+
CGROUP_MEMBERS=""
|
|
390
|
+
if ! contents="$(cat "$cgroup_procs" 2>/dev/null)"; then
|
|
391
|
+
return 2
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
if ! members="$(printf '%s\n' "$contents" | awk '
|
|
395
|
+
length($0) > 0 {
|
|
396
|
+
if ($0 !~ /^[0-9]+$/) {
|
|
397
|
+
exit 2
|
|
398
|
+
}
|
|
399
|
+
print
|
|
400
|
+
}
|
|
401
|
+
')"; then
|
|
402
|
+
return 2
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
CGROUP_MEMBERS="$members"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
read_cgroup_population() {
|
|
409
|
+
local cgroup_events="$1"
|
|
410
|
+
local contents populated
|
|
411
|
+
|
|
412
|
+
CGROUP_POPULATED=""
|
|
413
|
+
if ! contents="$(cat "$cgroup_events" 2>/dev/null)"; then
|
|
414
|
+
return 2
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
if ! populated="$(printf '%s\n' "$contents" | awk '
|
|
418
|
+
$1 == "populated" {
|
|
419
|
+
count++
|
|
420
|
+
if (NF != 2 || $2 !~ /^[01]$/) {
|
|
421
|
+
invalid=1
|
|
422
|
+
} else {
|
|
423
|
+
value=$2
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
END {
|
|
427
|
+
if (count != 1 || invalid) {
|
|
428
|
+
exit 2
|
|
429
|
+
}
|
|
430
|
+
print value
|
|
431
|
+
}
|
|
432
|
+
')"; then
|
|
433
|
+
return 2
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
CGROUP_POPULATED="$populated"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
probe_cgroup_directory() {
|
|
440
|
+
local cgroup_dir="$1"
|
|
441
|
+
local gnu_stat bsd_stat
|
|
442
|
+
|
|
443
|
+
if [ ! -e "$cgroup_dir" ]; then
|
|
444
|
+
[ -d "$(dirname "$cgroup_dir")" ] || return 2
|
|
445
|
+
return 1
|
|
446
|
+
fi
|
|
447
|
+
[ -d "$cgroup_dir" ] || return 2
|
|
448
|
+
|
|
449
|
+
if gnu_stat="$(LC_ALL=C stat -c '%F' "$cgroup_dir" 2>&1)"; then
|
|
450
|
+
case "$gnu_stat" in
|
|
451
|
+
directory) return 0 ;;
|
|
452
|
+
*) return 2 ;;
|
|
453
|
+
esac
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
if bsd_stat="$(LC_ALL=C stat -f '%HT' "$cgroup_dir" 2>&1)"; then
|
|
457
|
+
case "$bsd_stat" in
|
|
458
|
+
Directory|directory) return 0 ;;
|
|
459
|
+
*) return 2 ;;
|
|
460
|
+
esac
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
case "$gnu_stat\n$bsd_stat" in
|
|
464
|
+
*"No such file or directory"*|*"no such file or directory"*) return 1 ;;
|
|
465
|
+
*) return 2 ;;
|
|
466
|
+
esac
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
capture_service_identity() {
|
|
470
|
+
local active_state main_pid control_pid control_group cgroup_procs cgroup_dir cgroup_events members pid identity member_status
|
|
471
|
+
|
|
472
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
473
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
474
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
475
|
+
control_group="$(service_property ControlGroup 2>/dev/null)" || return 2
|
|
476
|
+
|
|
477
|
+
case "$active_state" in
|
|
478
|
+
active|activating) ;;
|
|
479
|
+
*) return 1 ;;
|
|
480
|
+
esac
|
|
481
|
+
|
|
482
|
+
[ "$main_pid" -gt 0 ] 2>/dev/null || return 1
|
|
483
|
+
[ "$control_pid" = "0" ] || [ "$control_pid" -gt 0 ] 2>/dev/null || return 2
|
|
484
|
+
[ -n "$control_group" ] || return 2
|
|
485
|
+
|
|
486
|
+
cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
|
|
487
|
+
cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
|
|
488
|
+
cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
|
|
489
|
+
if probe_cgroup_directory "$cgroup_dir"; then :; else return 2; fi
|
|
490
|
+
[ -r "$cgroup_procs" ] || return 2
|
|
491
|
+
[ -r "$cgroup_events" ] || return 2
|
|
492
|
+
if read_cgroup_members "$cgroup_procs"; then
|
|
493
|
+
member_status=0
|
|
494
|
+
else
|
|
495
|
+
member_status=$?
|
|
496
|
+
fi
|
|
497
|
+
[ "$member_status" -eq 0 ] || return "$member_status"
|
|
498
|
+
members="$CGROUP_MEMBERS"
|
|
499
|
+
[ -n "$members" ] || return 1
|
|
500
|
+
|
|
501
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES=""
|
|
502
|
+
while IFS= read -r pid; do
|
|
503
|
+
[ -n "$pid" ] || continue
|
|
504
|
+
identity="$(process_identity "$pid")" || return 2
|
|
505
|
+
CAPTURED_SERVICE_MEMBER_IDENTITIES="${CAPTURED_SERVICE_MEMBER_IDENTITIES}${identity}\n"
|
|
506
|
+
done <<EOF
|
|
507
|
+
$members
|
|
508
|
+
EOF
|
|
509
|
+
|
|
510
|
+
printf '%s\n' "$members" | grep -qx "$main_pid" || return 1
|
|
511
|
+
CAPTURED_SERVICE_CGROUP_PROCS="$cgroup_procs"
|
|
512
|
+
CAPTURED_SERVICE_CGROUP_DIR="$cgroup_dir"
|
|
513
|
+
CAPTURED_SERVICE_CGROUP_EVENTS="$cgroup_events"
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
service_quiesced() {
|
|
517
|
+
local active_state main_pid control_pid cgroup_dir_status member_status population_status probe_status pid captured_identity current_identity identity_status
|
|
518
|
+
|
|
519
|
+
[ -n "$CAPTURED_SERVICE_CGROUP_PROCS" ] || return 2
|
|
520
|
+
active_state="$(service_property ActiveState 2>/dev/null)" || return 2
|
|
521
|
+
main_pid="$(service_property MainPID 2>/dev/null)" || return 2
|
|
522
|
+
control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
|
|
523
|
+
|
|
524
|
+
case "$active_state" in
|
|
525
|
+
inactive|failed) ;;
|
|
526
|
+
*) return 1 ;;
|
|
527
|
+
esac
|
|
528
|
+
|
|
529
|
+
[ "$main_pid" = "0" ] || return 1
|
|
530
|
+
[ "$control_pid" = "0" ] || return 1
|
|
531
|
+
|
|
532
|
+
# Check every member captured before stop independently. This catches KillMode=process
|
|
533
|
+
# survivors even when systemd has already removed the original cgroup directory.
|
|
534
|
+
while IFS= read -r captured_identity; do
|
|
535
|
+
[ -n "$captured_identity" ] || continue
|
|
536
|
+
pid="${captured_identity%%|*}"
|
|
537
|
+
current_identity=""
|
|
538
|
+
if current_identity="$(process_identity "$pid")"; then
|
|
539
|
+
identity_status=0
|
|
540
|
+
else
|
|
541
|
+
identity_status=$?
|
|
542
|
+
fi
|
|
543
|
+
case "$identity_status" in
|
|
544
|
+
0) [ "$current_identity" != "$captured_identity" ] || return 1 ;;
|
|
545
|
+
1) ;;
|
|
546
|
+
*) return 2 ;;
|
|
547
|
+
esac
|
|
548
|
+
done <<EOF
|
|
549
|
+
$(printf '%b' "$CAPTURED_SERVICE_MEMBER_IDENTITIES")
|
|
550
|
+
EOF
|
|
551
|
+
|
|
552
|
+
# The original cgroup may be empty or removed after the service reaches inactive/failed with
|
|
553
|
+
# zero MainPID/ControlPID. A missing membership file while its directory remains is an
|
|
554
|
+
# unavailable probe, not proof that the subtree is empty.
|
|
555
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
556
|
+
if read_cgroup_members "$CAPTURED_SERVICE_CGROUP_PROCS"; then
|
|
557
|
+
member_status=0
|
|
558
|
+
else
|
|
559
|
+
member_status=$?
|
|
560
|
+
fi
|
|
561
|
+
if [ "$member_status" -eq 2 ]; then
|
|
562
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
563
|
+
return 2
|
|
564
|
+
else
|
|
565
|
+
probe_status=$?
|
|
566
|
+
fi
|
|
567
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
568
|
+
return 0
|
|
569
|
+
fi
|
|
570
|
+
[ "$member_status" -eq 0 ] || return "$member_status"
|
|
571
|
+
[ -z "$CGROUP_MEMBERS" ] || return 1
|
|
572
|
+
|
|
573
|
+
if read_cgroup_population "$CAPTURED_SERVICE_CGROUP_EVENTS"; then
|
|
574
|
+
population_status=0
|
|
575
|
+
else
|
|
576
|
+
population_status=$?
|
|
577
|
+
fi
|
|
578
|
+
if [ "$population_status" -eq 2 ]; then
|
|
579
|
+
if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
|
|
580
|
+
return 2
|
|
581
|
+
else
|
|
582
|
+
probe_status=$?
|
|
583
|
+
fi
|
|
584
|
+
[ "$probe_status" -eq 1 ] || return 2
|
|
585
|
+
return 0
|
|
586
|
+
fi
|
|
587
|
+
[ "$population_status" -eq 0 ] || return "$population_status"
|
|
588
|
+
[ "$CGROUP_POPULATED" = "0" ] || return 1
|
|
589
|
+
else
|
|
590
|
+
cgroup_dir_status=$?
|
|
591
|
+
case "$cgroup_dir_status" in
|
|
592
|
+
1) ;;
|
|
593
|
+
*) return 2 ;;
|
|
594
|
+
esac
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
return 0
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
wait_for_quiescence() {
|
|
601
|
+
local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS))
|
|
602
|
+
local result
|
|
603
|
+
|
|
604
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
605
|
+
if service_quiesced; then
|
|
606
|
+
return 0
|
|
607
|
+
else
|
|
608
|
+
result=$?
|
|
609
|
+
fi
|
|
610
|
+
if [ "$result" -eq 2 ]; then
|
|
611
|
+
return 2
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
sleep 1
|
|
615
|
+
done
|
|
616
|
+
|
|
617
|
+
return 1
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
wait_for_started_service() {
|
|
621
|
+
local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
|
|
622
|
+
local previous_main_pid=""
|
|
623
|
+
local previous_start_identity=""
|
|
624
|
+
local before_main_pid before_start_identity main_pid start_identity
|
|
625
|
+
|
|
626
|
+
while [ "$SECONDS" -le "$deadline" ]; do
|
|
627
|
+
if target_process_is_stable; then
|
|
628
|
+
before_main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
629
|
+
before_start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
630
|
+
|
|
631
|
+
if [ "$before_main_pid" = "0" ] || [ -z "$before_main_pid" ] || [ -z "$before_start_identity" ]; then
|
|
632
|
+
previous_main_pid=""
|
|
633
|
+
previous_start_identity=""
|
|
634
|
+
sleep 1
|
|
635
|
+
continue
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
if target_health_is_valid && target_process_is_stable; then
|
|
639
|
+
main_pid="$(service_property MainPID 2>/dev/null || true)"
|
|
640
|
+
start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
|
|
641
|
+
|
|
642
|
+
if [ "$before_main_pid" = "$main_pid" ] &&
|
|
643
|
+
[ "$before_start_identity" = "$start_identity" ]; then
|
|
644
|
+
if [ "$main_pid" = "$previous_main_pid" ] &&
|
|
645
|
+
[ "$start_identity" = "$previous_start_identity" ]; then
|
|
646
|
+
return 0
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
previous_main_pid="$main_pid"
|
|
650
|
+
previous_start_identity="$start_identity"
|
|
651
|
+
else
|
|
652
|
+
previous_main_pid=""
|
|
653
|
+
previous_start_identity=""
|
|
654
|
+
fi
|
|
655
|
+
else
|
|
656
|
+
previous_main_pid=""
|
|
657
|
+
previous_start_identity=""
|
|
658
|
+
fi
|
|
659
|
+
else
|
|
660
|
+
previous_main_pid=""
|
|
661
|
+
previous_start_identity=""
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
sleep 1
|
|
665
|
+
done
|
|
666
|
+
|
|
667
|
+
return 1
|
|
668
|
+
}
|
|
9
669
|
|
|
10
670
|
# Capture once — used for all status writes so timeout detection works correctly
|
|
11
671
|
STARTED_AT="$(date -Iseconds)"
|
|
@@ -45,7 +705,17 @@ update_status() {
|
|
|
45
705
|
cleanup() {
|
|
46
706
|
local exit_code=$?
|
|
47
707
|
|
|
48
|
-
if [
|
|
708
|
+
if [ -n "${BACKUP_TMP_PATH:-}" ]; then
|
|
709
|
+
rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
|
|
710
|
+
BACKUP_TMP_PATH=""
|
|
711
|
+
fi
|
|
712
|
+
if [ "$exit_code" -ne 0 ] && [ "$FINALIZED" != "true" ]; then
|
|
713
|
+
if [ -n "$ATTEMPT_ID" ] && { [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; }; then
|
|
714
|
+
hold_attempt "interrupted" "Update worker exited with code $exit_code during a state-changing phase"
|
|
715
|
+
elif [ -n "$ATTEMPT_ID" ]; then
|
|
716
|
+
finish_before_migration "failed" "Update process exited with code $exit_code"
|
|
717
|
+
fi
|
|
718
|
+
|
|
49
719
|
# Only write a generic failure if a specific error wasn't already recorded
|
|
50
720
|
if [ -f "$STATUS_FILE" ] && grep -q '"status": "failed"' "$STATUS_FILE" 2>/dev/null; then
|
|
51
721
|
return
|
|
@@ -56,6 +726,27 @@ cleanup() {
|
|
|
56
726
|
|
|
57
727
|
trap cleanup EXIT
|
|
58
728
|
|
|
729
|
+
handle_signal() {
|
|
730
|
+
local signal_name="$1"
|
|
731
|
+
local signal_code="$2"
|
|
732
|
+
|
|
733
|
+
if [ -n "$ATTEMPT_ID" ]; then
|
|
734
|
+
if [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; then
|
|
735
|
+
hold_attempt "interrupted" "Update worker received SIG${signal_name} after migration entry"
|
|
736
|
+
else
|
|
737
|
+
finish_before_migration "interrupted" "Update worker received SIG${signal_name} before migration entry"
|
|
738
|
+
fi
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
update_status "failed" "failed" "Update worker received SIG${signal_name}"
|
|
742
|
+
FINALIZED="true"
|
|
743
|
+
exit "$signal_code"
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
trap 'handle_signal TERM 143' TERM
|
|
747
|
+
trap 'handle_signal INT 130' INT
|
|
748
|
+
trap 'handle_signal HUP 129' HUP
|
|
749
|
+
|
|
59
750
|
# ──────────────────────────────────────────────────────────────
|
|
60
751
|
# Image-based update (Raspbian image installs)
|
|
61
752
|
# ──────────────────────────────────────────────────────────────
|
|
@@ -64,6 +755,85 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
64
755
|
CLEAN_VERSION="${VERSION#v}"
|
|
65
756
|
NEW_VERSION_DIR="${IMAGE_BASE_DIR}/v${CLEAN_VERSION}"
|
|
66
757
|
CURRENT_LINK="${IMAGE_BASE_DIR}/current"
|
|
758
|
+
ENV_FILE="${ENV_FILE:-/etc/smart-panel/environment}"
|
|
759
|
+
|
|
760
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
761
|
+
case "$VERSION" in latest|vlatest|'')
|
|
762
|
+
update_status "failed" "failed" "Stopped maintenance requires an exact target version"
|
|
763
|
+
FINALIZED="true"
|
|
764
|
+
exit 1
|
|
765
|
+
;; esac
|
|
766
|
+
for required in EXPECTED_CURRENT_VERSION TARGET_ARCHIVE_SHA256 TARGET_WORKER_SHA256 EXPECTED_IMAGE_BASE_DIR \
|
|
767
|
+
EXPECTED_STATUS_FILE EXPECTED_ATTEMPT_DIR EXPECTED_DB_PATH DB_BACKUP_PATH; do
|
|
768
|
+
if [ -z "${!required:-}" ]; then
|
|
769
|
+
update_status "failed" "failed" "Stopped maintenance identity is missing: ${required}"
|
|
770
|
+
FINALIZED="true"
|
|
771
|
+
exit 1
|
|
772
|
+
fi
|
|
773
|
+
done
|
|
774
|
+
TARGET_ARCHIVE_SHA256="$(printf '%s' "$TARGET_ARCHIVE_SHA256" | tr '[:upper:]' '[:lower:]')"
|
|
775
|
+
TARGET_WORKER_SHA256="$(printf '%s' "$TARGET_WORKER_SHA256" | tr '[:upper:]' '[:lower:]')"
|
|
776
|
+
case "$TARGET_ARCHIVE_SHA256$TARGET_WORKER_SHA256" in *[!0123456789abcdef]*)
|
|
777
|
+
update_status "failed" "failed" "Stopped maintenance checksum is invalid"
|
|
778
|
+
FINALIZED="true"
|
|
779
|
+
exit 1
|
|
780
|
+
;; esac
|
|
781
|
+
[ "${#TARGET_ARCHIVE_SHA256}" -eq 64 ] && [ "${#TARGET_WORKER_SHA256}" -eq 64 ] || {
|
|
782
|
+
update_status "failed" "failed" "Stopped maintenance checksum must be SHA-256"
|
|
783
|
+
FINALIZED="true"
|
|
784
|
+
exit 1
|
|
785
|
+
}
|
|
786
|
+
[ "$EXPECTED_IMAGE_BASE_DIR" = "$IMAGE_BASE_DIR" ] || {
|
|
787
|
+
update_status "failed" "failed" "Stopped maintenance image base identity mismatch"
|
|
788
|
+
FINALIZED="true"
|
|
789
|
+
exit 1
|
|
790
|
+
}
|
|
791
|
+
[ "$EXPECTED_STATUS_FILE" = "$STATUS_FILE" ] || {
|
|
792
|
+
update_status "failed" "failed" "Stopped maintenance status identity mismatch"
|
|
793
|
+
FINALIZED="true"
|
|
794
|
+
exit 1
|
|
795
|
+
}
|
|
796
|
+
[ "$EXPECTED_ATTEMPT_DIR" = "$ATTEMPT_DIR" ] || {
|
|
797
|
+
update_status "failed" "failed" "Stopped maintenance attempt identity mismatch"
|
|
798
|
+
FINALIZED="true"
|
|
799
|
+
exit 1
|
|
800
|
+
}
|
|
801
|
+
[ "$EXPECTED_DB_PATH" = "${FB_DB_PATH:-}" ] || {
|
|
802
|
+
update_status "failed" "failed" "Stopped maintenance database identity mismatch"
|
|
803
|
+
FINALIZED="true"
|
|
804
|
+
exit 1
|
|
805
|
+
}
|
|
806
|
+
if [ -f "$ENV_FILE" ]; then
|
|
807
|
+
configured_db_path="$(
|
|
808
|
+
set -a
|
|
809
|
+
# shellcheck source=/dev/null
|
|
810
|
+
. "$ENV_FILE"
|
|
811
|
+
printf '%s' "${FB_DB_PATH:-}"
|
|
812
|
+
)"
|
|
813
|
+
[ "$configured_db_path" = "$EXPECTED_DB_PATH" ] || \
|
|
814
|
+
fail_before_migration "preflight" "Configured database identity does not match stopped-maintenance packet"
|
|
815
|
+
fi
|
|
816
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
817
|
+
fail_before_migration "preflight" "Stopped maintenance backup path already exists"
|
|
818
|
+
fi
|
|
819
|
+
SOURCE_DB_PATH="${FB_DB_PATH%/}/database.sqlite"
|
|
820
|
+
source_database_is_valid || fail_before_migration "preflight" "Stopped maintenance source database is missing, symlinked, or empty"
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
# Image updates must verify the target release through the existing local health route. A
|
|
824
|
+
# missing route would otherwise silently reduce readiness to a PID check and reintroduce the
|
|
825
|
+
# startup/worker circular wait that the updater is designed to avoid.
|
|
826
|
+
if [ -z "$HEALTH_URL" ]; then
|
|
827
|
+
update_status "failed" "failed" "No local health URL configured for image update"
|
|
828
|
+
FINALIZED="true"
|
|
829
|
+
exit 1
|
|
830
|
+
fi
|
|
831
|
+
|
|
832
|
+
if ! acquire_attempt; then
|
|
833
|
+
update_status "failed" "failed" "An unresolved update attempt already owns the updater"
|
|
834
|
+
FINALIZED="true"
|
|
835
|
+
exit 1
|
|
836
|
+
fi
|
|
67
837
|
|
|
68
838
|
# Verify passwordless sudo is available for all required commands.
|
|
69
839
|
# Without -n, sudo may hang waiting for a password on a detached process.
|
|
@@ -77,8 +847,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
77
847
|
|
|
78
848
|
for cmd in "${SUDO_CMDS[@]}"; do
|
|
79
849
|
if ! sudo -n -l $cmd >/dev/null 2>&1; then
|
|
80
|
-
|
|
81
|
-
exit 1
|
|
850
|
+
fail_before_migration "preflight" "Passwordless sudo not available for: $cmd"
|
|
82
851
|
fi
|
|
83
852
|
done
|
|
84
853
|
PREVIOUS_TARGET=""
|
|
@@ -86,6 +855,24 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
86
855
|
# Save the current version for rollback
|
|
87
856
|
if [ -L "$CURRENT_LINK" ]; then
|
|
88
857
|
PREVIOUS_TARGET=$(readlink "$CURRENT_LINK")
|
|
858
|
+
case "$PREVIOUS_TARGET" in
|
|
859
|
+
/*) ;;
|
|
860
|
+
*) PREVIOUS_TARGET="${IMAGE_BASE_DIR}/${PREVIOUS_TARGET}" ;;
|
|
861
|
+
esac
|
|
862
|
+
fi
|
|
863
|
+
|
|
864
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
865
|
+
EXPECTED_CURRENT_LINK="${IMAGE_BASE_DIR}/v${EXPECTED_CURRENT_VERSION#v}"
|
|
866
|
+
RESOLVED_CURRENT="$(realpath "$CURRENT_LINK" 2>/dev/null || true)"
|
|
867
|
+
RESOLVED_EXPECTED="$(realpath "$EXPECTED_CURRENT_LINK" 2>/dev/null || true)"
|
|
868
|
+
[ -n "$RESOLVED_CURRENT" ] && [ "$RESOLVED_CURRENT" = "$RESOLVED_EXPECTED" ] || \
|
|
869
|
+
fail_before_migration "preflight" "Current release/link identity does not match stopped-maintenance packet"
|
|
870
|
+
if ! wait_for_stopped_service; then
|
|
871
|
+
hold_attempt "stopping" "Stopped service did not reach verified quiescence"
|
|
872
|
+
update_status "failed" "failed" "Stopped service quiescence could not be verified; update is held for recovery"
|
|
873
|
+
FINALIZED="true"
|
|
874
|
+
exit 1
|
|
875
|
+
fi
|
|
89
876
|
fi
|
|
90
877
|
|
|
91
878
|
# Guard: refuse to overwrite the currently running version
|
|
@@ -94,25 +881,28 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
94
881
|
RESOLVED_NEW=$(cd "$IMAGE_BASE_DIR" && realpath "$NEW_VERSION_DIR" 2>/dev/null || true)
|
|
95
882
|
|
|
96
883
|
if [ -n "$PREVIOUS_TARGET" ] && [ -n "$RESOLVED_PREV" ] && [ -n "$RESOLVED_NEW" ] && [ "$RESOLVED_PREV" = "$RESOLVED_NEW" ]; then
|
|
97
|
-
|
|
98
|
-
exit 1
|
|
884
|
+
fail_before_migration "preflight" "Target version v${CLEAN_VERSION} is already the active version"
|
|
99
885
|
fi
|
|
100
886
|
|
|
101
887
|
# ── Download ──
|
|
102
888
|
update_status "downloading" "downloading"
|
|
103
889
|
|
|
104
890
|
if [ -z "$DOWNLOAD_URL" ]; then
|
|
105
|
-
|
|
106
|
-
exit 1
|
|
891
|
+
fail_before_migration "downloading" "No download URL provided for image update"
|
|
107
892
|
fi
|
|
108
893
|
|
|
109
894
|
TMP_TARBALL="/tmp/smart-panel-backend-v${CLEAN_VERSION}.tar.gz"
|
|
110
895
|
|
|
111
896
|
curl -fSL -o "$TMP_TARBALL" "$DOWNLOAD_URL" 2>&1 || {
|
|
112
|
-
|
|
113
|
-
exit 1
|
|
897
|
+
fail_before_migration "downloading" "Download failed from ${DOWNLOAD_URL}"
|
|
114
898
|
}
|
|
115
899
|
|
|
900
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
901
|
+
actual_archive_sha256="$(sha256sum "$TMP_TARBALL" 2>/dev/null | awk '{print $1}')"
|
|
902
|
+
[ "$actual_archive_sha256" = "$TARGET_ARCHIVE_SHA256" ] || \
|
|
903
|
+
fail_before_migration "downloading" "Downloaded archive checksum does not match maintenance packet"
|
|
904
|
+
fi
|
|
905
|
+
|
|
116
906
|
# ── Extract ──
|
|
117
907
|
update_status "installing" "installing"
|
|
118
908
|
|
|
@@ -121,17 +911,36 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
121
911
|
tar -xzf "$TMP_TARBALL" -C "$NEW_VERSION_DIR" 2>&1 || {
|
|
122
912
|
rm -rf "$NEW_VERSION_DIR"
|
|
123
913
|
rm -f "$TMP_TARBALL"
|
|
124
|
-
|
|
125
|
-
exit 1
|
|
914
|
+
fail_before_migration "installing" "Failed to extract update archive"
|
|
126
915
|
}
|
|
127
916
|
|
|
128
917
|
rm -f "$TMP_TARBALL"
|
|
129
918
|
|
|
919
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
920
|
+
target_worker="${NEW_VERSION_DIR}/dist/modules/system/scripts/update-worker.sh"
|
|
921
|
+
[ -f "$target_worker" ] || {
|
|
922
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
923
|
+
fail_before_migration "installing" "Target release worker is missing"
|
|
924
|
+
}
|
|
925
|
+
actual_worker_sha256="$(sha256sum "$target_worker" 2>/dev/null | awk '{print $1}')"
|
|
926
|
+
[ "$actual_worker_sha256" = "$TARGET_WORKER_SHA256" ] || {
|
|
927
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
928
|
+
fail_before_migration "installing" "Target release worker checksum does not match maintenance packet"
|
|
929
|
+
}
|
|
930
|
+
MAINTENANCE_DIR="${MAINTENANCE_DIR:-${ATTEMPT_DIR}/maintenance-${ATTEMPT_ID}}"
|
|
931
|
+
case "$MAINTENANCE_DIR" in
|
|
932
|
+
"$IMAGE_BASE_DIR"/*|"$NEW_VERSION_DIR"/*) fail_before_migration "installing" "Maintenance directory is inside a release tree" ;;
|
|
933
|
+
esac
|
|
934
|
+
mkdir -p "$MAINTENANCE_DIR"
|
|
935
|
+
chmod 700 "$MAINTENANCE_DIR"
|
|
936
|
+
cp "$target_worker" "$MAINTENANCE_DIR/update-worker.sh"
|
|
937
|
+
chmod 700 "$MAINTENANCE_DIR/update-worker.sh"
|
|
938
|
+
fi
|
|
939
|
+
|
|
130
940
|
# Create the image-install marker in the new version
|
|
131
941
|
touch "${NEW_VERSION_DIR}/.image-install" || {
|
|
132
942
|
rm -rf "$NEW_VERSION_DIR"
|
|
133
|
-
|
|
134
|
-
exit 1
|
|
943
|
+
fail_before_migration "installing" "Failed to create image-install marker"
|
|
135
944
|
}
|
|
136
945
|
|
|
137
946
|
# ── Verify dependencies ──
|
|
@@ -143,8 +952,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
143
952
|
if [ ! -d "node_modules" ]; then
|
|
144
953
|
npm install --omit=dev 2>&1 || {
|
|
145
954
|
rm -rf "$NEW_VERSION_DIR"
|
|
146
|
-
|
|
147
|
-
exit 1
|
|
955
|
+
fail_before_migration "installing" "npm install failed"
|
|
148
956
|
}
|
|
149
957
|
fi
|
|
150
958
|
|
|
@@ -153,72 +961,184 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
153
961
|
# hanging on password prompt when running as a detached process.
|
|
154
962
|
sudo -n chown -R smart-panel:smart-panel "$NEW_VERSION_DIR" || {
|
|
155
963
|
rm -rf "$NEW_VERSION_DIR"
|
|
156
|
-
|
|
157
|
-
exit 1
|
|
964
|
+
fail_before_migration "installing" "Failed to set ownership on ${NEW_VERSION_DIR}"
|
|
158
965
|
}
|
|
159
966
|
|
|
160
|
-
# ── Stop service ──
|
|
967
|
+
# ── Stop/qualify service ──
|
|
161
968
|
update_status "stopping" "stopping"
|
|
162
|
-
|
|
969
|
+
write_attempt "active" "stopping" "" "false" "$MIGRATION_ENTERED"
|
|
970
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
971
|
+
if ! wait_for_stopped_service; then
|
|
972
|
+
hold_attempt "stopping" "Stopped service changed or failed quiescence recheck"
|
|
973
|
+
update_status "failed" "failed" "Stopped service quiescence could not be rechecked; update is held for recovery"
|
|
974
|
+
FINALIZED="true"
|
|
975
|
+
exit 1
|
|
976
|
+
fi
|
|
977
|
+
else
|
|
978
|
+
if ! capture_service_identity; then
|
|
979
|
+
hold_attempt "stopping" "Could not capture service process/cgroup identity before stop"
|
|
980
|
+
update_status "failed" "failed" "Service process/cgroup identity could not be captured; update is held for recovery"
|
|
981
|
+
FINALIZED="true"
|
|
982
|
+
exit 1
|
|
983
|
+
fi
|
|
984
|
+
|
|
985
|
+
if ! sudo -n systemctl stop "$SERVICE_NAME" 2>/dev/null; then
|
|
986
|
+
hold_attempt "stopping" "Service stop failed; writer quiescence is unknown"
|
|
987
|
+
update_status "failed" "failed" "Could not stop ${SERVICE_NAME}; update is held for recovery"
|
|
988
|
+
FINALIZED="true"
|
|
989
|
+
exit 1
|
|
990
|
+
fi
|
|
991
|
+
|
|
992
|
+
if ! wait_for_quiescence; then
|
|
993
|
+
hold_attempt "stopping" "Service did not reach verified quiescence before migration"
|
|
994
|
+
update_status "failed" "failed" "Service quiescence could not be verified; update is held for recovery"
|
|
995
|
+
FINALIZED="true"
|
|
996
|
+
exit 1
|
|
997
|
+
fi
|
|
998
|
+
|
|
999
|
+
write_attempt "active" "quiesced" "" "false" "$MIGRATION_ENTERED"
|
|
1000
|
+
fi
|
|
1001
|
+
|
|
1002
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1003
|
+
# A consistent backup is an input safeguard, never a rollback mechanism. SQLite's own
|
|
1004
|
+
# backup API is required so a copied WAL database cannot be mistaken for a snapshot.
|
|
1005
|
+
command -v sqlite3 >/dev/null 2>&1 || {
|
|
1006
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1007
|
+
fail_before_migration "quiesced" "sqlite3 is required for stopped-maintenance backup"
|
|
1008
|
+
}
|
|
1009
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
1010
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1011
|
+
fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
|
|
1012
|
+
fi
|
|
1013
|
+
source_database_is_valid || {
|
|
1014
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1015
|
+
fail_before_migration "quiesced" "Stopped maintenance source database became unavailable"
|
|
1016
|
+
}
|
|
1017
|
+
mkdir -p "$(dirname "$DB_BACKUP_PATH")" || {
|
|
1018
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1019
|
+
fail_before_migration "quiesced" "Could not create SQLite backup directory"
|
|
1020
|
+
}
|
|
1021
|
+
BACKUP_TMP_PATH="$(mktemp "${DB_BACKUP_PATH}.tmp-${ATTEMPT_ID}.XXXXXX")" || {
|
|
1022
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1023
|
+
fail_before_migration "quiesced" "Could not create temporary SQLite backup"
|
|
1024
|
+
}
|
|
1025
|
+
if ! sqlite3 "${SOURCE_DB_PATH}" ".backup '${BACKUP_TMP_PATH}'"; then
|
|
1026
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1027
|
+
BACKUP_TMP_PATH=""
|
|
1028
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1029
|
+
fail_before_migration "quiesced" "Consistent SQLite backup failed"
|
|
1030
|
+
fi
|
|
1031
|
+
if [ ! -s "$BACKUP_TMP_PATH" ]; then
|
|
1032
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1033
|
+
BACKUP_TMP_PATH=""
|
|
1034
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1035
|
+
fail_before_migration "quiesced" "Consistent SQLite backup is empty"
|
|
1036
|
+
fi
|
|
1037
|
+
if ! ln "$BACKUP_TMP_PATH" "$DB_BACKUP_PATH" 2>/dev/null; then
|
|
1038
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1039
|
+
BACKUP_TMP_PATH=""
|
|
1040
|
+
rm -rf "$NEW_VERSION_DIR"
|
|
1041
|
+
if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
|
|
1042
|
+
fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
|
|
1043
|
+
fi
|
|
1044
|
+
fail_before_migration "quiesced" "Consistent SQLite backup publish failed"
|
|
1045
|
+
fi
|
|
1046
|
+
rm -f "$BACKUP_TMP_PATH"
|
|
1047
|
+
BACKUP_TMP_PATH=""
|
|
1048
|
+
fi
|
|
163
1049
|
|
|
164
1050
|
# ── Switch symlink (atomic on same filesystem) ──
|
|
165
1051
|
sudo -n ln -sfn "$NEW_VERSION_DIR" "$CURRENT_LINK" || {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1052
|
+
if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
|
|
1053
|
+
hold_attempt "switching" "Failed to switch version symlink in stopped maintenance"
|
|
1054
|
+
update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
|
|
1055
|
+
FINALIZED="true"
|
|
1056
|
+
exit 1
|
|
169
1057
|
fi
|
|
170
|
-
|
|
171
|
-
|
|
1058
|
+
# Revert only when the old link, target cleanup and old-service health are all verified.
|
|
1059
|
+
if restore_before_migration "$PREVIOUS_TARGET" "$CURRENT_LINK" "$NEW_VERSION_DIR"; then
|
|
1060
|
+
fail_before_migration "switching" "Failed to switch version symlink"
|
|
1061
|
+
fi
|
|
1062
|
+
|
|
1063
|
+
hold_attempt "switching" "Failed to switch version symlink and recovery start was not verified"
|
|
1064
|
+
update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
|
|
1065
|
+
FINALIZED="true"
|
|
172
1066
|
exit 1
|
|
173
1067
|
}
|
|
174
1068
|
|
|
175
1069
|
# ── Run database migrations ──
|
|
176
1070
|
update_status "migrating" "migrating"
|
|
177
1071
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
|
185
|
-
set +a
|
|
186
|
-
cd "$NEW_VERSION_DIR"
|
|
187
|
-
node node_modules/typeorm/cli.js migration:run -d dist/dataSource.js
|
|
188
|
-
) 2>&1 || {
|
|
189
|
-
# Migration failed — revert symlink if possible
|
|
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
|
|
1072
|
+
if [ ! -f "${NEW_VERSION_DIR}/dist/dataSource.js" ]; then
|
|
1073
|
+
hold_attempt "migrating" "Target data source is missing; update is held for recovery"
|
|
1074
|
+
update_status "failed" "failed" "Target data source is missing; update is held for recovery"
|
|
1075
|
+
FINALIZED="true"
|
|
1076
|
+
exit 1
|
|
1077
|
+
fi
|
|
205
1078
|
|
|
206
|
-
|
|
207
|
-
|
|
1079
|
+
MIGRATION_ENTERED="true"
|
|
1080
|
+
if ! write_attempt "active" "migrating" "" "false" "true"; then
|
|
1081
|
+
hold_attempt "migrating" "Could not persist migration-entry checkpoint"
|
|
1082
|
+
update_status "failed" "failed" "Could not persist migration-entry checkpoint; update is held for recovery"
|
|
1083
|
+
FINALIZED="true"
|
|
1084
|
+
exit 1
|
|
1085
|
+
fi
|
|
1086
|
+
|
|
1087
|
+
MIGRATION_LOG="${ATTEMPT_DIR}/${ATTEMPT_ID}.migration.log"
|
|
1088
|
+
: > "$MIGRATION_LOG"
|
|
1089
|
+
chmod 600 "$MIGRATION_LOG"
|
|
1090
|
+
write_attempt "active" "migrating" "" "false" "true"
|
|
1091
|
+
|
|
1092
|
+
set +e
|
|
1093
|
+
(
|
|
1094
|
+
set -a
|
|
1095
|
+
# shellcheck source=/dev/null
|
|
1096
|
+
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
|
1097
|
+
set +a
|
|
1098
|
+
cd "$NEW_VERSION_DIR"
|
|
1099
|
+
node node_modules/typeorm/cli.js migration:run -d dist/dataSource.js
|
|
1100
|
+
) 2>&1 | awk -v output="$MIGRATION_LOG" '
|
|
1101
|
+
BEGIN { limit = 65536; bytes = 0 }
|
|
1102
|
+
{
|
|
1103
|
+
if (bytes < limit) {
|
|
1104
|
+
line = $0 ORS;
|
|
1105
|
+
remaining = limit - bytes;
|
|
1106
|
+
if (length(line) > remaining) line = substr(line, 1, remaining);
|
|
1107
|
+
printf "%s", line >> output;
|
|
1108
|
+
bytes += length(line);
|
|
1109
|
+
}
|
|
208
1110
|
}
|
|
1111
|
+
'
|
|
1112
|
+
pipeline_status=("${PIPESTATUS[@]}")
|
|
1113
|
+
migration_exit_code="${pipeline_status[0]}"
|
|
1114
|
+
capture_exit_code="${pipeline_status[1]}"
|
|
1115
|
+
set -e
|
|
1116
|
+
|
|
1117
|
+
if [ "$capture_exit_code" -ne 0 ]; then
|
|
1118
|
+
hold_attempt "migration_capture_failed" "Could not retain bounded migration diagnostics"
|
|
1119
|
+
update_status "failed" "failed" "Migration diagnostics could not be retained; update is held for recovery"
|
|
1120
|
+
FINALIZED="true"
|
|
1121
|
+
exit 1
|
|
1122
|
+
fi
|
|
1123
|
+
|
|
1124
|
+
if [ "$migration_exit_code" -eq 0 ]; then
|
|
1125
|
+
write_attempt "active" "migrated" "" "false" "true"
|
|
1126
|
+
else
|
|
1127
|
+
hold_attempt "migration_failed" "Database migration failed with exit code ${migration_exit_code}"
|
|
1128
|
+
update_status "failed" "failed" "Database migration failed; update is held for recovery"
|
|
1129
|
+
FINALIZED="true"
|
|
1130
|
+
exit "$migration_exit_code"
|
|
209
1131
|
fi
|
|
210
1132
|
|
|
211
1133
|
# ── Start service ──
|
|
212
1134
|
update_status "starting" "starting"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
fi
|
|
219
|
-
update_status "failed" "failed" "Failed to start service after update"
|
|
1135
|
+
write_attempt "active" "starting" "" "false" "true"
|
|
1136
|
+
if ! sudo -n systemctl start "$SERVICE_NAME" 2>&1 || ! wait_for_started_service; then
|
|
1137
|
+
hold_attempt "start_failed" "Target service did not reach verified health after migration"
|
|
1138
|
+
update_status "failed" "failed" "Target service failed to start or become healthy; update is held for recovery"
|
|
1139
|
+
FINALIZED="true"
|
|
220
1140
|
exit 1
|
|
221
|
-
|
|
1141
|
+
fi
|
|
222
1142
|
|
|
223
1143
|
# ── Cleanup old versions (keep max 2 previous) ──
|
|
224
1144
|
cd "$IMAGE_BASE_DIR"
|
|
@@ -239,7 +1159,9 @@ if [ "$INSTALL_TYPE" = "image" ]; then
|
|
|
239
1159
|
fi
|
|
240
1160
|
|
|
241
1161
|
# ── Mark complete ──
|
|
1162
|
+
release_attempt
|
|
242
1163
|
update_status "complete" "complete"
|
|
1164
|
+
FINALIZED="true"
|
|
243
1165
|
trap - EXIT
|
|
244
1166
|
exit 0
|
|
245
1167
|
fi
|