charness 3.6.1 → 3.7.0
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/CHANGELOG.md +267 -0
- package/DEVELOPMENT.md +31 -5
- package/MIGRATION_RENAME_TO_CHARNESS.md +3 -2
- package/README.md +84 -782
- package/SETUP.md +37 -10
- package/agent-plugin/.claude-plugin/plugin.json +7 -0
- package/agent-plugin/README.md +35 -0
- package/agent-plugin/skills/charness-delegate-claude/SKILL.md +85 -0
- package/agent-plugin/skills/charness-delegate-claude/summarize-events.py +90 -0
- package/agent-plugin/skills/charness-delegate-opencode/SKILL.md +68 -0
- package/agent-plugin/skills/charness-manager/SKILL.md +152 -0
- package/dist/adapters/adapterNames.d.ts +6 -0
- package/dist/adapters/adapterNames.d.ts.map +1 -1
- package/dist/adapters/adapterNames.js +8 -2
- package/dist/adapters/adapterNames.js.map +1 -1
- package/dist/adapters/claudeCliAdapter.d.ts +8 -0
- package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeCliAdapter.js +42 -5
- package/dist/adapters/claudeCliAdapter.js.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +10 -9
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +53 -55
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +8 -0
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +14 -2
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +43 -6
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +95 -6
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +1 -1
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/apiRetryKick.d.ts +4 -3
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +16 -9
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +1313 -381
- package/dist/bot.js.map +1 -1
- package/dist/cli/claudeProfiles.d.ts +11 -6
- package/dist/cli/claudeProfiles.d.ts.map +1 -1
- package/dist/cli/claudeProfiles.js +50 -22
- package/dist/cli/claudeProfiles.js.map +1 -1
- package/dist/cli/hot.d.ts +5 -0
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +71 -25
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli/tscWatchGuard.d.ts +95 -0
- package/dist/cli/tscWatchGuard.d.ts.map +1 -0
- package/dist/cli/tscWatchGuard.js +295 -0
- package/dist/cli/tscWatchGuard.js.map +1 -0
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/connectors/jira/answerSink.d.ts +10 -0
- package/dist/connectors/jira/answerSink.d.ts.map +1 -1
- package/dist/connectors/jira/answerSink.js +13 -0
- package/dist/connectors/jira/answerSink.js.map +1 -1
- package/dist/connectors/jira/config.d.ts +2 -0
- package/dist/connectors/jira/config.d.ts.map +1 -1
- package/dist/connectors/jira/config.js +9 -1
- package/dist/connectors/jira/config.js.map +1 -1
- package/dist/connectors/jira/connector.d.ts +11 -1
- package/dist/connectors/jira/connector.d.ts.map +1 -1
- package/dist/connectors/jira/connector.js +7 -0
- package/dist/connectors/jira/connector.js.map +1 -1
- package/dist/connectors/jira/inbound.d.ts +27 -4
- package/dist/connectors/jira/inbound.d.ts.map +1 -1
- package/dist/connectors/jira/inbound.js +69 -8
- package/dist/connectors/jira/inbound.js.map +1 -1
- package/dist/connectors/jira/issueBlocks.d.ts +5 -0
- package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
- package/dist/connectors/jira/issueBlocks.js +11 -0
- package/dist/connectors/jira/issueBlocks.js.map +1 -1
- package/dist/connectors/telegram/bindPicker.d.ts +5 -0
- package/dist/connectors/telegram/bindPicker.d.ts.map +1 -1
- package/dist/connectors/telegram/bindPicker.js +18 -1
- package/dist/connectors/telegram/bindPicker.js.map +1 -1
- package/dist/connectors/telegram/bindPickerClaims.d.ts +46 -0
- package/dist/connectors/telegram/bindPickerClaims.d.ts.map +1 -0
- package/dist/connectors/telegram/bindPickerClaims.js +83 -0
- package/dist/connectors/telegram/bindPickerClaims.js.map +1 -0
- package/dist/connectors/telegram/bindSweep.d.ts +32 -0
- package/dist/connectors/telegram/bindSweep.d.ts.map +1 -0
- package/dist/connectors/telegram/bindSweep.js +47 -0
- package/dist/connectors/telegram/bindSweep.js.map +1 -0
- package/dist/connectors/telegram/bindingWelcome.d.ts +23 -0
- package/dist/connectors/telegram/bindingWelcome.d.ts.map +1 -0
- package/dist/connectors/telegram/bindingWelcome.js +26 -0
- package/dist/connectors/telegram/bindingWelcome.js.map +1 -0
- package/dist/connectors/telegram/botApiConnection.d.ts +74 -0
- package/dist/connectors/telegram/botApiConnection.d.ts.map +1 -0
- package/dist/connectors/telegram/botApiConnection.js +201 -0
- package/dist/connectors/telegram/botApiConnection.js.map +1 -0
- package/dist/connectors/telegram/commands/botCore.d.ts +8 -2
- package/dist/connectors/telegram/commands/botCore.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/botSkills.d.ts +21 -0
- package/dist/connectors/telegram/commands/botSkills.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/botSkills.js +133 -0
- package/dist/connectors/telegram/commands/botSkills.js.map +1 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts +5 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/callbackMessage.js +9 -0
- package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
- package/dist/connectors/telegram/commands/compaction.d.ts +65 -3
- package/dist/connectors/telegram/commands/compaction.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/compaction.js +512 -94
- package/dist/connectors/telegram/commands/compaction.js.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.js +6 -18
- package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.js +5 -5
- package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
- package/dist/connectors/telegram/commands/reminders.d.ts +1 -1
- package/dist/connectors/telegram/commands/reminders.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/reminders.js +4 -15
- package/dist/connectors/telegram/commands/reminders.js.map +1 -1
- package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/selfUpdate.js +1 -11
- package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -1
- package/dist/connectors/telegram/commands/taskAgents.d.ts +22 -0
- package/dist/connectors/telegram/commands/taskAgents.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/taskAgents.js +104 -0
- package/dist/connectors/telegram/commands/taskAgents.js.map +1 -0
- package/dist/connectors/telegram/commands/todo.d.ts +82 -0
- package/dist/connectors/telegram/commands/todo.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/todo.js +384 -0
- package/dist/connectors/telegram/commands/todo.js.map +1 -0
- package/dist/connectors/telegram/commands/topicCommand.d.ts +6 -2
- package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/topicCommand.js +6 -9
- package/dist/connectors/telegram/commands/topicCommand.js.map +1 -1
- package/dist/connectors/telegram/openBindMessages.d.ts +45 -0
- package/dist/connectors/telegram/openBindMessages.d.ts.map +1 -0
- package/dist/connectors/telegram/openBindMessages.js +67 -0
- package/dist/connectors/telegram/openBindMessages.js.map +1 -0
- package/dist/connectors/telegram/outputFlushPlan.d.ts +49 -0
- package/dist/connectors/telegram/outputFlushPlan.d.ts.map +1 -1
- package/dist/connectors/telegram/outputFlushPlan.js +46 -0
- package/dist/connectors/telegram/outputFlushPlan.js.map +1 -1
- package/dist/connectors/telegram/pendingTopicName.d.ts +26 -0
- package/dist/connectors/telegram/pendingTopicName.d.ts.map +1 -0
- package/dist/connectors/telegram/pendingTopicName.js +27 -0
- package/dist/connectors/telegram/pendingTopicName.js.map +1 -0
- package/dist/connectors/test/connector.d.ts.map +1 -1
- package/dist/connectors/test/connector.js +6 -1
- package/dist/connectors/test/connector.js.map +1 -1
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +90 -7
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +90 -7
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +90 -7
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +90 -7
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +90 -7
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +90 -7
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +90 -7
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +90 -7
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +90 -7
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +90 -7
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +90 -7
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +90 -7
- package/dist/i18n/zh.js.map +1 -1
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/installManager.d.ts +13 -0
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +38 -4
- package/dist/installManager.js.map +1 -1
- package/dist/instanceSettings.d.ts +40 -1
- package/dist/instanceSettings.d.ts.map +1 -1
- package/dist/instanceSettings.js +105 -26
- package/dist/instanceSettings.js.map +1 -1
- package/dist/openCodeQuestionFlow.d.ts +30 -0
- package/dist/openCodeQuestionFlow.d.ts.map +1 -1
- package/dist/openCodeQuestionFlow.js +43 -0
- package/dist/openCodeQuestionFlow.js.map +1 -1
- package/dist/postToSession.d.ts +20 -5
- package/dist/postToSession.d.ts.map +1 -1
- package/dist/postToSession.js +38 -7
- package/dist/postToSession.js.map +1 -1
- package/dist/remoteRun/cliArgs.d.ts +73 -0
- package/dist/remoteRun/cliArgs.d.ts.map +1 -0
- package/dist/remoteRun/cliArgs.js +197 -0
- package/dist/remoteRun/cliArgs.js.map +1 -0
- package/dist/remoteRun/commands.d.ts +56 -0
- package/dist/remoteRun/commands.d.ts.map +1 -0
- package/dist/remoteRun/commands.js +306 -0
- package/dist/remoteRun/commands.js.map +1 -0
- package/dist/remoteRun/depsPlan.d.ts +16 -0
- package/dist/remoteRun/depsPlan.d.ts.map +1 -0
- package/dist/remoteRun/depsPlan.js +82 -0
- package/dist/remoteRun/depsPlan.js.map +1 -0
- package/dist/remoteRun/exitCodes.d.ts +34 -0
- package/dist/remoteRun/exitCodes.d.ts.map +1 -0
- package/dist/remoteRun/exitCodes.js +38 -0
- package/dist/remoteRun/exitCodes.js.map +1 -0
- package/dist/remoteRun/foreground.d.ts +29 -0
- package/dist/remoteRun/foreground.d.ts.map +1 -0
- package/dist/remoteRun/foreground.js +159 -0
- package/dist/remoteRun/foreground.js.map +1 -0
- package/dist/remoteRun/interrupts.d.ts +17 -0
- package/dist/remoteRun/interrupts.d.ts.map +1 -0
- package/dist/remoteRun/interrupts.js +41 -0
- package/dist/remoteRun/interrupts.js.map +1 -0
- package/dist/remoteRun/jobFiles.d.ts +31 -0
- package/dist/remoteRun/jobFiles.d.ts.map +1 -0
- package/dist/remoteRun/jobFiles.js +84 -0
- package/dist/remoteRun/jobFiles.js.map +1 -0
- package/dist/remoteRun/jobFormat.d.ts +16 -0
- package/dist/remoteRun/jobFormat.d.ts.map +1 -0
- package/dist/remoteRun/jobFormat.js +130 -0
- package/dist/remoteRun/jobFormat.js.map +1 -0
- package/dist/remoteRun/jobId.d.ts +15 -0
- package/dist/remoteRun/jobId.d.ts.map +1 -0
- package/dist/remoteRun/jobId.js +85 -0
- package/dist/remoteRun/jobId.js.map +1 -0
- package/dist/remoteRun/jobMeta.d.ts +18 -0
- package/dist/remoteRun/jobMeta.d.ts.map +1 -0
- package/dist/remoteRun/jobMeta.js +33 -0
- package/dist/remoteRun/jobMeta.js.map +1 -0
- package/dist/remoteRun/jobQuery.d.ts +12 -0
- package/dist/remoteRun/jobQuery.d.ts.map +1 -0
- package/dist/remoteRun/jobQuery.js +22 -0
- package/dist/remoteRun/jobQuery.js.map +1 -0
- package/dist/remoteRun/jobRecords.d.ts +22 -0
- package/dist/remoteRun/jobRecords.d.ts.map +1 -0
- package/dist/remoteRun/jobRecords.js +145 -0
- package/dist/remoteRun/jobRecords.js.map +1 -0
- package/dist/remoteRun/jobState.d.ts +62 -0
- package/dist/remoteRun/jobState.d.ts.map +1 -0
- package/dist/remoteRun/jobState.js +132 -0
- package/dist/remoteRun/jobState.js.map +1 -0
- package/dist/remoteRun/leaseTiming.d.ts +18 -0
- package/dist/remoteRun/leaseTiming.d.ts.map +1 -0
- package/dist/remoteRun/leaseTiming.js +43 -0
- package/dist/remoteRun/leaseTiming.js.map +1 -0
- package/dist/remoteRun/output.d.ts +35 -0
- package/dist/remoteRun/output.d.ts.map +1 -0
- package/dist/remoteRun/output.js +82 -0
- package/dist/remoteRun/output.js.map +1 -0
- package/dist/remoteRun/pathPatterns.d.ts +33 -0
- package/dist/remoteRun/pathPatterns.d.ts.map +1 -0
- package/dist/remoteRun/pathPatterns.js +115 -0
- package/dist/remoteRun/pathPatterns.js.map +1 -0
- package/dist/remoteRun/probe.d.ts +56 -0
- package/dist/remoteRun/probe.d.ts.map +1 -0
- package/dist/remoteRun/probe.js +123 -0
- package/dist/remoteRun/probe.js.map +1 -0
- package/dist/remoteRun/project.d.ts +26 -0
- package/dist/remoteRun/project.d.ts.map +1 -0
- package/dist/remoteRun/project.js +159 -0
- package/dist/remoteRun/project.js.map +1 -0
- package/dist/remoteRun/projectConfig.d.ts +38 -0
- package/dist/remoteRun/projectConfig.d.ts.map +1 -0
- package/dist/remoteRun/projectConfig.js +87 -0
- package/dist/remoteRun/projectConfig.js.map +1 -0
- package/dist/remoteRun/remoteRunCli.d.ts +15 -0
- package/dist/remoteRun/remoteRunCli.d.ts.map +1 -0
- package/dist/remoteRun/remoteRunCli.js +144 -0
- package/dist/remoteRun/remoteRunCli.js.map +1 -0
- package/dist/remoteRun/remoteRunError.d.ts +14 -0
- package/dist/remoteRun/remoteRunError.d.ts.map +1 -0
- package/dist/remoteRun/remoteRunError.js +28 -0
- package/dist/remoteRun/remoteRunError.js.map +1 -0
- package/dist/remoteRun/rsync.d.ts +17 -0
- package/dist/remoteRun/rsync.d.ts.map +1 -0
- package/dist/remoteRun/rsync.js +39 -0
- package/dist/remoteRun/rsync.js.map +1 -0
- package/dist/remoteRun/runnerClient.d.ts +50 -0
- package/dist/remoteRun/runnerClient.d.ts.map +1 -0
- package/dist/remoteRun/runnerClient.js +139 -0
- package/dist/remoteRun/runnerClient.js.map +1 -0
- package/dist/remoteRun/runners.d.ts +57 -0
- package/dist/remoteRun/runners.d.ts.map +1 -0
- package/dist/remoteRun/runners.js +181 -0
- package/dist/remoteRun/runners.js.map +1 -0
- package/dist/remoteRun/runnersConfig.d.ts +44 -0
- package/dist/remoteRun/runnersConfig.d.ts.map +1 -0
- package/dist/remoteRun/runnersConfig.js +107 -0
- package/dist/remoteRun/runnersConfig.js.map +1 -0
- package/dist/remoteRun/safeCopy.d.ts +31 -0
- package/dist/remoteRun/safeCopy.d.ts.map +1 -0
- package/dist/remoteRun/safeCopy.js +137 -0
- package/dist/remoteRun/safeCopy.js.map +1 -0
- package/dist/remoteRun/sendPlan.d.ts +67 -0
- package/dist/remoteRun/sendPlan.d.ts.map +1 -0
- package/dist/remoteRun/sendPlan.js +139 -0
- package/dist/remoteRun/sendPlan.js.map +1 -0
- package/dist/remoteRun/ssh.d.ts +62 -0
- package/dist/remoteRun/ssh.d.ts.map +1 -0
- package/dist/remoteRun/ssh.js +202 -0
- package/dist/remoteRun/ssh.js.map +1 -0
- package/dist/remoteRun/submit.d.ts +29 -0
- package/dist/remoteRun/submit.d.ts.map +1 -0
- package/dist/remoteRun/submit.js +142 -0
- package/dist/remoteRun/submit.js.map +1 -0
- package/dist/requests/answerRequest.d.ts +5 -0
- package/dist/requests/answerRequest.d.ts.map +1 -1
- package/dist/requests/answerRequest.js +16 -1
- package/dist/requests/answerRequest.js.map +1 -1
- package/dist/requests/requestLedger.d.ts +13 -0
- package/dist/requests/requestLedger.d.ts.map +1 -1
- package/dist/requests/requestLedger.js +9 -0
- package/dist/requests/requestLedger.js.map +1 -1
- package/dist/requests/topicRequestSettle.d.ts +30 -0
- package/dist/requests/topicRequestSettle.d.ts.map +1 -0
- package/dist/requests/topicRequestSettle.js +23 -0
- package/dist/requests/topicRequestSettle.js.map +1 -0
- package/dist/requests/wakeUpEngine.d.ts +4 -0
- package/dist/requests/wakeUpEngine.d.ts.map +1 -1
- package/dist/requests/wakeUpEngine.js +5 -1
- package/dist/requests/wakeUpEngine.js.map +1 -1
- package/dist/scheduler/delivery.d.ts +2 -0
- package/dist/scheduler/delivery.d.ts.map +1 -1
- package/dist/scheduler/delivery.js +8 -4
- package/dist/scheduler/delivery.js.map +1 -1
- package/dist/scheduler/mcpSurface.d.ts +59 -10
- package/dist/scheduler/mcpSurface.d.ts.map +1 -1
- package/dist/scheduler/mcpSurface.js +276 -79
- package/dist/scheduler/mcpSurface.js.map +1 -1
- package/dist/scheduler/recurrence.d.ts +5 -0
- package/dist/scheduler/recurrence.d.ts.map +1 -1
- package/dist/scheduler/recurrence.js +12 -2
- package/dist/scheduler/recurrence.js.map +1 -1
- package/dist/scheduler/strictToolInput.d.ts +45 -0
- package/dist/scheduler/strictToolInput.d.ts.map +1 -0
- package/dist/scheduler/strictToolInput.js +155 -0
- package/dist/scheduler/strictToolInput.js.map +1 -0
- package/dist/scheduler/types.d.ts +5 -0
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler/workerWatch.d.ts +23 -0
- package/dist/scheduler/workerWatch.d.ts.map +1 -0
- package/dist/scheduler/workerWatch.js +26 -0
- package/dist/scheduler/workerWatch.js.map +1 -0
- package/dist/selfUpdate/emittedSource.d.ts +11 -0
- package/dist/selfUpdate/emittedSource.d.ts.map +1 -0
- package/dist/selfUpdate/emittedSource.js +24 -0
- package/dist/selfUpdate/emittedSource.js.map +1 -0
- package/dist/selfUpdate/gitSource.d.ts +0 -6
- package/dist/selfUpdate/gitSource.d.ts.map +1 -1
- package/dist/selfUpdate/gitSource.js +2 -18
- package/dist/selfUpdate/gitSource.js.map +1 -1
- package/dist/selfUpdate/updatePending.d.ts +1 -1
- package/dist/sendErrorClassifier.d.ts +10 -0
- package/dist/sendErrorClassifier.d.ts.map +1 -1
- package/dist/sendErrorClassifier.js +22 -0
- package/dist/sendErrorClassifier.js.map +1 -1
- package/dist/state.d.ts +78 -7
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +125 -8
- package/dist/state.js.map +1 -1
- package/dist/stateFile.d.ts +9 -0
- package/dist/stateFile.d.ts.map +1 -0
- package/dist/stateFile.js +57 -0
- package/dist/stateFile.js.map +1 -0
- package/dist/stt/sttInstall.d.ts +7 -3
- package/dist/stt/sttInstall.d.ts.map +1 -1
- package/dist/stt/sttInstall.js +8 -4
- package/dist/stt/sttInstall.js.map +1 -1
- package/dist/stt/sttSettings.d.ts +2 -1
- package/dist/stt/sttSettings.d.ts.map +1 -1
- package/dist/stt/sttSettings.js +3 -2
- package/dist/stt/sttSettings.js.map +1 -1
- package/dist/stt/transcribeChain.js +2 -0
- package/dist/stt/transcribeChain.js.map +1 -1
- package/dist/testTimings.d.ts +13 -3
- package/dist/testTimings.d.ts.map +1 -1
- package/dist/testTimings.js +26 -2
- package/dist/testTimings.js.map +1 -1
- package/dist/todos/board.d.ts +83 -0
- package/dist/todos/board.d.ts.map +1 -0
- package/dist/todos/board.js +227 -0
- package/dist/todos/board.js.map +1 -0
- package/dist/todos/format.d.ts +33 -0
- package/dist/todos/format.d.ts.map +1 -0
- package/dist/todos/format.js +90 -0
- package/dist/todos/format.js.map +1 -0
- package/dist/todos/merge.d.ts +46 -0
- package/dist/todos/merge.d.ts.map +1 -0
- package/dist/todos/merge.js +137 -0
- package/dist/todos/merge.js.map +1 -0
- package/dist/todos/midTurn.d.ts +75 -0
- package/dist/todos/midTurn.d.ts.map +1 -0
- package/dist/todos/midTurn.js +214 -0
- package/dist/todos/midTurn.js.map +1 -0
- package/dist/todos/nag.d.ts +85 -0
- package/dist/todos/nag.d.ts.map +1 -0
- package/dist/todos/nag.js +103 -0
- package/dist/todos/nag.js.map +1 -0
- package/dist/todos/order.d.ts +25 -0
- package/dist/todos/order.d.ts.map +1 -0
- package/dist/todos/order.js +61 -0
- package/dist/todos/order.js.map +1 -0
- package/dist/todos/projects.d.ts +102 -0
- package/dist/todos/projects.d.ts.map +1 -0
- package/dist/todos/projects.js +342 -0
- package/dist/todos/projects.js.map +1 -0
- package/dist/todos/registry.d.ts +64 -0
- package/dist/todos/registry.d.ts.map +1 -0
- package/dist/todos/registry.js +116 -0
- package/dist/todos/registry.js.map +1 -0
- package/dist/todos/reminderEngine.d.ts +78 -0
- package/dist/todos/reminderEngine.d.ts.map +1 -0
- package/dist/todos/reminderEngine.js +162 -0
- package/dist/todos/reminderEngine.js.map +1 -0
- package/dist/todos/store.d.ts +34 -0
- package/dist/todos/store.d.ts.map +1 -0
- package/dist/todos/store.js +208 -0
- package/dist/todos/store.js.map +1 -0
- package/dist/todos/tools.d.ts +26 -0
- package/dist/todos/tools.d.ts.map +1 -0
- package/dist/todos/tools.js +25 -0
- package/dist/todos/tools.js.map +1 -0
- package/dist/todos/types.d.ts +85 -0
- package/dist/todos/types.d.ts.map +1 -0
- package/dist/todos/types.js +30 -0
- package/dist/todos/types.js.map +1 -0
- package/dist/trackers/taskAgentLimit.d.ts +58 -0
- package/dist/trackers/taskAgentLimit.d.ts.map +1 -0
- package/dist/trackers/taskAgentLimit.js +62 -0
- package/dist/trackers/taskAgentLimit.js.map +1 -0
- package/dist/trackers/taskAgentSlots.d.ts +109 -0
- package/dist/trackers/taskAgentSlots.d.ts.map +1 -0
- package/dist/trackers/taskAgentSlots.js +227 -0
- package/dist/trackers/taskAgentSlots.js.map +1 -0
- package/dist/trackers/types.d.ts +14 -0
- package/dist/trackers/types.d.ts.map +1 -0
- package/dist/trackers/types.js +3 -0
- package/dist/trackers/types.js.map +1 -0
- package/dist/types.d.ts +80 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/adoptReplayGate.d.ts +16 -0
- package/dist/utils/adoptReplayGate.d.ts.map +1 -0
- package/dist/utils/adoptReplayGate.js +35 -0
- package/dist/utils/adoptReplayGate.js.map +1 -0
- package/dist/utils/agentPlugin.d.ts +113 -0
- package/dist/utils/agentPlugin.d.ts.map +1 -0
- package/dist/utils/agentPlugin.js +192 -0
- package/dist/utils/agentPlugin.js.map +1 -0
- package/dist/utils/autoContinueOnLimit.d.ts +7 -4
- package/dist/utils/autoContinueOnLimit.d.ts.map +1 -1
- package/dist/utils/autoContinueOnLimit.js +8 -5
- package/dist/utils/autoContinueOnLimit.js.map +1 -1
- package/dist/utils/botSkills.d.ts +62 -0
- package/dist/utils/botSkills.d.ts.map +1 -0
- package/dist/utils/botSkills.js +53 -0
- package/dist/utils/botSkills.js.map +1 -0
- package/dist/utils/botSkillsSetting.d.ts +7 -0
- package/dist/utils/botSkillsSetting.d.ts.map +1 -0
- package/dist/utils/botSkillsSetting.js +9 -0
- package/dist/utils/botSkillsSetting.js.map +1 -0
- package/dist/utils/claudeCodeVersion.d.ts +7 -0
- package/dist/utils/claudeCodeVersion.d.ts.map +1 -1
- package/dist/utils/claudeCodeVersion.js +14 -1
- package/dist/utils/claudeCodeVersion.js.map +1 -1
- package/dist/utils/claudeCompactHook.d.ts +33 -15
- package/dist/utils/claudeCompactHook.d.ts.map +1 -1
- package/dist/utils/claudeCompactHook.js +36 -17
- package/dist/utils/claudeCompactHook.js.map +1 -1
- package/dist/utils/claudeCompactSummary.d.ts +15 -0
- package/dist/utils/claudeCompactSummary.d.ts.map +1 -0
- package/dist/utils/claudeCompactSummary.js +96 -0
- package/dist/utils/claudeCompactSummary.js.map +1 -0
- package/dist/utils/claudeLimitSwitch.d.ts +11 -9
- package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
- package/dist/utils/claudeLimitSwitch.js +18 -16
- package/dist/utils/claudeLimitSwitch.js.map +1 -1
- package/dist/utils/claudeProfiles.d.ts +5 -0
- package/dist/utils/claudeProfiles.d.ts.map +1 -1
- package/dist/utils/claudeProfiles.js +14 -0
- package/dist/utils/claudeProfiles.js.map +1 -1
- package/dist/utils/claudeSessionTitle.d.ts +2 -1
- package/dist/utils/claudeSessionTitle.d.ts.map +1 -1
- package/dist/utils/claudeSessionTitle.js +14 -14
- package/dist/utils/claudeSessionTitle.js.map +1 -1
- package/dist/utils/claudeUsage.d.ts +95 -15
- package/dist/utils/claudeUsage.d.ts.map +1 -1
- package/dist/utils/claudeUsage.js +241 -41
- package/dist/utils/claudeUsage.js.map +1 -1
- package/dist/utils/claudeUsageRefresher.d.ts +10 -0
- package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
- package/dist/utils/claudeUsageRefresher.js +11 -0
- package/dist/utils/claudeUsageRefresher.js.map +1 -1
- package/dist/utils/claudeUserEntry.d.ts +50 -0
- package/dist/utils/claudeUserEntry.d.ts.map +1 -0
- package/dist/utils/claudeUserEntry.js +120 -0
- package/dist/utils/claudeUserEntry.js.map +1 -0
- package/dist/utils/clearThreadOutputQueues.d.ts +4 -3
- package/dist/utils/clearThreadOutputQueues.d.ts.map +1 -1
- package/dist/utils/clearThreadOutputQueues.js +1 -1
- package/dist/utils/clearThreadOutputQueues.js.map +1 -1
- package/dist/utils/compactOnIdle.d.ts +33 -13
- package/dist/utils/compactOnIdle.d.ts.map +1 -1
- package/dist/utils/compactOnIdle.js +35 -15
- package/dist/utils/compactOnIdle.js.map +1 -1
- package/dist/utils/fallbackBridge.d.ts +5 -0
- package/dist/utils/fallbackBridge.d.ts.map +1 -1
- package/dist/utils/fallbackBridge.js +19 -3
- package/dist/utils/fallbackBridge.js.map +1 -1
- package/dist/utils/fallbackChain.d.ts +9 -3
- package/dist/utils/fallbackChain.d.ts.map +1 -1
- package/dist/utils/fallbackChain.js +21 -3
- package/dist/utils/fallbackChain.js.map +1 -1
- package/dist/utils/fileSendPlan.d.ts +6 -0
- package/dist/utils/fileSendPlan.d.ts.map +1 -1
- package/dist/utils/fileSendPlan.js +11 -0
- package/dist/utils/fileSendPlan.js.map +1 -1
- package/dist/utils/fileSendService.d.ts +7 -1
- package/dist/utils/fileSendService.d.ts.map +1 -1
- package/dist/utils/fileSendService.js +6 -2
- package/dist/utils/fileSendService.js.map +1 -1
- package/dist/utils/groupFinalizePlan.d.ts +8 -10
- package/dist/utils/groupFinalizePlan.d.ts.map +1 -1
- package/dist/utils/groupFinalizePlan.js +8 -12
- package/dist/utils/groupFinalizePlan.js.map +1 -1
- package/dist/utils/jsonStreamHost.d.ts +13 -0
- package/dist/utils/jsonStreamHost.d.ts.map +1 -1
- package/dist/utils/jsonStreamHost.js +13 -0
- package/dist/utils/jsonStreamHost.js.map +1 -1
- package/dist/utils/messageSendService.d.ts +5 -3
- package/dist/utils/messageSendService.d.ts.map +1 -1
- package/dist/utils/messageSendService.js +12 -5
- package/dist/utils/messageSendService.js.map +1 -1
- package/dist/utils/openCodeCompactPlugin.d.ts +39 -14
- package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
- package/dist/utils/openCodeCompactPlugin.js +106 -38
- package/dist/utils/openCodeCompactPlugin.js.map +1 -1
- package/dist/utils/promptDelivery.d.ts +29 -12
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +13 -5
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/sentKindsSummary.d.ts +10 -0
- package/dist/utils/sentKindsSummary.d.ts.map +1 -0
- package/dist/utils/sentKindsSummary.js +18 -0
- package/dist/utils/sentKindsSummary.js.map +1 -0
- package/dist/utils/telegramError.d.ts +6 -0
- package/dist/utils/telegramError.d.ts.map +1 -1
- package/dist/utils/telegramError.js +12 -0
- package/dist/utils/telegramError.js.map +1 -1
- package/dist/utils/topicView.d.ts +1 -1
- package/dist/utils/topicView.js +1 -1
- package/dist/utils/transcribeAudio.d.ts +10 -0
- package/dist/utils/transcribeAudio.d.ts.map +1 -1
- package/dist/utils/transcribeAudio.js +22 -3
- package/dist/utils/transcribeAudio.js.map +1 -1
- package/dist/utils/transcriptErrorTime.d.ts +8 -0
- package/dist/utils/transcriptErrorTime.d.ts.map +1 -0
- package/dist/utils/transcriptErrorTime.js +56 -0
- package/dist/utils/transcriptErrorTime.js.map +1 -0
- package/dist/utils/wedgeRecovery.d.ts +2 -0
- package/dist/utils/wedgeRecovery.d.ts.map +1 -1
- package/dist/utils/wedgeRecovery.js +1 -1
- package/dist/utils/wedgeRecovery.js.map +1 -1
- package/package.json +11 -3
- package/remoteRun/runner.sh +1242 -0
- package/stt/gigaam_sidecar.py +555 -50
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# charness remote-run — the script that runs ON a runner (never on the main box).
|
|
3
|
+
#
|
|
4
|
+
# `charness remote-run` uploads this file to <workDir>/bin/runner-<sha8>.sh and calls it over ssh. Every subcommand takes
|
|
5
|
+
# the runner's work dir first: runner.sh <subcommand> <workDir> [arguments…]
|
|
6
|
+
# Pueue is reached through PUEUE_BIN / PUEUE_CONFIG (both optional), set by the caller on every call.
|
|
7
|
+
#
|
|
8
|
+
# bash 3.2-compatible on purpose (the stock bash of macOS): no associative arrays, no `wait -n`, no `mapfile`, no
|
|
9
|
+
# `timeout(1)`, no `read -t` (its timeout status differs between 3.2 and 4+), no GNU-only flags. Time limits are done by background watchdogs.
|
|
10
|
+
#
|
|
11
|
+
# Subcommands (the contract is in src/remoteRun/README.md):
|
|
12
|
+
# probe key=value lines: OS, CPU, RAM, disk, jobs, capabilities
|
|
13
|
+
# prepare layout for a job, Pueue daemon + groups, prune
|
|
14
|
+
# lease add the job to Pueue, stream its log, kill it when the caller's heartbeat stops (foreground run)
|
|
15
|
+
# submit add the job to Pueue and return (--bg)
|
|
16
|
+
# job the Pueue task itself: lock → apply the staged tree → deps → command → result files
|
|
17
|
+
# follow stream a job's log until it ends (logs -f)
|
|
18
|
+
# wait block until the job ends
|
|
19
|
+
# result key=value facts of a job (exit code, phase, markers)
|
|
20
|
+
# meta the job's meta.json
|
|
21
|
+
# tail the last N lines of a job's log
|
|
22
|
+
# kill stop a job: SIGTERM, SIGKILL after 10 s
|
|
23
|
+
# exec one-off command in the project's copy, leased like a foreground job
|
|
24
|
+
# workdir the work dir as an absolute path
|
|
25
|
+
# pueue run Pueue itself with the configured binary and config
|
|
26
|
+
# jobs the meta.json of every job of a project (or of all), as a JSON array
|
|
27
|
+
# prune remove old jobs, stale stage dirs and unused project copies
|
|
28
|
+
# sync-tree the apply step alone (tests)
|
|
29
|
+
|
|
30
|
+
LC_ALL=C
|
|
31
|
+
export LC_ALL
|
|
32
|
+
set -u
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────── constants ───────────────────────────
|
|
35
|
+
|
|
36
|
+
kill_grace_sec=10
|
|
37
|
+
pueue_start_wait_sec=8
|
|
38
|
+
default_timeout_sec=10800
|
|
39
|
+
job_keep_days=3
|
|
40
|
+
copy_keep_days=30
|
|
41
|
+
stage_keep_days=1
|
|
42
|
+
drain_interval_sec=0.2
|
|
43
|
+
probe_step_sec=4
|
|
44
|
+
exit_file_wait_sec=5
|
|
45
|
+
task_id_wait_sec=5
|
|
46
|
+
heartbeat_poll_sec=1
|
|
47
|
+
# How long a job waiting for its project's lock sleeps between two attempts (a test shortens it to race many waiters quickly).
|
|
48
|
+
lock_poll_sec=1
|
|
49
|
+
|
|
50
|
+
# ─────────────────────────── helpers ───────────────────────────
|
|
51
|
+
|
|
52
|
+
die() {
|
|
53
|
+
printf 'remote-run-runner: %s\n' "$*" >&2
|
|
54
|
+
exit 125
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Own progress lines of a job, written into its log; the caller shows lines with this prefix on stderr.
|
|
58
|
+
say() {
|
|
59
|
+
printf 'remote-run: %s\n' "$*"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pq() {
|
|
63
|
+
"${PUEUE_BIN:-pueue}" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} "$@"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pueued_bin() {
|
|
67
|
+
if [ -n "${PUEUE_BIN:-}" ] && [ -x "$(dirname "$PUEUE_BIN")/pueued" ]; then
|
|
68
|
+
printf '%s\n' "$(dirname "$PUEUE_BIN")/pueued"
|
|
69
|
+
else
|
|
70
|
+
printf 'pueued\n'
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# A name that is safe to use as one path component.
|
|
75
|
+
check_name() {
|
|
76
|
+
case $1 in
|
|
77
|
+
'' | *[!A-Za-z0-9._-]* | .*) die "bad name: $1" ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# A relative path inside a project: no leading slash, no ".." component.
|
|
82
|
+
check_relative_path() {
|
|
83
|
+
case $1 in
|
|
84
|
+
/*) return 1 ;;
|
|
85
|
+
esac
|
|
86
|
+
case "/$1/" in
|
|
87
|
+
*/../*) return 1 ;;
|
|
88
|
+
esac
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Sets work, slug, job and every directory derived from them. A slug of "-" means "the project this job id belongs to" (the commands
|
|
93
|
+
# that take only a job id): found by looking for the job's folder.
|
|
94
|
+
set_paths() {
|
|
95
|
+
work=$1
|
|
96
|
+
slug=${2:-}
|
|
97
|
+
job=${3:-}
|
|
98
|
+
if [ "$slug" = "-" ] && [ -n "$job" ]; then
|
|
99
|
+
slug=
|
|
100
|
+
local found
|
|
101
|
+
for found in "$work"/p/*/jobs/"$job"; do
|
|
102
|
+
[ -d "$found" ] && slug=$(basename "$(dirname "$(dirname "$found")")") && break
|
|
103
|
+
done
|
|
104
|
+
[ -n "$slug" ] || die "no such job: $job"
|
|
105
|
+
fi
|
|
106
|
+
check_name "$slug"
|
|
107
|
+
proj="$work/p/$slug"
|
|
108
|
+
tree="$proj/tree"
|
|
109
|
+
jobs_dir="$proj/jobs"
|
|
110
|
+
lock="$proj/lock"
|
|
111
|
+
if [ -n "$job" ]; then
|
|
112
|
+
case $job in *[!a-z0-9-]* | -*) die "bad job id: $job" ;; esac
|
|
113
|
+
jd="$jobs_dir/$job"
|
|
114
|
+
stage="$proj/stage/$job"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set_phase() {
|
|
119
|
+
printf '%s\n' "$1" > "$jd/phase"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
read_file() {
|
|
123
|
+
cat "$1" 2>/dev/null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# ─────────────────────────── probe ───────────────────────────
|
|
127
|
+
|
|
128
|
+
kb_to_mb() {
|
|
129
|
+
awk -v kb="$1" 'BEGIN { printf "%d", kb / 1024 }'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
probe_linux() {
|
|
133
|
+
local os_name os_version
|
|
134
|
+
os_name=Linux
|
|
135
|
+
os_version=
|
|
136
|
+
if [ -r /etc/os-release ]; then
|
|
137
|
+
os_name=$(. /etc/os-release; printf '%s' "${NAME:-Linux}")
|
|
138
|
+
os_version=$(. /etc/os-release; printf '%s' "${VERSION_ID:-}")
|
|
139
|
+
fi
|
|
140
|
+
echo "os_name=$os_name"
|
|
141
|
+
echo "os_version=$os_version"
|
|
142
|
+
echo "cpu_model=$(sed -n 's/^model name[[:space:]]*:[[:space:]]*//p' /proc/cpuinfo | head -n 1)"
|
|
143
|
+
local threads cores
|
|
144
|
+
threads=$(grep -c '^processor' /proc/cpuinfo)
|
|
145
|
+
cores=$(awk -F: '/^physical id/ { p = $2 } /^core id/ { print p ":" $2 }' /proc/cpuinfo | sort -u | wc -l | tr -d ' ')
|
|
146
|
+
[ "$cores" -gt 0 ] 2> /dev/null || cores=$threads
|
|
147
|
+
echo "cpu_cores=$cores"
|
|
148
|
+
echo "cpu_threads=$threads"
|
|
149
|
+
echo "load=$(cut -d' ' -f1 /proc/loadavg)"
|
|
150
|
+
echo "ram_total_mb=$(kb_to_mb "$(sed -n 's/^MemTotal:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo)")"
|
|
151
|
+
echo "ram_free_mb=$(kb_to_mb "$(sed -n 's/^MemAvailable:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo)")"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
probe_darwin() {
|
|
155
|
+
echo "os_name=$(sw_vers -productName 2> /dev/null || echo macOS)"
|
|
156
|
+
echo "os_version=$(sw_vers -productVersion 2> /dev/null)"
|
|
157
|
+
echo "cpu_model=$(sysctl -n machdep.cpu.brand_string 2> /dev/null)"
|
|
158
|
+
echo "cpu_cores=$(sysctl -n hw.physicalcpu 2> /dev/null)"
|
|
159
|
+
echo "cpu_threads=$(sysctl -n hw.logicalcpu 2> /dev/null)"
|
|
160
|
+
echo "load=$(sysctl -n vm.loadavg 2> /dev/null | tr -d '{}' | awk '{ print $1 }')"
|
|
161
|
+
local mem_bytes page_size free_pages
|
|
162
|
+
mem_bytes=$(sysctl -n hw.memsize 2> /dev/null)
|
|
163
|
+
page_size=$(sysctl -n hw.pagesize 2> /dev/null)
|
|
164
|
+
free_pages=$(vm_stat 2> /dev/null | awk '/^Pages (free|inactive|speculative):/ { gsub("\\.", "", $NF); sum += $NF } END { print sum + 0 }')
|
|
165
|
+
echo "ram_total_mb=$(awk -v b="$mem_bytes" 'BEGIN { printf "%d", b / 1048576 }')"
|
|
166
|
+
echo "ram_free_mb=$(awk -v p="$free_pages" -v s="${page_size:-4096}" 'BEGIN { printf "%d", p * s / 1048576 }')"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# capability lines: cap_<name>=yes|no and cap_<name>_detail=<words>
|
|
170
|
+
probe_android() {
|
|
171
|
+
local acceleration=
|
|
172
|
+
case $(uname -s) in
|
|
173
|
+
Linux) [ -r /dev/kvm ] && [ -w /dev/kvm ] && acceleration=KVM ;;
|
|
174
|
+
Darwin) [ "$(sysctl -n kern.hv_support 2> /dev/null)" = 1 ] && acceleration=HVF ;;
|
|
175
|
+
esac
|
|
176
|
+
local emulator_path=
|
|
177
|
+
local sdk_root
|
|
178
|
+
for sdk_root in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do
|
|
179
|
+
[ -n "$sdk_root" ] && [ -x "$sdk_root/emulator/emulator" ] && emulator_path="$sdk_root/emulator/emulator" && break
|
|
180
|
+
done
|
|
181
|
+
[ -z "$emulator_path" ] && emulator_path=$(command -v emulator 2> /dev/null || true)
|
|
182
|
+
if [ -n "$acceleration" ] && [ -n "$emulator_path" ]; then
|
|
183
|
+
echo "cap_android=yes"
|
|
184
|
+
echo "cap_android_detail=$acceleration, emulator"
|
|
185
|
+
else
|
|
186
|
+
echo "cap_android=no"
|
|
187
|
+
if [ -z "$acceleration" ] && [ -z "$emulator_path" ]; then
|
|
188
|
+
echo "cap_android_detail=no hardware acceleration, no emulator binary"
|
|
189
|
+
elif [ -z "$acceleration" ]; then
|
|
190
|
+
echo "cap_android_detail=no hardware acceleration (KVM / HVF)"
|
|
191
|
+
else
|
|
192
|
+
echo "cap_android_detail=no emulator binary"
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
probe_playwright() {
|
|
198
|
+
local browsers_dir=${PLAYWRIGHT_BROWSERS_PATH:-}
|
|
199
|
+
if [ -z "$browsers_dir" ]; then
|
|
200
|
+
case $(uname -s) in
|
|
201
|
+
Darwin) browsers_dir="$HOME/Library/Caches/ms-playwright" ;;
|
|
202
|
+
*) browsers_dir="$HOME/.cache/ms-playwright" ;;
|
|
203
|
+
esac
|
|
204
|
+
fi
|
|
205
|
+
local found=
|
|
206
|
+
local kind
|
|
207
|
+
for kind in chromium firefox webkit; do
|
|
208
|
+
if ls -d "$browsers_dir/$kind-"* > /dev/null 2>&1; then
|
|
209
|
+
found="${found:+$found, }$kind"
|
|
210
|
+
fi
|
|
211
|
+
done
|
|
212
|
+
if [ -n "$found" ]; then
|
|
213
|
+
echo "cap_playwright=yes"
|
|
214
|
+
echo "cap_playwright_detail=$found"
|
|
215
|
+
else
|
|
216
|
+
echo "cap_playwright=no"
|
|
217
|
+
echo "cap_playwright_detail=no browsers installed"
|
|
218
|
+
fi
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Jobs by their files: running = a live job process, queued = submitted and not started, finished = has an exit file.
|
|
222
|
+
probe_jobs() {
|
|
223
|
+
local running=0 queued=0
|
|
224
|
+
local active_slugs=
|
|
225
|
+
local job_dir slug pid
|
|
226
|
+
for job_dir in "$work"/p/*/jobs/*/; do
|
|
227
|
+
[ -d "$job_dir" ] || continue
|
|
228
|
+
[ -f "$job_dir/exit" ] && continue
|
|
229
|
+
[ -f "$job_dir/task-id" ] || continue
|
|
230
|
+
slug=$(basename "$(dirname "$(dirname "$job_dir")")")
|
|
231
|
+
pid=$(read_file "$job_dir/pid")
|
|
232
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then
|
|
233
|
+
running=$((running + 1))
|
|
234
|
+
elif [ -z "$pid" ]; then
|
|
235
|
+
queued=$((queued + 1))
|
|
236
|
+
else
|
|
237
|
+
continue
|
|
238
|
+
fi
|
|
239
|
+
case " $active_slugs " in
|
|
240
|
+
*" $slug "*) ;;
|
|
241
|
+
*) active_slugs="$active_slugs $slug" ;;
|
|
242
|
+
esac
|
|
243
|
+
done
|
|
244
|
+
local project_count=0
|
|
245
|
+
local counted
|
|
246
|
+
for counted in $active_slugs; do project_count=$((project_count + 1)); done
|
|
247
|
+
echo "jobs_running=$running"
|
|
248
|
+
echo "jobs_queued=$queued"
|
|
249
|
+
echo "jobs_projects=$project_count"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# The probe's slow questions — Pueue, the disk, Xcode and its simulators, Docker — are asked at once, each bounded by probe_step_sec, so
|
|
253
|
+
# a hung one costs that much in all, not that much each: the caller gives the whole probe 20 s, and a Mac with a slow simctl, a stalled
|
|
254
|
+
# mount and a sleeping Docker would otherwise read as offline. One step: output in $probe_dir/<name>.out, its status in <name>.status
|
|
255
|
+
# (137: it was cut off; no file: it never finished).
|
|
256
|
+
start_probe_step() {
|
|
257
|
+
local name=$1
|
|
258
|
+
local limit=$2
|
|
259
|
+
shift 2
|
|
260
|
+
(
|
|
261
|
+
"$@" > "$probe_dir/$name.out" 2> /dev/null < /dev/null &
|
|
262
|
+
local command_pid=$!
|
|
263
|
+
( sleep "$limit"; kill -9 "$command_pid" ) > /dev/null 2>&1 &
|
|
264
|
+
local watchdog_pid=$!
|
|
265
|
+
wait "$command_pid" 2> /dev/null
|
|
266
|
+
echo "$?" > "$probe_dir/$name.status"
|
|
267
|
+
kill "$watchdog_pid" > /dev/null 2>&1
|
|
268
|
+
) &
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
probe_step_status() {
|
|
272
|
+
read_file "$probe_dir/$1.status"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
probe_step_output() {
|
|
276
|
+
read_file "$probe_dir/$1.out"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
probe_slow() {
|
|
280
|
+
mkdir -p "$work/tmp"
|
|
281
|
+
probe_dir="$work/tmp/probe.$$.$RANDOM"
|
|
282
|
+
mkdir -p "$probe_dir" || return 1
|
|
283
|
+
local is_darwin=0
|
|
284
|
+
[ "$(uname -s)" = Darwin ] && is_darwin=1
|
|
285
|
+
local has_docker=0
|
|
286
|
+
command -v docker > /dev/null 2>&1 && has_docker=1
|
|
287
|
+
start_probe_step pueue "$probe_step_sec" "${PUEUE_BIN:-pueue}" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} --version
|
|
288
|
+
start_probe_step disk "$probe_step_sec" df -Pk "$work"
|
|
289
|
+
if [ "$is_darwin" -eq 1 ]; then
|
|
290
|
+
start_probe_step xcode "$probe_step_sec" xcodebuild -version
|
|
291
|
+
start_probe_step simctl "$probe_step_sec" xcrun simctl list devices available
|
|
292
|
+
fi
|
|
293
|
+
[ "$has_docker" -eq 1 ] && start_probe_step docker "$probe_step_sec" docker info
|
|
294
|
+
wait
|
|
295
|
+
|
|
296
|
+
echo "disk_free_mb=$(kb_to_mb "$(probe_step_output disk | awk 'NR == 2 { print $4 }')")"
|
|
297
|
+
if [ "$(probe_step_status pueue)" = 0 ]; then
|
|
298
|
+
echo "pueue=$(probe_step_output pueue | awk '{ print $NF }')"
|
|
299
|
+
else
|
|
300
|
+
echo "pueue=missing"
|
|
301
|
+
fi
|
|
302
|
+
if [ "$is_darwin" -eq 0 ]; then
|
|
303
|
+
echo "cap_ios=no"
|
|
304
|
+
echo "cap_ios_detail=$(uname -s)"
|
|
305
|
+
elif [ "$(probe_step_status xcode)" = 0 ]; then
|
|
306
|
+
local xcode_version
|
|
307
|
+
xcode_version=$(probe_step_output xcode | head -n 1 | sed 's/^Xcode //')
|
|
308
|
+
echo "cap_ios=yes"
|
|
309
|
+
if [ "$(probe_step_status simctl)" = 0 ]; then
|
|
310
|
+
echo "cap_ios_detail=Xcode $xcode_version, $(probe_step_output simctl | grep -c '(Shutdown)\|(Booted)') simulators"
|
|
311
|
+
else
|
|
312
|
+
# A slow simctl is no reason to say there is no Xcode, nor to invent a simulator count.
|
|
313
|
+
echo "cap_ios_detail=Xcode $xcode_version"
|
|
314
|
+
fi
|
|
315
|
+
else
|
|
316
|
+
echo "cap_ios=no"
|
|
317
|
+
if [ "$(probe_step_status xcode)" = 137 ] || [ -z "$(probe_step_status xcode)" ]; then
|
|
318
|
+
echo "cap_ios_detail=Xcode did not answer in time"
|
|
319
|
+
else
|
|
320
|
+
echo "cap_ios_detail=Xcode not found"
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
if [ "$has_docker" -eq 0 ]; then
|
|
324
|
+
echo "cap_docker=no"
|
|
325
|
+
echo "cap_docker_detail=docker not installed"
|
|
326
|
+
elif [ "$(probe_step_status docker)" = 0 ]; then
|
|
327
|
+
echo "cap_docker=yes"
|
|
328
|
+
echo "cap_docker_detail="
|
|
329
|
+
else
|
|
330
|
+
echo "cap_docker=no"
|
|
331
|
+
echo "cap_docker_detail=daemon not reachable"
|
|
332
|
+
fi
|
|
333
|
+
rm -rf "$probe_dir"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
cmd_probe() {
|
|
337
|
+
work=$1
|
|
338
|
+
mkdir -p "$work" 2> /dev/null
|
|
339
|
+
echo "os=$(uname -s)"
|
|
340
|
+
echo "arch=$(uname -m)"
|
|
341
|
+
echo "kernel=$(uname -r)"
|
|
342
|
+
case $(uname -s) in
|
|
343
|
+
Linux) probe_linux ;;
|
|
344
|
+
Darwin) probe_darwin ;;
|
|
345
|
+
esac
|
|
346
|
+
probe_slow
|
|
347
|
+
probe_jobs
|
|
348
|
+
probe_android
|
|
349
|
+
probe_playwright
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# ─────────────────────────── pueue ───────────────────────────
|
|
353
|
+
|
|
354
|
+
# Start the daemon once when it does not answer, with its output away from the ssh channel (it would keep the call open).
|
|
355
|
+
ensure_pueued() {
|
|
356
|
+
if pq status > /dev/null 2>&1; then
|
|
357
|
+
return 0
|
|
358
|
+
fi
|
|
359
|
+
"$(pueued_bin)" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} -d > /dev/null 2>&1 < /dev/null
|
|
360
|
+
local started=$SECONDS
|
|
361
|
+
while [ $((SECONDS - started)) -lt "$pueue_start_wait_sec" ]; do
|
|
362
|
+
pq status > /dev/null 2>&1 && return 0
|
|
363
|
+
sleep 0.2
|
|
364
|
+
done
|
|
365
|
+
return 1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# groups = "name=parallel,name=parallel"
|
|
369
|
+
apply_groups() {
|
|
370
|
+
printf '%s\n' "$1" | tr ',' '\n' | while IFS= read -r pair; do
|
|
371
|
+
name=${pair%%=*}
|
|
372
|
+
parallel=${pair#*=}
|
|
373
|
+
[ -n "$name" ] || continue
|
|
374
|
+
[ "$name" = default ] || pq group add "$name" > /dev/null 2>&1
|
|
375
|
+
pq parallel -g "$name" "$parallel" > /dev/null 2>&1
|
|
376
|
+
done
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# ─────────────────────────── prepare / prune ───────────────────────────
|
|
380
|
+
|
|
381
|
+
cmd_prepare() {
|
|
382
|
+
set_paths "$1" "$2" "$3"
|
|
383
|
+
local groups=${4:-}
|
|
384
|
+
# Ids are 30 random bits, made on the caller's side: a clash would make two jobs share one folder.
|
|
385
|
+
if [ -e "$jd/meta.json" ] || [ -e "$jd/task-id" ]; then
|
|
386
|
+
die "job id already in use: $job"
|
|
387
|
+
fi
|
|
388
|
+
mkdir -p "$work/bin" "$tree" "$proj/stage" "$jobs_dir" "$stage" "$jd" || die "cannot create the layout under $work"
|
|
389
|
+
: > "$jd/log"
|
|
390
|
+
touch "$proj/used"
|
|
391
|
+
if ! ensure_pueued; then
|
|
392
|
+
die "Pueue daemon is not reachable and could not be started (check PUEUE_BIN / the pueue settings of this runner)"
|
|
393
|
+
fi
|
|
394
|
+
apply_groups "$groups"
|
|
395
|
+
prune_work "$work" "$slug"
|
|
396
|
+
echo "work=$(cd "$work" && pwd -P)"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# Has the job in folder $1 ended? It wrote its exit file; or it never got a Pueue task (its submit failed half-way); or its process is
|
|
400
|
+
# gone (a runner script of an earlier version SIGKILLed it without recording that). One that waits in the queue, or runs, has not.
|
|
401
|
+
check_job_dir_ended() {
|
|
402
|
+
[ -f "$1/exit" ] && return 0
|
|
403
|
+
[ -f "$1/task-id" ] || return 0
|
|
404
|
+
local recorded_pid
|
|
405
|
+
recorded_pid=$(read_file "$1/pid")
|
|
406
|
+
[ -n "$recorded_pid" ] && ! kill -0 "$recorded_pid" 2> /dev/null
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Is Pueue's task number $1 the task of job $2? Pueue hands a number it has freed (a task that was removed, or cleaned) to the next task
|
|
410
|
+
# added, and `pueue wait` / `pueue remove` of a number act on whoever holds it — so a job's recorded number is only good while the task
|
|
411
|
+
# under it carries the job's own label. The label filter is Pueue's (`status --json label=…`: an exact match), the answer the task's id.
|
|
412
|
+
check_task_belongs_to_job() {
|
|
413
|
+
pq status --json "label=rr-$2" 2> /dev/null | grep -Eq "\"$1\": ?\{ ?\"id\": ?$1[,}]"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Remove: jobs older than the keep time (and their Pueue tasks), leftover stage dirs, project copies unused for long.
|
|
417
|
+
prune_work() {
|
|
418
|
+
local work_dir=$1
|
|
419
|
+
local keep_slug=${2:-}
|
|
420
|
+
[ -d "$work_dir/p" ] || return 0
|
|
421
|
+
find "$work_dir/p" -mindepth 3 -maxdepth 3 -path '*/jobs/*' -type d -mtime +"$job_keep_days" 2> /dev/null | while IFS= read -r old_job; do
|
|
422
|
+
check_job_dir_ended "$old_job" || continue
|
|
423
|
+
task=$(read_file "$old_job/task-id")
|
|
424
|
+
if [ -n "$task" ] && [ ! -f "$old_job/task-removed" ] && check_task_belongs_to_job "$task" "$(basename "$old_job")"; then
|
|
425
|
+
pq remove "$task" > /dev/null 2>&1
|
|
426
|
+
fi
|
|
427
|
+
rm -rf "$old_job"
|
|
428
|
+
done
|
|
429
|
+
find "$work_dir/p" -mindepth 3 -maxdepth 3 -path '*/stage/*' -type d -mtime +"$stage_keep_days" 2> /dev/null | while IFS= read -r old_stage; do
|
|
430
|
+
# A job that is queued in Pueue (a task, no exit yet) has not applied its stage: behind a busy group it can wait for days.
|
|
431
|
+
check_job_dir_ended "$(dirname "$(dirname "$old_stage")")/jobs/$(basename "$old_stage")" || continue
|
|
432
|
+
rm -rf "$old_stage"
|
|
433
|
+
done
|
|
434
|
+
# Lease state of a watcher that was killed hard (a live one rewrites its beat every few seconds, so its folder is never this old).
|
|
435
|
+
find "$work_dir/tmp" -mindepth 1 -maxdepth 1 -mtime +"$stage_keep_days" 2> /dev/null | while IFS= read -r old_lease; do
|
|
436
|
+
rm -rf "$old_lease"
|
|
437
|
+
done
|
|
438
|
+
find "$work_dir/p" -mindepth 2 -maxdepth 2 -name used -type f -mtime +"$copy_keep_days" 2> /dev/null | while IFS= read -r used_file; do
|
|
439
|
+
old_copy=$(dirname "$used_file")
|
|
440
|
+
[ "$(basename "$old_copy")" = "$keep_slug" ] && continue
|
|
441
|
+
[ -e "$old_copy/lock" ] && continue
|
|
442
|
+
rm -rf "$old_copy"
|
|
443
|
+
done
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
cmd_prune() {
|
|
447
|
+
prune_work "$1" ""
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# ─────────────────────────── adding the job to Pueue ───────────────────────────
|
|
451
|
+
|
|
452
|
+
# Quote one word for a shell (single quotes).
|
|
453
|
+
quote_word() {
|
|
454
|
+
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
add_job() {
|
|
458
|
+
local group=$1
|
|
459
|
+
local script_path
|
|
460
|
+
script_path=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
|
|
461
|
+
local command_line
|
|
462
|
+
command_line="exec bash $(quote_word "$script_path") job $(quote_word "$work") $(quote_word "$slug") $(quote_word "$job")"
|
|
463
|
+
local task
|
|
464
|
+
# The job's time limit counts from here (a foreground run's own timer does too): a job that waited its whole limit in the queue is
|
|
465
|
+
# over when it starts (cmd_job), not given the limit again.
|
|
466
|
+
date +%s > "$jd/queued-at"
|
|
467
|
+
task=$(pq add --print-task-id -g "$group" -l "rr-$job" -w "$proj" -- "$command_line") || return 1
|
|
468
|
+
printf '%s\n' "$task" > "$jd/task-id"
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
cmd_submit() {
|
|
472
|
+
set_paths "$1" "$2" "$3"
|
|
473
|
+
add_job "${4:-default}" || die "pueue add failed (does the group \"${4:-default}\" exist on this runner?)"
|
|
474
|
+
echo queued
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# ─────────────────────────── killing ───────────────────────────
|
|
478
|
+
|
|
479
|
+
# Signal the whole process group of the job (the script, its command and everything the command started). Pueue runs a task as
|
|
480
|
+
# `sh -c <command>` in a process group of its own, and the command line of add_job starts with `exec`, so the job script IS that
|
|
481
|
+
# group's leader: a group-wide signal has no wrapper to end first (which made Pueue call the task done while the job still ran).
|
|
482
|
+
# The group is read from the LIVE job process at the moment of use and signalled only while the job leads its own group: a stale id,
|
|
483
|
+
# or a Pueue that stopped isolating its tasks, can never make this hit another group (pueued's, the caller's).
|
|
484
|
+
signal_job_group() {
|
|
485
|
+
local job_pid group_id
|
|
486
|
+
job_pid=$(read_file "$jd/pid")
|
|
487
|
+
[ -n "$job_pid" ] && kill -0 "$job_pid" 2> /dev/null || return 1
|
|
488
|
+
group_id=$(ps -o pgid= -p "$job_pid" 2> /dev/null | tr -d ' ')
|
|
489
|
+
[ -n "$group_id" ] && [ "$group_id" = "$job_pid" ] || return 1
|
|
490
|
+
kill "-$1" -- "-$group_id" 2> /dev/null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Stop a job: a queued one is removed, a running one gets SIGTERM (its whole group) and, after the grace time, Pueue's own kill of the
|
|
494
|
+
# whole task tree. $1 says why: `timeout` when the caller's own time limit ran out (recorded like the job's own watchdog does it — 124,
|
|
495
|
+
# "timed out" — whichever of the two gets there first), anything else is a kill (137).
|
|
496
|
+
kill_job() {
|
|
497
|
+
local task
|
|
498
|
+
task=$(read_file "$jd/task-id")
|
|
499
|
+
[ -n "$task" ] || return 1
|
|
500
|
+
[ -f "$jd/exit" ] && return 0
|
|
501
|
+
if [ "${1:-}" = timeout ]; then
|
|
502
|
+
touch "$jd/timeout"
|
|
503
|
+
else
|
|
504
|
+
touch "$jd/killed"
|
|
505
|
+
fi
|
|
506
|
+
if [ ! -f "$jd/pid" ] && pq remove "$task" > /dev/null 2>&1; then
|
|
507
|
+
# Pueue gives a number it has freed to the next task it is asked to add: from here on `task-id` is a number that may belong to another
|
|
508
|
+
# job, so nothing may act on it any more (`prune_work` skips such a job's task; waiting is decided by the exit file below).
|
|
509
|
+
touch "$jd/task-removed"
|
|
510
|
+
finish_killed_job
|
|
511
|
+
return 0
|
|
512
|
+
fi
|
|
513
|
+
signal_job_group TERM || pq kill "$task" > /dev/null 2>&1
|
|
514
|
+
local started=$SECONDS
|
|
515
|
+
while [ $((SECONDS - started)) -lt "$kill_grace_sec" ]; do
|
|
516
|
+
check_job_ended && return 0
|
|
517
|
+
sleep 0.2
|
|
518
|
+
done
|
|
519
|
+
hard_kill_job "$task"
|
|
520
|
+
sleep 1
|
|
521
|
+
return 0
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# SIGKILL of the whole group; Pueue's own kill follows only so that its task list agrees. Without a verifiable group, Pueue's kill
|
|
525
|
+
# is all there is. A job that is SIGKILLed cannot write its own result, so the result is recorded FIRST: when the caller is the job's own
|
|
526
|
+
# watchdog the group-wide SIGKILL takes the watchdog down with the job.
|
|
527
|
+
hard_kill_job() {
|
|
528
|
+
finish_killed_job
|
|
529
|
+
signal_job_group KILL || pq kill "$1" > /dev/null 2>&1
|
|
530
|
+
pq kill "$1" > /dev/null 2>&1
|
|
531
|
+
return 0
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# A job that was killed (never started and removed from the queue, or SIGKILLed after it ignored SIGTERM): its result is written the way a
|
|
535
|
+
# finished job writes it — 124 when its time limit did it, 137 otherwise — so it is an ENDED job: `wait` and `ls` read a code, the next job
|
|
536
|
+
# of the project takes over its lock at once, and prune removes it after 3 days like any other.
|
|
537
|
+
finish_killed_job() {
|
|
538
|
+
[ -f "$jd/exit" ] && return 0
|
|
539
|
+
local code=137
|
|
540
|
+
[ -f "$jd/timeout" ] && code=124
|
|
541
|
+
rm -rf "$stage"
|
|
542
|
+
printf '%s\n' done > "$jd/phase"
|
|
543
|
+
date +%s > "$jd/finished-at"
|
|
544
|
+
printf '%s\n' "$code" > "$jd/exit.tmp" && mv "$jd/exit.tmp" "$jd/exit"
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
# Ended = it wrote its exit file, or its process is gone.
|
|
548
|
+
check_job_ended() {
|
|
549
|
+
[ -f "$jd/exit" ] && return 0
|
|
550
|
+
local pid
|
|
551
|
+
pid=$(read_file "$jd/pid")
|
|
552
|
+
[ -n "$pid" ] && ! kill -0 "$pid" 2> /dev/null && return 0
|
|
553
|
+
return 1
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# Prints `killed`, or `already-ended <code>` for a job that had finished before the kill came (nothing is killed then, and the caller
|
|
557
|
+
# must not be told it was).
|
|
558
|
+
cmd_kill() {
|
|
559
|
+
set_paths "$1" "$2" "$3"
|
|
560
|
+
if [ -n "$(read_file "$jd/task-id")" ] && [ -f "$jd/exit" ]; then
|
|
561
|
+
echo "already-ended $(read_file "$jd/exit")"
|
|
562
|
+
return 0
|
|
563
|
+
fi
|
|
564
|
+
kill_job "${4:-}" || die "no such job: $job"
|
|
565
|
+
echo killed
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# ─────────────────────────── log streaming ───────────────────────────
|
|
569
|
+
|
|
570
|
+
# Print what the log gained since $log_offset, and move the offset.
|
|
571
|
+
drain_log() {
|
|
572
|
+
local size
|
|
573
|
+
size=$(wc -c < "$jd/log" 2> /dev/null | tr -d ' ')
|
|
574
|
+
[ -n "$size" ] || return 0
|
|
575
|
+
if [ "$size" -gt "$log_offset" ]; then
|
|
576
|
+
tail -c +"$((log_offset + 1))" "$jd/log" | head -c "$((size - log_offset))"
|
|
577
|
+
log_offset=$size
|
|
578
|
+
fi
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Stream the log until the file $jd/drain-stop appears, then print the rest. Runs as a background job.
|
|
582
|
+
stream_log() {
|
|
583
|
+
log_offset=${1:-0}
|
|
584
|
+
while [ ! -f "$jd/drain-stop" ]; do
|
|
585
|
+
drain_log
|
|
586
|
+
sleep "$drain_interval_sec"
|
|
587
|
+
done
|
|
588
|
+
drain_log
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
# Return when the job is over: Pueue says its task is done, or the job's exit file is there — whichever comes first, and at once if it
|
|
592
|
+
# already is. The exit file is the job's own record. The number of a task that was removed (a queued job that was killed) can be given to
|
|
593
|
+
# another job, and `pueue wait` on it would then follow THAT job for as long as it runs (hours, behind a busy group), or return at once
|
|
594
|
+
# for a number nobody holds: so the wait is cut as soon as the exit file appears, and the number is never trusted alone.
|
|
595
|
+
#
|
|
596
|
+
# The Pueue wait runs in a process group of its own (`set -m`, as `exec` does) and the cut signals the GROUP. `pq` is a function, so the
|
|
597
|
+
# background job is a subshell and `pueue wait` its child — unless the bash happens to run it in place of the subshell (5.3 can; 5.2, the
|
|
598
|
+
# bash of Ubuntu 24.04, does not). A kill of the subshell alone left `pueue wait` running for as long as the task, however many times
|
|
599
|
+
# the exit file won the race. Under job control bash no longer gives a background job /dev/null as stdin, so it is given here: in a
|
|
600
|
+
# lease the caller's stdin is the heartbeat stream, which only the lease watcher (fd 3) may read.
|
|
601
|
+
wait_for_task() {
|
|
602
|
+
[ -f "$jd/exit" ] && return 0
|
|
603
|
+
set -m
|
|
604
|
+
pq wait "$1" < /dev/null > /dev/null 2>&1 &
|
|
605
|
+
local waiter_pid=$!
|
|
606
|
+
set +m
|
|
607
|
+
while kill -0 "$waiter_pid" 2> /dev/null; do
|
|
608
|
+
if [ -f "$jd/exit" ]; then
|
|
609
|
+
# The group, or the process alone when it did not get a group of its own: a group id that is not a live group is harmless.
|
|
610
|
+
kill -- "-$waiter_pid" > /dev/null 2>&1 || kill "$waiter_pid" > /dev/null 2>&1
|
|
611
|
+
break
|
|
612
|
+
fi
|
|
613
|
+
sleep 0.2
|
|
614
|
+
done
|
|
615
|
+
{ wait "$waiter_pid"; } 2> /dev/null
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# Stream the log of the task until Pueue reports it done.
|
|
620
|
+
follow_until_done() {
|
|
621
|
+
rm -f "$jd/drain-stop"
|
|
622
|
+
stream_log "$1" &
|
|
623
|
+
local streamer_pid=$!
|
|
624
|
+
local task
|
|
625
|
+
task=$(read_file "$jd/task-id")
|
|
626
|
+
[ -n "$task" ] && wait_for_task "$task"
|
|
627
|
+
touch "$jd/drain-stop"
|
|
628
|
+
wait "$streamer_pid" 2> /dev/null
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
# Where `follow` starts in the log: the beginning, the end, or the last N lines.
|
|
632
|
+
resolve_follow_offset() {
|
|
633
|
+
local size
|
|
634
|
+
size=$(wc -c < "$jd/log" 2> /dev/null | tr -d ' ')
|
|
635
|
+
size=${size:-0}
|
|
636
|
+
case ${1:-start} in
|
|
637
|
+
end) echo "$size" ;;
|
|
638
|
+
tail:*)
|
|
639
|
+
local tail_bytes
|
|
640
|
+
tail_bytes=$(tail -n "${1#tail:}" "$jd/log" 2> /dev/null | wc -c | tr -d ' ')
|
|
641
|
+
echo $((size - tail_bytes))
|
|
642
|
+
;;
|
|
643
|
+
*) echo 0 ;;
|
|
644
|
+
esac
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
cmd_follow() {
|
|
648
|
+
set_paths "$1" "$2" "$3"
|
|
649
|
+
local from_offset
|
|
650
|
+
from_offset=$(resolve_follow_offset "${4:-start}")
|
|
651
|
+
follow_until_done "$from_offset"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# After Pueue says a task is done, its own last file may still be a moment away: wait for the exit file, or for the job process to
|
|
655
|
+
# be gone (killed with SIGKILL, it never writes one), at most five seconds.
|
|
656
|
+
wait_for_exit_file() {
|
|
657
|
+
local started=$SECONDS
|
|
658
|
+
while [ ! -f "$jd/exit" ] && [ $((SECONDS - started)) -lt "$exit_file_wait_sec" ]; do
|
|
659
|
+
check_job_ended && return 0
|
|
660
|
+
sleep 0.2
|
|
661
|
+
done
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
cmd_wait() {
|
|
665
|
+
set_paths "$1" "$2" "$3"
|
|
666
|
+
local task
|
|
667
|
+
task=$(read_file "$jd/task-id")
|
|
668
|
+
[ -n "$task" ] || die "no such job: $job"
|
|
669
|
+
wait_for_task "$task"
|
|
670
|
+
wait_for_exit_file
|
|
671
|
+
echo done
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# The facts of a job, one key=value per line (parsed by jobState.ts).
|
|
675
|
+
print_job_facts() {
|
|
676
|
+
echo "slug=$slug"
|
|
677
|
+
echo "exit=$(read_file "$jd/exit")"
|
|
678
|
+
echo "phase=$(read_file "$jd/phase")"
|
|
679
|
+
echo "task=$(read_file "$jd/task-id")"
|
|
680
|
+
echo "started=$(read_file "$jd/started-at")"
|
|
681
|
+
echo "finished=$(read_file "$jd/finished-at")"
|
|
682
|
+
[ -f "$jd/timeout" ] && echo "timeout=1" || echo "timeout=0"
|
|
683
|
+
[ -f "$jd/killed" ] && echo "killed=1" || echo "killed=0"
|
|
684
|
+
[ -f "$jd/pid" ] && echo "has_pid=1" || echo "has_pid=0"
|
|
685
|
+
[ -d "$jd/artifacts" ] && echo "artifacts=1" || echo "artifacts=0"
|
|
686
|
+
echo "failed_step=$(read_file "$jd/failed-step")"
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
cmd_result() {
|
|
690
|
+
set_paths "$1" "$2" "$3"
|
|
691
|
+
[ -d "$jd" ] || die "no such job: $job"
|
|
692
|
+
print_job_facts
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
cmd_meta() {
|
|
696
|
+
set_paths "$1" "$2" "$3"
|
|
697
|
+
cat "$jd/meta.json" 2> /dev/null || die "no such job: $job"
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
# The last N lines of a job's log (all of it when N is "all").
|
|
701
|
+
cmd_tail() {
|
|
702
|
+
set_paths "$1" "$2" "$3"
|
|
703
|
+
if [ "${4:-all}" = all ]; then cat "$jd/log" 2> /dev/null; else tail -n "$4" "$jd/log" 2> /dev/null; fi
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# ─────────────────────────── the lease ───────────────────────────
|
|
707
|
+
|
|
708
|
+
# The caller writes one byte every few seconds. No byte for the lease time, or the end of its stream, means the caller is gone: the job
|
|
709
|
+
# is killed. A caller that finishes normally has no use for the lease any more because the job it guarded is over (the lease script
|
|
710
|
+
# stops its watcher when the job ends), and one that wants the job stopped says so with `kill` (an interrupted `exec` ends its stream on purpose, to have its command stopped).
|
|
711
|
+
# A stock sshd never notices a half-open connection, so this is the only thing that stops an orphaned job. The stream is the caller's,
|
|
712
|
+
# on fd 3 (a background job gets /dev/null as stdin, so the callers keep theirs there). A reader process does plain blocking reads and
|
|
713
|
+
# records each byte's arrival time; this loop compares the clock with it once a second — no `read -t`, whose exit status for a timeout
|
|
714
|
+
# is not the same in bash 3.2 (macOS) and bash 4+, and which would make a quiet second look like a closed connection.
|
|
715
|
+
watch_lease() {
|
|
716
|
+
local lease_sec=$1
|
|
717
|
+
lease_state_dir=
|
|
718
|
+
lease_reader_pid=
|
|
719
|
+
# The traps first: a command that ends at once gets the watcher a SIGTERM before it has done anything, and whatever it created by
|
|
720
|
+
# then must go with it. The name is made here, not by a command substitution, so there is no instant with a dir nobody knows of.
|
|
721
|
+
trap 'end_lease_watch' EXIT
|
|
722
|
+
trap 'exit 143' TERM
|
|
723
|
+
lease_state_dir="$work/tmp/lease.$$.$RANDOM"
|
|
724
|
+
mkdir -p "$lease_state_dir" || return 1
|
|
725
|
+
write_lease_beat
|
|
726
|
+
(
|
|
727
|
+
byte=
|
|
728
|
+
while IFS= read -r -n 1 byte; do
|
|
729
|
+
write_lease_beat
|
|
730
|
+
done
|
|
731
|
+
: > "$lease_state_dir/eof"
|
|
732
|
+
) <&3 &
|
|
733
|
+
lease_reader_pid=$!
|
|
734
|
+
local verdict=0
|
|
735
|
+
local last_beat
|
|
736
|
+
while :; do
|
|
737
|
+
if [ -f "$lease_state_dir/eof" ]; then
|
|
738
|
+
verdict=1
|
|
739
|
+
break
|
|
740
|
+
fi
|
|
741
|
+
last_beat=$(cat "$lease_state_dir/beat" 2> /dev/null)
|
|
742
|
+
case $last_beat in
|
|
743
|
+
'' | *[!0-9]*) ;;
|
|
744
|
+
*) [ $(($(date +%s) - last_beat)) -ge "$lease_sec" ] && { verdict=1; break; } ;;
|
|
745
|
+
esac
|
|
746
|
+
sleep "$heartbeat_poll_sec"
|
|
747
|
+
done
|
|
748
|
+
end_lease_watch
|
|
749
|
+
return "$verdict"
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
# The time of the latest heartbeat, replaced atomically: a plain `> beat` truncates the file first, and a watcher that read it in
|
|
753
|
+
# that instant got an empty string, whose arithmetic is a syntax error that ended the watcher without a verdict.
|
|
754
|
+
write_lease_beat() {
|
|
755
|
+
date +%s > "$lease_state_dir/beat.new" && mv -f "$lease_state_dir/beat.new" "$lease_state_dir/beat"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
end_lease_watch() {
|
|
759
|
+
[ -n "${lease_reader_pid:-}" ] && kill "$lease_reader_pid" > /dev/null 2>&1
|
|
760
|
+
[ -n "${lease_state_dir:-}" ] && rm -rf "$lease_state_dir"
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
ms_to_sec() {
|
|
764
|
+
awk -v ms="$1" 'BEGIN { s = int((ms + 999) / 1000); if (s < 2) s = 2; print s }'
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
cmd_lease() {
|
|
768
|
+
set_paths "$1" "$2" "$3"
|
|
769
|
+
local group=$4
|
|
770
|
+
local lease_sec
|
|
771
|
+
lease_sec=$(ms_to_sec "$5")
|
|
772
|
+
# A background job gets /dev/null as stdin unless job control is on: keep the caller's stream on fd 3 for the watcher.
|
|
773
|
+
exec 3<&0
|
|
774
|
+
add_job "$group" || die "pueue add failed (does the group \"$group\" exist on this runner?)"
|
|
775
|
+
rm -f "$jd/drain-stop"
|
|
776
|
+
stream_log 0 &
|
|
777
|
+
local streamer_pid=$!
|
|
778
|
+
(
|
|
779
|
+
watch_lease "$lease_sec" || kill_job
|
|
780
|
+
) &
|
|
781
|
+
local watcher_pid=$!
|
|
782
|
+
local task
|
|
783
|
+
task=$(read_file "$jd/task-id")
|
|
784
|
+
wait_for_task "$task"
|
|
785
|
+
kill "$watcher_pid" > /dev/null 2>&1
|
|
786
|
+
wait_for_exit_file
|
|
787
|
+
touch "$jd/drain-stop"
|
|
788
|
+
wait "$streamer_pid" 2> /dev/null
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
# ─────────────────────────── the job (the Pueue task) ───────────────────────────
|
|
792
|
+
|
|
793
|
+
lock_held=0
|
|
794
|
+
|
|
795
|
+
# The project lock is a FILE made with the shell's own exclusive create (`set -C`, noclobber: open(O_EXCL)) — never with `mkdir`. The uutils
|
|
796
|
+
# coreutils that current Ubuntu ships as /usr/bin/mkdir let several simultaneous `mkdir` of one path ALL succeed (measured here: 265 races
|
|
797
|
+
# of 1200 with five contenders), which gave two jobs of one project the lock together. The file holds the holder's pid and job, two lines,
|
|
798
|
+
# written by the very redirection that creates it. Succeeds only for the one that created the file.
|
|
799
|
+
create_lock_file() {
|
|
800
|
+
( set -C; printf '%s\n%s\n' "$$" "$job" > "$1" ) 2> /dev/null
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
# Read the lock file $1 into holder_pid and holder_job (empty when it is missing or has no holder written yet).
|
|
804
|
+
read_lock_holder() {
|
|
805
|
+
holder_pid=
|
|
806
|
+
holder_job=
|
|
807
|
+
{ IFS= read -r holder_pid; IFS= read -r holder_job; } 2> /dev/null < "$1"
|
|
808
|
+
return 0
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# Is the lock held by nobody any more? Its holder's process is gone, or its job has an exit record, or no holder was ever written and it
|
|
812
|
+
# is over a minute old (its creator died between creating the file and writing it).
|
|
813
|
+
check_lock_is_stale() {
|
|
814
|
+
local holder_pid holder_job
|
|
815
|
+
read_lock_holder "$lock"
|
|
816
|
+
if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2> /dev/null; then
|
|
817
|
+
return 0
|
|
818
|
+
fi
|
|
819
|
+
if [ -n "$holder_job" ] && [ -f "$jobs_dir/$holder_job/exit" ]; then
|
|
820
|
+
return 0
|
|
821
|
+
fi
|
|
822
|
+
[ -z "$holder_pid" ] && [ -n "$(find "$lock" -maxdepth 0 -mmin +1 2> /dev/null)" ]
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
# Break a stale lock — by ONE waiter at a time, and only the lock of the very holder that was seen dead. Several waiters can see the same
|
|
826
|
+
# dead holder at once; if each simply removed the lock, a slow one could remove the lock a faster one had already re-created, and two jobs
|
|
827
|
+
# of a project would run on one tree. So the RIGHT to break the lock of holder (pid, job) is a file named for that holder, made with the
|
|
828
|
+
# exclusive create: of the waiters that saw it, one wins and the others yield. That holder can never hold the lock again (its process is
|
|
829
|
+
# gone, its job id is unique), so once the winner has seen the lock still carries that holder, nothing but the winner can remove it, and a
|
|
830
|
+
# lock some other job has taken since is not that holder's and is left alone. Returns 1 when another waiter is at it.
|
|
831
|
+
break_stale_lock() {
|
|
832
|
+
local holder_pid holder_job
|
|
833
|
+
read_lock_holder "$lock"
|
|
834
|
+
check_lock_is_stale || return 0
|
|
835
|
+
break_lock_of_holder "$holder_pid" "$holder_job"
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
# The right to break the lock of holder $1/$2 (pid, job): the file `$lock.breaking.<pid>.<job>.<generation>`. A winner that died leaves its
|
|
839
|
+
# file behind, held by a process that is gone: the next waiter takes the next generation of the name — and again only one of them wins it.
|
|
840
|
+
# A name is made of the holder's own words only (a lock file with something else in it must not name a path).
|
|
841
|
+
break_lock_of_holder() {
|
|
842
|
+
local stale_pid=$1 stale_job=$2
|
|
843
|
+
case $stale_pid in *[!0-9]*) stale_pid= ;; esac
|
|
844
|
+
case $stale_job in *[!A-Za-z0-9._-]*) stale_job= ;; esac
|
|
845
|
+
local generation=0 right
|
|
846
|
+
while :; do
|
|
847
|
+
right="$lock.breaking.${stale_pid:-none}.${stale_job:-none}.$generation"
|
|
848
|
+
create_lock_file "$right" && break
|
|
849
|
+
check_breaker_is_alive "$right" && return 1
|
|
850
|
+
generation=$((generation + 1))
|
|
851
|
+
done
|
|
852
|
+
# The lock must carry that holder AND still be stale. For a named holder the second test adds nothing (it is dead for good). For a lock
|
|
853
|
+
# with no holder written it is what matters: every fresh lock is empty for a moment between its creation and the write of its pid, so
|
|
854
|
+
# "no holder" names no one in particular — only the over-a-minute test tells a creator that died from one that is about to write.
|
|
855
|
+
local holder_pid holder_job
|
|
856
|
+
read_lock_holder "$lock"
|
|
857
|
+
if [ "$holder_pid" = "$1" ] && [ "$holder_job" = "$2" ] && check_lock_is_stale; then
|
|
858
|
+
rm -f "$lock"
|
|
859
|
+
fi
|
|
860
|
+
return 0
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# Is the breaker that made the right file $1 still at it? Its process is alive — the test the lock itself uses, so a breaker that is merely
|
|
864
|
+
# slow (a busy disk) keeps its right however long it takes — and its job has no exit record (the lock's second test: a breaker that died
|
|
865
|
+
# and whose pid went to some long-lived process would otherwise hold its right for ever, and the project's queue would never drain). A file
|
|
866
|
+
# with no process written yet is a breaker between creating it and writing it: it counts until it is over a minute old.
|
|
867
|
+
check_breaker_is_alive() {
|
|
868
|
+
local holder_pid holder_job
|
|
869
|
+
read_lock_holder "$1"
|
|
870
|
+
if [ -n "$holder_job" ] && [ -f "$jobs_dir/$holder_job/exit" ]; then
|
|
871
|
+
return 1
|
|
872
|
+
fi
|
|
873
|
+
if [ -n "$holder_pid" ]; then
|
|
874
|
+
kill -0 "$holder_pid" 2> /dev/null
|
|
875
|
+
return
|
|
876
|
+
fi
|
|
877
|
+
[ -z "$(find "$1" -maxdepth 0 -mmin +1 2> /dev/null)" ]
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
acquire_lock() {
|
|
881
|
+
local noticed=0
|
|
882
|
+
local holder_pid holder_job
|
|
883
|
+
while ! create_lock_file "$lock"; do
|
|
884
|
+
[ "$killed" -eq 1 ] && return 1
|
|
885
|
+
if check_lock_is_stale && break_stale_lock; then
|
|
886
|
+
continue
|
|
887
|
+
fi
|
|
888
|
+
if [ "$noticed" -eq 0 ]; then
|
|
889
|
+
read_lock_holder "$lock"
|
|
890
|
+
say "waiting for job ${holder_job:-another job} of this project"
|
|
891
|
+
noticed=1
|
|
892
|
+
fi
|
|
893
|
+
nap "$lock_poll_sec"
|
|
894
|
+
done
|
|
895
|
+
lock_held=1
|
|
896
|
+
# The rights to break the lock name holders that are gone and can never hold it again: nothing needs them any more.
|
|
897
|
+
rm -f "$lock".breaking.* 2> /dev/null
|
|
898
|
+
return 0
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
# Give the lock back — only if it is still ours (a lock that was broken as stale and taken by another job is not ours to remove).
|
|
902
|
+
release_lock() {
|
|
903
|
+
if [ "$lock_held" -eq 1 ]; then
|
|
904
|
+
local holder_pid holder_job
|
|
905
|
+
read_lock_holder "$lock"
|
|
906
|
+
if [ "$holder_pid" = "$$" ] && [ "$holder_job" = "$job" ]; then
|
|
907
|
+
rm -f "$lock"
|
|
908
|
+
fi
|
|
909
|
+
fi
|
|
910
|
+
lock_held=0
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
remove_tree_file() {
|
|
914
|
+
check_relative_path "$1" || return 0
|
|
915
|
+
rm -f "$tree/$1"
|
|
916
|
+
local dir
|
|
917
|
+
dir=$(dirname "$1")
|
|
918
|
+
while [ "$dir" != "." ] && rmdir "$tree/$dir" 2> /dev/null; do
|
|
919
|
+
dir=$(dirname "$dir")
|
|
920
|
+
done
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
# The apply step: delete what the previous submit had and this one has not, copy the staged files over, keep this submit's list.
|
|
924
|
+
sync_tree() {
|
|
925
|
+
mkdir -p "$tree"
|
|
926
|
+
if [ -f "$proj/manifest" ]; then
|
|
927
|
+
sort "$proj/manifest" > "$jd/manifest.old.sorted"
|
|
928
|
+
sort "$jd/manifest" > "$jd/manifest.new.sorted"
|
|
929
|
+
comm -23 "$jd/manifest.old.sorted" "$jd/manifest.new.sorted" | while IFS= read -r removed_file; do
|
|
930
|
+
remove_tree_file "$removed_file"
|
|
931
|
+
done
|
|
932
|
+
fi
|
|
933
|
+
# --modify-window=-1: compare the mtimes to the nanosecond too — a same-length edit within one second of the previous version is
|
|
934
|
+
# otherwise "unchanged" (rsync's quick check) and the copy would keep the old file.
|
|
935
|
+
rsync -a --modify-window=-1 --exclude=/.git "$stage/" "$tree/" || return 1
|
|
936
|
+
if [ -d "$stage/.git" ]; then
|
|
937
|
+
mkdir -p "$tree/.git"
|
|
938
|
+
rsync -a --modify-window=-1 --delete "$stage/.git/" "$tree/.git/" || return 1
|
|
939
|
+
else
|
|
940
|
+
# `.git` only with --with-git: a copy that got one from an earlier run must not keep serving its stale history.
|
|
941
|
+
rm -rf "$tree/.git"
|
|
942
|
+
fi
|
|
943
|
+
cp "$jd/manifest" "$proj/manifest"
|
|
944
|
+
rm -rf "$stage"
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
cmd_sync_tree() {
|
|
948
|
+
set_paths "$1" "$2" "$3"
|
|
949
|
+
sync_tree || die "apply failed"
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
# bash -c <command> in <directory>, with the job's -e variables, output into the log; sets step_code. Runs as a child so a
|
|
953
|
+
# SIGTERM to the job (the `killed` flag) does not abandon it half-way.
|
|
954
|
+
run_step() {
|
|
955
|
+
local directory=$1
|
|
956
|
+
local command_text=$2
|
|
957
|
+
(
|
|
958
|
+
cd "$directory" || exit 126
|
|
959
|
+
if [ -s "$jd/env" ]; then
|
|
960
|
+
while IFS= read -r env_line; do export "$env_line"; done < "$jd/env"
|
|
961
|
+
fi
|
|
962
|
+
exec bash -c "$command_text"
|
|
963
|
+
) < /dev/null &
|
|
964
|
+
local step_pid=$!
|
|
965
|
+
# The notices bash prints when it reaps a signalled child ("Terminated") are not the job's output.
|
|
966
|
+
{ wait "$step_pid"; } 2> /dev/null
|
|
967
|
+
step_code=$?
|
|
968
|
+
while kill -0 "$step_pid" 2> /dev/null; do
|
|
969
|
+
{ wait "$step_pid"; } 2> /dev/null
|
|
970
|
+
step_code=$?
|
|
971
|
+
done
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
# A sleep that a group-wide SIGTERM does not end (and bash does not report as "Terminated").
|
|
975
|
+
nap() {
|
|
976
|
+
( trap '' TERM; exec sleep "$1" )
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
# The timeout: from the job's start, covering the lock wait, the install and the command. SIGTERM to the job's group, SIGKILL after the
|
|
980
|
+
# grace time (a process that ignores SIGTERM). It polls once a second for the cancel file instead of sleeping the whole limit: a
|
|
981
|
+
# sleeper that a group-wide SIGTERM (a kill) ends would look like an expired limit, and a finished job needs no process to reap.
|
|
982
|
+
start_watchdog() {
|
|
983
|
+
local limit=$1
|
|
984
|
+
local task
|
|
985
|
+
task=$(read_file "$jd/task-id")
|
|
986
|
+
(
|
|
987
|
+
trap '' TERM
|
|
988
|
+
local started=$SECONDS
|
|
989
|
+
while [ $((SECONDS - started)) -lt "$limit" ]; do
|
|
990
|
+
[ -f "$jd/watchdog-cancel" ] && exit 0
|
|
991
|
+
nap 1
|
|
992
|
+
done
|
|
993
|
+
[ -f "$jd/watchdog-cancel" ] && exit 0
|
|
994
|
+
touch "$jd/timeout"
|
|
995
|
+
signal_job_group TERM || pq kill "$task" > /dev/null 2>&1
|
|
996
|
+
local grace_started=$SECONDS
|
|
997
|
+
while [ $((SECONDS - grace_started)) -lt "$kill_grace_sec" ]; do
|
|
998
|
+
[ -f "$jd/watchdog-cancel" ] && exit 0
|
|
999
|
+
nap 1
|
|
1000
|
+
done
|
|
1001
|
+
hard_kill_job "$task"
|
|
1002
|
+
) > /dev/null 2>&1 &
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
stop_watchdog() {
|
|
1006
|
+
touch "$jd/watchdog-cancel"
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
# True when "$tree/$1" has no symlink in it, so a walk that starts there stays inside the copy (the walk of the whole copy never followed a
|
|
1010
|
+
# link either).
|
|
1011
|
+
check_has_no_symlink() {
|
|
1012
|
+
local parent physical
|
|
1013
|
+
[ ! -L "$tree/$1" ] || return 1
|
|
1014
|
+
parent=$(dirname "$1")
|
|
1015
|
+
[ "$parent" = . ] && return 0
|
|
1016
|
+
physical=$(cd "$tree/$parent" 2> /dev/null && pwd -P) || return 1
|
|
1017
|
+
[ "$physical" = "$(cd "$tree" && pwd -P)/$parent" ]
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
# Files the command created or changed that match pull.ere (one extended regular expression per line), kept per job. Only the folders in
|
|
1021
|
+
# pull.roots are walked (the fixed prefixes of the patterns, `e2e/artifacts`): the copy also holds node_modules and every build folder,
|
|
1022
|
+
# which a Gradle or Metro build fills with hundreds of thousands of files. `.` there (a pattern that starts with a wildcard) or no
|
|
1023
|
+
# pull.roots at all (an older main box) walks the whole copy.
|
|
1024
|
+
collect_artifacts() {
|
|
1025
|
+
[ -s "$jd/pull.ere" ] || return 0
|
|
1026
|
+
[ -f "$jd/start-marker" ] || return 0
|
|
1027
|
+
local roots=()
|
|
1028
|
+
local root
|
|
1029
|
+
if [ -s "$jd/pull.roots" ]; then
|
|
1030
|
+
while IFS= read -r root; do
|
|
1031
|
+
[ -n "$root" ] || continue
|
|
1032
|
+
if [ "$root" = . ]; then
|
|
1033
|
+
roots=(.)
|
|
1034
|
+
break
|
|
1035
|
+
fi
|
|
1036
|
+
check_relative_path "$root" && [ -e "$tree/$root" ] && check_has_no_symlink "$root" && roots+=("$root")
|
|
1037
|
+
done < "$jd/pull.roots"
|
|
1038
|
+
[ "${#roots[@]}" -gt 0 ] || return 0
|
|
1039
|
+
else
|
|
1040
|
+
roots=(.)
|
|
1041
|
+
fi
|
|
1042
|
+
local patterns=()
|
|
1043
|
+
local pattern
|
|
1044
|
+
while IFS= read -r pattern; do
|
|
1045
|
+
[ -n "$pattern" ] && patterns+=("$pattern")
|
|
1046
|
+
done < "$jd/pull.ere"
|
|
1047
|
+
[ "${#patterns[@]}" -gt 0 ] || return 0
|
|
1048
|
+
local candidate relative
|
|
1049
|
+
while IFS= read -r -d '' candidate; do
|
|
1050
|
+
relative=${candidate#./}
|
|
1051
|
+
for pattern in "${patterns[@]}"; do
|
|
1052
|
+
if [[ $relative =~ $pattern ]]; then
|
|
1053
|
+
mkdir -p "$jd/artifacts/$(dirname "$relative")" && cp -p "$tree/$relative" "$jd/artifacts/$relative"
|
|
1054
|
+
break
|
|
1055
|
+
fi
|
|
1056
|
+
done
|
|
1057
|
+
done < <(cd "$tree" && find "${roots[@]}" -name .git -prune -o -type f -newer "$jd/start-marker" -print0 2> /dev/null)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
finish_job() {
|
|
1061
|
+
local code=$1
|
|
1062
|
+
if [ -f "$jd/timeout" ]; then
|
|
1063
|
+
code=124
|
|
1064
|
+
say "timed out after ${timeout_sec}s"
|
|
1065
|
+
elif [ "$killed" -eq 1 ]; then
|
|
1066
|
+
code=137
|
|
1067
|
+
say "job killed"
|
|
1068
|
+
fi
|
|
1069
|
+
stop_watchdog
|
|
1070
|
+
set_phase collecting
|
|
1071
|
+
collect_artifacts
|
|
1072
|
+
release_lock
|
|
1073
|
+
rm -rf "$stage"
|
|
1074
|
+
set_phase done
|
|
1075
|
+
date +%s > "$jd/finished-at"
|
|
1076
|
+
printf '%s\n' "$code" > "$jd/exit.tmp" && mv "$jd/exit.tmp" "$jd/exit"
|
|
1077
|
+
exit "$code"
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
cmd_job() {
|
|
1081
|
+
set_paths "$1" "$2" "$3"
|
|
1082
|
+
exec >> "$jd/log" 2>&1
|
|
1083
|
+
printf '%s\n' "$$" > "$jd/pid"
|
|
1084
|
+
date +%s > "$jd/started-at"
|
|
1085
|
+
set_phase starting
|
|
1086
|
+
killed=0
|
|
1087
|
+
trap 'killed=1' TERM INT HUP
|
|
1088
|
+
# Pueue starts the task before the adder has written its id down.
|
|
1089
|
+
local id_wait_started=$SECONDS
|
|
1090
|
+
while [ ! -s "$jd/task-id" ] && [ $((SECONDS - id_wait_started)) -lt "$task_id_wait_sec" ]; do
|
|
1091
|
+
sleep 0.1
|
|
1092
|
+
done
|
|
1093
|
+
timeout_sec=$(read_file "$jd/timeout-sec")
|
|
1094
|
+
[ -n "$timeout_sec" ] || timeout_sec=$default_timeout_sec
|
|
1095
|
+
local limit_sec=$timeout_sec
|
|
1096
|
+
local queued_at
|
|
1097
|
+
queued_at=$(read_file "$jd/queued-at")
|
|
1098
|
+
case $queued_at in
|
|
1099
|
+
'' | *[!0-9]*) ;;
|
|
1100
|
+
*) limit_sec=$((timeout_sec - ($(date +%s) - queued_at))) ;;
|
|
1101
|
+
esac
|
|
1102
|
+
if [ "$limit_sec" -le 0 ]; then
|
|
1103
|
+
touch "$jd/timeout"
|
|
1104
|
+
say "its time limit ran out while it was queued"
|
|
1105
|
+
finish_job 124
|
|
1106
|
+
fi
|
|
1107
|
+
start_watchdog "$limit_sec"
|
|
1108
|
+
|
|
1109
|
+
set_phase waiting-lock
|
|
1110
|
+
acquire_lock || finish_job 137
|
|
1111
|
+
|
|
1112
|
+
set_phase syncing
|
|
1113
|
+
sync_tree || { say "applying the files failed"; finish_job 125; }
|
|
1114
|
+
|
|
1115
|
+
local subfolder
|
|
1116
|
+
subfolder=$(read_file "$jd/subfolder")
|
|
1117
|
+
check_relative_path "$subfolder" || { say "bad subfolder"; finish_job 125; }
|
|
1118
|
+
[ "$killed" -eq 1 ] && finish_job 137
|
|
1119
|
+
|
|
1120
|
+
local install_command install_key
|
|
1121
|
+
install_command=$(read_file "$jd/install-command")
|
|
1122
|
+
install_key=$(read_file "$jd/install-key")
|
|
1123
|
+
if [ -n "$install_command" ] && [ "$(read_file "$proj/deps.hash")" != "$install_key" ]; then
|
|
1124
|
+
set_phase deps
|
|
1125
|
+
say "installing dependencies: $install_command"
|
|
1126
|
+
run_step "$tree" "$install_command"
|
|
1127
|
+
if [ "$step_code" -ne 0 ]; then
|
|
1128
|
+
rm -f "$proj/deps.hash"
|
|
1129
|
+
echo deps > "$jd/failed-step"
|
|
1130
|
+
say "the dependencies step failed with exit code $step_code"
|
|
1131
|
+
finish_job 125
|
|
1132
|
+
fi
|
|
1133
|
+
printf '%s\n' "$install_key" > "$proj/deps.hash"
|
|
1134
|
+
fi
|
|
1135
|
+
[ "$killed" -eq 1 ] && finish_job 137
|
|
1136
|
+
|
|
1137
|
+
touch "$jd/start-marker"
|
|
1138
|
+
set_phase running
|
|
1139
|
+
run_step "$tree/$subfolder" "$(read_file "$jd/cmd")"
|
|
1140
|
+
finish_job "$step_code"
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
# ─────────────────────────── exec ───────────────────────────
|
|
1144
|
+
|
|
1145
|
+
cmd_exec() {
|
|
1146
|
+
set_paths "$1" "$2" ""
|
|
1147
|
+
local subfolder=$3
|
|
1148
|
+
local lease_sec
|
|
1149
|
+
lease_sec=$(ms_to_sec "$4")
|
|
1150
|
+
shift 4
|
|
1151
|
+
check_relative_path "$subfolder" || die "bad subfolder"
|
|
1152
|
+
[ -d "$tree/$subfolder" ] || die "the runner has no copy of this project yet (run a command first)"
|
|
1153
|
+
exec 3<&0
|
|
1154
|
+
# Own process group, so the lease can stop the command and everything it started.
|
|
1155
|
+
set -m
|
|
1156
|
+
( cd "$tree/$subfolder" && exec bash -c "$*" ) < /dev/null &
|
|
1157
|
+
local exec_pid=$!
|
|
1158
|
+
set +m
|
|
1159
|
+
(
|
|
1160
|
+
if ! watch_lease "$lease_sec"; then
|
|
1161
|
+
kill -TERM -- "-$exec_pid" 2> /dev/null
|
|
1162
|
+
local grace_started=$SECONDS
|
|
1163
|
+
while kill -0 "$exec_pid" 2> /dev/null && [ $((SECONDS - grace_started)) -lt "$kill_grace_sec" ]; do
|
|
1164
|
+
sleep 0.2
|
|
1165
|
+
done
|
|
1166
|
+
kill -KILL -- "-$exec_pid" 2> /dev/null
|
|
1167
|
+
fi
|
|
1168
|
+
) &
|
|
1169
|
+
local watcher_pid=$!
|
|
1170
|
+
wait "$exec_pid"
|
|
1171
|
+
local exec_code=$?
|
|
1172
|
+
kill "$watcher_pid" > /dev/null 2>&1
|
|
1173
|
+
# The command's own code, said out loud: ssh reports 255 for ITS failures, and a command that really exits 255 (ssh and adb do) would
|
|
1174
|
+
# look like a lost connection. The marker (RS, the name, the code, a newline) is the last thing on stderr; the CLI takes it out of what it
|
|
1175
|
+
# shows. A session that dies before it has none, which is how a lost connection is told from a command that exited 255.
|
|
1176
|
+
printf '\036remote-run-exit:%s\n' "$exec_code" >&2
|
|
1177
|
+
exit "$exec_code"
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
# ─────────────────────────── jobs / pueue passthrough ───────────────────────────
|
|
1181
|
+
|
|
1182
|
+
# One JSON object per job: {"id", "facts": "k=v;k=v", "meta": <meta.json>} — everything `ls` and `status` show, in one call.
|
|
1183
|
+
cmd_jobs() {
|
|
1184
|
+
local work_dir=$1
|
|
1185
|
+
local only_slug=${2:-}
|
|
1186
|
+
local first=1
|
|
1187
|
+
local meta job_dir
|
|
1188
|
+
printf '['
|
|
1189
|
+
for meta in "$work_dir"/p/${only_slug:-*}/jobs/*/meta.json; do
|
|
1190
|
+
[ -f "$meta" ] || continue
|
|
1191
|
+
job_dir=$(dirname "$meta")
|
|
1192
|
+
set_paths "$work_dir" "$(basename "$(dirname "$(dirname "$job_dir")")")" "$(basename "$job_dir")"
|
|
1193
|
+
[ "$first" -eq 1 ] || printf ','
|
|
1194
|
+
first=0
|
|
1195
|
+
printf '{"id":"%s","facts":"%s","meta":' "$job" "$(print_job_facts | tr '\n' ';')"
|
|
1196
|
+
cat "$meta"
|
|
1197
|
+
printf '}'
|
|
1198
|
+
done
|
|
1199
|
+
printf ']\n'
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
# The work dir as an absolute path (a `~/…` the caller configured is expanded by the shell before it gets here).
|
|
1203
|
+
cmd_workdir() {
|
|
1204
|
+
mkdir -p "$1" && (cd "$1" && pwd -P)
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
cmd_pueue() {
|
|
1208
|
+
shift
|
|
1209
|
+
pq "$@"
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
# ─────────────────────────── dispatch ───────────────────────────
|
|
1213
|
+
|
|
1214
|
+
main() {
|
|
1215
|
+
local subcommand=${1:-}
|
|
1216
|
+
[ $# -gt 0 ] && shift
|
|
1217
|
+
case $subcommand in
|
|
1218
|
+
probe) cmd_probe "$@" ;;
|
|
1219
|
+
prepare) cmd_prepare "$@" ;;
|
|
1220
|
+
lease) cmd_lease "$@" ;;
|
|
1221
|
+
submit) cmd_submit "$@" ;;
|
|
1222
|
+
job) cmd_job "$@" ;;
|
|
1223
|
+
follow) cmd_follow "$@" ;;
|
|
1224
|
+
wait) cmd_wait "$@" ;;
|
|
1225
|
+
result) cmd_result "$@" ;;
|
|
1226
|
+
meta) cmd_meta "$@" ;;
|
|
1227
|
+
tail) cmd_tail "$@" ;;
|
|
1228
|
+
kill) cmd_kill "$@" ;;
|
|
1229
|
+
exec) cmd_exec "$@" ;;
|
|
1230
|
+
pueue) cmd_pueue "$@" ;;
|
|
1231
|
+
workdir) cmd_workdir "$@" ;;
|
|
1232
|
+
jobs) cmd_jobs "$@" ;;
|
|
1233
|
+
prune) cmd_prune "$@" ;;
|
|
1234
|
+
sync-tree) cmd_sync_tree "$@" ;;
|
|
1235
|
+
*) die "unknown subcommand: $subcommand" ;;
|
|
1236
|
+
esac
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
# Run as a script it dispatches; sourced (the unit tests call the functions one by one) it only defines them.
|
|
1240
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
1241
|
+
main "$@"
|
|
1242
|
+
fi
|