Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

836 lines
28 KiB
Go

package daemon
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"sync"
"time"
"github.com/google/uuid"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
type HostConfig struct {
Repository market.Repository
CatalogSource market.CatalogSource
ReleaseInstallations market.ReleaseInstallationManager
ImplementationHost market.ImplementationHost
Authorization market.AuthorizationProvider
AuthorizationProjections market.AuthorizationProjectionStore
AuthorizationSnapshots market.AuthorizationSnapshotSource
AuthorizationEvents market.AuthorizationEventSource
AuthorizationReadiness *market.AuthorizationReadinessGate
RuntimeBindings market.RuntimeBindingResolver
Compatibility market.CompatibilityEvaluator
ImplementationRegistry market.ImplementationRegistry
Outbox market.ChangedEventOutbox
Lifecycle market.LifecycleCleanupStore
LifecyclePolicy LifecycleCleanupPolicy
Publisher ChangedEventPublisher
Publication CapabilityPublicationController
}
// CapabilityPublicationController is the daemon-level publication boundary
// for runtimes owned by another process or machine.
type CapabilityPublicationController interface {
ApplyCapabilityPublication(context.Context, market.OperationScope, bool) error
}
type Host struct {
Application *market.Application
cancel context.CancelFunc
scheduler *OperationScheduler
outboxDone chan struct{}
lifecycleDone chan struct{}
authorizationSyncDone chan struct{}
authorizationSyncWake chan struct{}
authorizationEventsDone chan struct{}
authorizationScopeWake chan struct{}
runtimeRecoveryDone chan struct{}
runtimeRecoveryWake chan struct{}
runtimeConvergenceDone chan struct{}
runtimeConvergenceWake chan struct{}
operationRecoveryDone chan struct{}
closeOnce sync.Once
bootstrapMu sync.Mutex
bootstrapped bool
bootstrapScope market.OperationScope
refreshWorkerStarted bool
repository market.Repository
implementationHost market.ImplementationHost
activationGate *activationGateHost
publicationGate capabilityPublicationGate
publication CapabilityPublicationController
authorizationSnapshots market.AuthorizationSnapshotSource
authorizationSnapshotStore market.AuthorizationSnapshotStore
authorizationEvents market.AuthorizationEventSource
authorizationReadiness *market.AuthorizationReadinessGate
authorizationDirty map[string]map[string]struct{}
runtimeRecoveryPending map[string]struct{}
}
type capabilityPublicationGate interface {
SetCapabilityPublication(bool)
}
func NewHost(parent context.Context, config HostConfig) (*Host, error) {
if parent == nil {
parent = context.Background()
}
if config.Outbox == nil || config.Lifecycle == nil || config.Publisher == nil {
return nil, errors.New("connector market outbox, lifecycle cleanup, and publisher are required")
}
hostContext, cancel := context.WithCancel(parent)
scheduler := NewOperationScheduler(hostContext)
activationGate := newActivationGateHost(config.ImplementationHost)
application, err := market.NewApplication(market.ApplicationConfig{
Repository: config.Repository,
CatalogSource: config.CatalogSource,
ReleaseInstallations: config.ReleaseInstallations,
Host: activationGate,
Authorization: config.Authorization,
AuthorizationProjections: config.AuthorizationProjections,
AuthorizationSnapshots: config.AuthorizationSnapshots,
AuthorizationReadiness: config.AuthorizationReadiness,
RuntimeBindings: config.RuntimeBindings,
Compatibility: config.Compatibility,
Scheduler: scheduler,
ImplementationRegistry: config.ImplementationRegistry,
})
if err != nil {
cancel()
return nil, err
}
if err := scheduler.Bind(application); err != nil {
cancel()
return nil, err
}
host := &Host{
Application: application,
cancel: cancel,
scheduler: scheduler,
outboxDone: make(chan struct{}),
lifecycleDone: make(chan struct{}),
authorizationSyncDone: make(chan struct{}),
authorizationSyncWake: make(chan struct{}, 1),
authorizationEventsDone: make(chan struct{}),
authorizationScopeWake: make(chan struct{}, 1),
runtimeRecoveryDone: make(chan struct{}),
runtimeRecoveryWake: make(chan struct{}, 1),
runtimeConvergenceDone: make(chan struct{}),
runtimeConvergenceWake: make(chan struct{}, 1),
operationRecoveryDone: make(chan struct{}),
repository: config.Repository,
implementationHost: config.ImplementationHost,
activationGate: activationGate,
publication: config.Publication,
authorizationSnapshots: config.AuthorizationSnapshots,
authorizationEvents: config.AuthorizationEvents,
authorizationReadiness: config.AuthorizationReadiness,
authorizationDirty: make(map[string]map[string]struct{}),
runtimeRecoveryPending: make(map[string]struct{}),
}
if snapshotStore, ok := config.AuthorizationProjections.(market.AuthorizationSnapshotStore); ok {
host.authorizationSnapshotStore = snapshotStore
}
if publicationGate, ok := config.ImplementationHost.(capabilityPublicationGate); ok {
host.publicationGate = publicationGate
if host.publication == nil {
publicationGate.SetCapabilityPublication(false)
}
}
dispatcher := OutboxDispatcher{Outbox: config.Outbox, Publisher: config.Publisher}
go func() {
defer close(host.outboxDone)
dispatcher.Run(hostContext)
}()
if host.authorizationSnapshots != nil && host.authorizationSnapshotStore != nil {
go host.runAuthorizationSnapshotWorker(hostContext)
} else {
close(host.authorizationSyncDone)
}
if host.authorizationEvents != nil {
go host.runAuthorizationEventWorker(hostContext)
} else {
close(host.authorizationEventsDone)
}
if _, ok := config.Authorization.(market.AuthorizationObserver); ok {
go host.runAuthorizationReconcileWorker(hostContext)
}
go func() {
defer close(host.runtimeRecoveryDone)
host.runRuntimeRecoveryWorker(hostContext)
}()
go func() {
defer close(host.runtimeConvergenceDone)
host.runRuntimeConvergenceWorker(hostContext)
}()
go func() {
defer close(host.operationRecoveryDone)
host.runOperationRecoveryWorker(hostContext)
}()
cleanupWorker := LifecycleCleanupWorker{Store: config.Lifecycle, Policy: config.LifecyclePolicy}
go func() {
defer close(host.lifecycleDone)
cleanupWorker.Run(hostContext)
}()
return host, nil
}
func (host *Host) runAuthorizationReconcileWorker(ctx context.Context) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
reconcileContext, cancel := context.WithTimeout(ctx, 15*time.Second)
host.bootstrapMu.Lock()
bootstrapped, scope := host.bootstrapped, host.bootstrapScope
if !bootstrapped || strings.TrimSpace(scope.AccountID) == "" {
host.bootstrapMu.Unlock()
cancel()
continue
}
err := host.reconcileAuthorizationsLocked(reconcileContext, scope)
host.bootstrapMu.Unlock()
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector authorization reconciliation failed", "error", err)
}
}
}
}
func (host *Host) reconcileAuthorizationsLocked(ctx context.Context, scope market.OperationScope) error {
intents, err := host.Application.ReconcileAuthorizations(ctx, scope)
if err != nil || len(intents) == 0 {
return err
}
// ReconcileAuthorizations persists projection intent. Create or join one
// scoped runtime operation for every affected Connector before releasing the
// lifecycle fence, so logout or account switching cannot be followed by a
// late old-account route publication.
connectorKeys := make(map[string]struct{}, len(intents))
for _, intent := range intents {
connectorKeys[intent.ConnectorKey] = struct{}{}
}
for connectorKey := range connectorKeys {
if reconcileErr := host.reconcileRuntimeForScopeLocked(ctx, scope, connectorKey); reconcileErr != nil {
err = errors.Join(err, reconcileErr)
}
}
if err != nil {
return err
}
for _, intent := range intents {
if resolveErr := host.repository.ResolveAuthorizationSession(ctx, intent.OperationID, intent.Resolution); resolveErr != nil {
err = errors.Join(err, resolveErr)
}
}
return err
}
// Bootstrap restores durable local runtime intent without depending on the
// remote catalog. Account-authorized remote routes additionally require a
// fresh server snapshot before the lifecycle gate opens.
func (host *Host) Bootstrap(ctx context.Context) error {
return host.BootstrapForScope(ctx, market.OperationScope{})
}
// BootstrapForScope restores device-installed runtimes for the explicitly
// active account authority. The scope is retained for retry workers but no
// short-lived grant is retained by the daemon.
func (host *Host) BootstrapForScope(ctx context.Context, scope market.OperationScope) error {
if host == nil || host.Application == nil {
return errors.New("connector market host is unavailable")
}
bootstrapStartedAt := time.Now()
host.bootstrapMu.Lock()
defer host.bootstrapMu.Unlock()
sameScope := host.bootstrapScope == scope
if host.bootstrapped && sameScope && !host.activationGate.requiresRecovery() {
host.notifyRuntimeRecovery()
host.notifyRuntimeConvergence()
return nil
}
host.bootstrapScope = scope
host.runtimeRecoveryPending = make(map[string]struct{})
if host.authorizationReadiness != nil && strings.TrimSpace(scope.AccountID) != "" {
host.authorizationReadiness.SetReady(scope.AccountID, false)
}
host.notifyAuthorizationScopeChanged()
host.bootstrapped = false
if !host.refreshWorkerStarted {
host.refreshWorkerStarted = true
go host.runCatalogRefreshWorker()
}
host.activationGate.setOpen(scope, false)
if err := host.applyCapabilityPublication(ctx, scope, false); err != nil {
return err
}
committed := false
defer func() {
if committed {
return
}
host.activationGate.setOpen(scope, false)
_ = host.applyCapabilityPublication(context.Background(), scope, false)
fenceContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := host.implementationHost.FailClosed(fenceContext, time.Now().Add(10*time.Second)); err != nil {
slog.Error("connector market bootstrap rollback runtime fence failed", "error", err)
}
if err := host.Application.FenceInstalledRuntimesForScope(fenceContext, scope); err != nil {
slog.Error("connector market bootstrap rollback fence failed", "error", err)
}
}()
// Fence any route left by an interrupted previous bootstrap before recovery
// can replay host-touching operations. Reconcile calls remain staged behind
// activationGate until durable local recovery has completed.
if err := host.implementationHost.FailClosed(ctx, time.Now().Add(10*time.Second)); err != nil {
return err
}
if err := host.Application.FenceInstalledRuntimesForScope(ctx, scope); err != nil {
return err
}
if err := host.recoverAndWait(ctx); err != nil {
return err
}
installedConnectorKeys, err := host.Application.InstallationCalibrationConnectorKeys(ctx)
if err != nil {
return err
}
for _, connectorKey := range installedConnectorKeys {
host.runtimeRecoveryPending[connectorKey] = struct{}{}
}
// Global bootstrap ends once stale authority is fenced and durable work is
// safe to resume. Connector-local planning, runtime convergence, physical
// installation calibration, and account authorization refresh continue on
// independent workers after publication opens. Remote authorized routes
// remain fail-closed behind AuthorizationReadiness until their fresh account
// snapshot is applied.
host.activationGate.setOpen(scope, true)
if err := host.applyCapabilityPublication(ctx, scope, true); err != nil {
return err
}
host.activationGate.markRecovered()
host.bootstrapped = true
committed = true
host.notifyRuntimeRecovery()
if strings.TrimSpace(scope.AccountID) != "" {
host.NotifyAuthorizationChanged()
}
slog.Info("connector market bootstrap committed",
"accountScoped", strings.TrimSpace(scope.AccountID) != "",
"connectorRecoveryCount", len(installedConnectorKeys),
"durationMs", time.Since(bootstrapStartedAt).Milliseconds())
return nil
}
// NotifyAuthorizationChanged treats realtime events and MCP authorization
// errors as convergence hints. The account snapshot remains the only truth.
func (host *Host) NotifyAuthorizationChanged() {
if host == nil {
return
}
select {
case host.authorizationSyncWake <- struct{}{}:
default:
}
}
func (host *Host) notifyAuthorizationScopeChanged() {
select {
case host.authorizationScopeWake <- struct{}{}:
default:
}
}
func (host *Host) runAuthorizationEventWorker(ctx context.Context) {
defer close(host.authorizationEventsDone)
retry := time.Second
for {
host.bootstrapMu.Lock()
scope := host.bootstrapScope
host.bootstrapMu.Unlock()
if strings.TrimSpace(scope.AccountID) == "" {
select {
case <-ctx.Done():
return
case <-host.authorizationScopeWake:
continue
}
}
attemptCtx, cancel := context.WithCancel(ctx)
result := make(chan error, 1)
go func(accountID string) {
result <- host.authorizationEvents.RunAuthorizationEvents(attemptCtx, accountID, host.NotifyAuthorizationChanged)
}(scope.AccountID)
select {
case <-ctx.Done():
cancel()
<-result
return
case <-host.authorizationScopeWake:
cancel()
<-result
retry = time.Second
continue
case err := <-result:
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector authorization realtime listener failed", "error", err)
}
}
timer := time.NewTimer(retry)
select {
case <-ctx.Done():
timer.Stop()
return
case <-host.authorizationScopeWake:
timer.Stop()
retry = time.Second
case <-timer.C:
if retry < time.Minute {
retry *= 2
}
}
}
}
type authorizationSyncResult struct {
changed []string
receipts []string
becameReady bool
calibrate bool
snapshotApplied bool
}
func (host *Host) syncAuthorizationSnapshot(ctx context.Context, scope market.OperationScope) (authorizationSyncResult, error) {
if host.authorizationSnapshots == nil || host.authorizationSnapshotStore == nil || strings.TrimSpace(scope.AccountID) == "" {
return authorizationSyncResult{}, nil
}
snapshot, err := host.authorizationSnapshots.AuthorizationSnapshot(ctx, scope.AccountID)
if err != nil {
return authorizationSyncResult{}, err
}
applied, err := host.authorizationSnapshotStore.ApplyAuthorizationSnapshot(ctx, scope.AccountID, snapshot)
result := authorizationSyncResult{
changed: applied.ChangedConnectorKeys,
receipts: applied.PendingReceiptConnectorKeys,
snapshotApplied: err == nil,
}
return result, err
}
func (host *Host) runAuthorizationSnapshotWorker(ctx context.Context) {
defer close(host.authorizationSyncDone)
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
calibrate := false
select {
case <-ctx.Done():
return
case <-ticker.C:
calibrate = true
case <-host.authorizationSyncWake:
}
host.bootstrapMu.Lock()
bootstrapped, scope := host.bootstrapped, host.bootstrapScope
host.bootstrapMu.Unlock()
if !bootstrapped || strings.TrimSpace(scope.AccountID) == "" {
continue
}
syncContext, cancel := context.WithTimeout(ctx, 30*time.Second)
result, err := host.syncAuthorizationSnapshot(syncContext, scope)
result.calibrate = calibrate
cancel()
if err != nil {
if !errors.Is(err, context.Canceled) {
slog.Warn("connector authorization snapshot sync failed", "error", err)
}
continue
}
reconcileContext, cancel := context.WithTimeout(ctx, 2*time.Minute)
err = host.reconcileAuthorizationChanges(reconcileContext, scope, result)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector authorization runtime reconcile failed", "error", err)
}
}
}
func (host *Host) reconcileAuthorizationChanges(ctx context.Context, scope market.OperationScope, result authorizationSyncResult) error {
host.bootstrapMu.Lock()
defer host.bootstrapMu.Unlock()
if !host.bootstrapped || host.bootstrapScope != scope || host.activationGate.requiresRecovery() {
return nil
}
if result.snapshotApplied && host.authorizationReadiness != nil {
result.becameReady = host.authorizationReadiness.SetReady(scope.AccountID, true)
}
dirty := host.authorizationDirty[scope.AccountID]
if dirty == nil {
dirty = make(map[string]struct{})
host.authorizationDirty[scope.AccountID] = dirty
}
connectorKeys, err := host.Application.InstalledRemoteAuthorizedConnectorKeys(ctx)
if err != nil {
return err
}
eligible := make(map[string]struct{}, len(connectorKeys))
for _, connectorKey := range connectorKeys {
eligible[connectorKey] = struct{}{}
}
for connectorKey := range dirty {
if _, ok := eligible[connectorKey]; !ok {
delete(dirty, connectorKey)
}
}
for _, connectorKey := range result.changed {
if _, ok := eligible[connectorKey]; ok {
dirty[connectorKey] = struct{}{}
}
}
for _, connectorKey := range result.receipts {
if _, ok := eligible[connectorKey]; ok {
dirty[connectorKey] = struct{}{}
}
}
if result.becameReady || result.calibrate {
for _, connectorKey := range connectorKeys {
dirty[connectorKey] = struct{}{}
}
}
var reconcileErr error
for connectorKey := range dirty {
if err := host.reconcileRuntimeForScopeLocked(ctx, scope, connectorKey); err != nil {
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("%s: %w", connectorKey, err))
continue
}
delete(dirty, connectorKey)
}
return reconcileErr
}
// FenceForScope closes publication and runtime authority for an account
// boundary without deleting device installation truth. A later bootstrap,
// including one for the same account, must perform full recovery before routes
// can be published again.
func (host *Host) FenceForScope(ctx context.Context, scope market.OperationScope) error {
if host == nil || host.Application == nil {
return errors.New("connector market host is unavailable")
}
host.bootstrapMu.Lock()
defer host.bootstrapMu.Unlock()
host.activationGate.setOpen(scope, false)
previousScope := host.bootstrapScope
if host.authorizationReadiness != nil && strings.TrimSpace(previousScope.AccountID) != "" {
host.authorizationReadiness.SetReady(previousScope.AccountID, false)
}
if host.authorizationReadiness != nil && strings.TrimSpace(scope.AccountID) != "" {
host.authorizationReadiness.SetReady(scope.AccountID, false)
}
host.bootstrapped = false
host.bootstrapScope = scope
host.runtimeRecoveryPending = make(map[string]struct{})
host.notifyAuthorizationScopeChanged()
publicationErr := host.applyCapabilityPublication(ctx, scope, false)
fenceErr := host.activationGate.FailClosed(ctx, time.Now().Add(10*time.Second))
return errors.Join(publicationErr, fenceErr)
}
// ReconcileRuntimeForScope repairs one observed runtime route under the same
// lifecycle gate as bootstrap and fencing. The operation is awaited while the
// gate is held so a concurrent runtime replacement cannot fence its generation
// after acceptance but before the VM receipt is committed.
func (host *Host) ReconcileRuntimeForScope(ctx context.Context, scope market.OperationScope, connectorKey string) error {
if host == nil || host.Application == nil {
return errors.New("connector market host is unavailable")
}
if !host.bootstrapMu.TryLock() {
// A bootstrap, fence, or earlier repair already owns convergence. The
// observer will verify a fresh VM snapshot after that operation finishes.
return nil
}
defer host.bootstrapMu.Unlock()
if !host.bootstrapped || host.bootstrapScope != scope || host.activationGate.requiresRecovery() {
// Bootstrap owns convergence while the lifecycle gate is closed. Enqueuing
// a second per-Connector operation here would race its generation fence.
return nil
}
return host.reconcileRuntimeForScopeLockedMode(ctx, scope, connectorKey, true)
}
func (host *Host) reconcileRuntimeForScopeLocked(ctx context.Context, scope market.OperationScope, connectorKey string) error {
return host.reconcileRuntimeForScopeLockedMode(ctx, scope, connectorKey, false)
}
func (host *Host) reconcileRuntimeForScopeLockedMode(
ctx context.Context,
scope market.OperationScope,
connectorKey string,
force bool,
) error {
connector, err := host.Application.GetConnector(ctx, connectorKey)
if errors.Is(err, market.ErrNotFound) || err == nil && connector.Installation.State != market.InstallationStateInstalled {
return nil
}
if err != nil {
return err
}
if force {
return host.Application.ReconcileRuntimeAfterInvalidation(ctx, scope, connectorKey)
}
return host.Application.ReconcileRuntimeDesired(ctx, scope, connectorKey)
}
// ObserveAuthorizationForScope commits account authorization and its runtime
// reconcile under the lifecycle gate. This prevents authorization callbacks
// from publishing a generation concurrently with bootstrap recovery.
func (host *Host) ObserveAuthorizationForScope(
ctx context.Context,
scope market.OperationScope,
projection market.AuthorizationProjection,
) error {
if host == nil || host.Application == nil {
return errors.New("connector market host is unavailable")
}
host.bootstrapMu.Lock()
defer host.bootstrapMu.Unlock()
if err := host.Application.ProjectAuthorization(ctx, scope, projection); err != nil {
return err
}
return host.reconcileRuntimeForScopeLocked(ctx, scope, projection.ConnectorKey)
}
func (host *Host) applyCapabilityPublication(ctx context.Context, scope market.OperationScope, enabled bool) error {
if host.publication != nil {
return host.publication.ApplyCapabilityPublication(ctx, scope, enabled)
}
if host.publicationGate != nil {
host.publicationGate.SetCapabilityPublication(enabled)
}
return nil
}
func (host *Host) recoverAndWait(ctx context.Context) error {
operations, err := host.repository.RecoverableOperations(ctx)
if err != nil {
return err
}
if len(operations) == 0 {
return nil
}
if err := host.Application.Recover(ctx); err != nil {
return err
}
ticker := time.NewTicker(25 * time.Millisecond)
defer ticker.Stop()
for {
pending := false
for _, candidate := range operations {
// Remote refresh may wait on the network. Start authorization is not
// replayed from durable state. Recover those rows, but do not make
// local route restoration wait for their terminal state.
if candidate.Kind != market.OperationKindInstall && candidate.Kind != market.OperationKindUninstall &&
candidate.Kind != market.OperationKindReconcileRuntime {
continue
}
operation, err := host.Application.GetOperation(ctx, candidate.OperationID)
if err != nil {
return err
}
if operation.State == market.OperationStateAccepted || operation.State == market.OperationStateRunning {
pending = true
}
}
if !pending {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
func (host *Host) refreshAndWait(ctx context.Context) error {
snapshot, err := host.Application.Snapshot(ctx)
if err != nil {
return err
}
result, err := host.Application.RefreshCatalog(ctx, market.Mutation{
ClientRequestID: "daemon-refresh-" + uuid.NewString(), ExpectedRevision: snapshot.Revision,
})
if err != nil {
return err
}
ticker := time.NewTicker(25 * time.Millisecond)
defer ticker.Stop()
for {
operation, err := host.Application.GetOperation(ctx, result.Operation.OperationID)
if err != nil {
return err
}
switch operation.State {
case market.OperationStateCompleted:
return nil
case market.OperationStateFailed:
return fmt.Errorf("connector market refresh failed: %s", operation.FailureCode)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
func (host *Host) runCatalogRefreshWorker() {
bootstrapRetry := time.Second
catalogRetry := time.Duration(0)
for {
host.bootstrapMu.Lock()
bootstrapped := host.bootstrapped
scope := host.bootstrapScope
host.bootstrapMu.Unlock()
if !bootstrapped {
timer := time.NewTimer(bootstrapRetry)
select {
case <-host.scheduler.ctx.Done():
timer.Stop()
return
case <-timer.C:
}
bootstrapContext, cancel := context.WithTimeout(host.scheduler.ctx, 45*time.Second)
err := host.BootstrapForScope(bootstrapContext, scope)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector market bootstrap retry failed", "error", err)
if bootstrapRetry < time.Minute {
bootstrapRetry *= 2
}
} else {
bootstrapRetry = time.Second
}
continue
}
timer := time.NewTimer(catalogRetry)
select {
case <-host.scheduler.ctx.Done():
timer.Stop()
return
case <-timer.C:
}
refreshContext, cancel := context.WithTimeout(host.scheduler.ctx, 45*time.Second)
err := host.refreshAndWait(refreshContext)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector market scheduled refresh failed", "error", err)
if catalogRetry < time.Minute {
catalogRetry = time.Minute
} else if catalogRetry < 5*time.Minute {
catalogRetry *= 2
}
continue
}
catalogRetry = time.Minute
}
}
func (host *Host) Close() {
if host == nil {
return
}
host.closeOnce.Do(func() {
host.cancel()
if closer, ok := host.implementationHost.(interface{ Close() error }); ok {
_ = closer.Close()
}
<-host.outboxDone
<-host.lifecycleDone
<-host.authorizationSyncDone
<-host.authorizationEventsDone
<-host.runtimeRecoveryDone
<-host.runtimeConvergenceDone
<-host.operationRecoveryDone
host.scheduler.Wait()
})
}
// CatalogOnlyPorts deliberately advertise no installable implementation. The
// host can safely expose remote browsing before a concrete runtime activator,
// artifact resolver, and authorization provider are registered.
func CatalogOnlyPorts() (
market.ReleaseInstallationManager,
market.ImplementationHost,
market.AuthorizationProvider,
market.CompatibilityEvaluator,
market.ImplementationRegistry,
) {
return unavailableReleaseInstaller{}, unavailableRuntime{}, unavailableAuthorization{},
rejectingCompatibility{}, market.NewImplementationRegistry(nil)
}
type unavailableReleaseInstaller struct{}
func (unavailableReleaseInstaller) InstallRelease(context.Context, market.InstallReleaseRequest) (market.ReleaseInstallationReceipt, error) {
return market.ReleaseInstallationReceipt{}, errors.New("connector release installation is not registered")
}
func (unavailableReleaseInstaller) InspectReleaseInstallation(_ context.Context, request market.InspectReleaseInstallationRequest) (market.ReleaseInstallationObservation, error) {
return market.ReleaseInstallationObservation{State: market.ReleaseInstallationIndeterminate,
ConnectorKey: request.Release.ConnectorKey, ReleaseDigest: request.Release.ReleaseDigest,
ReasonCode: "release_installation_manager_unavailable"}, nil
}
func (unavailableReleaseInstaller) CommitReleaseInstallation(context.Context, market.CommitReleaseInstallationRequest) error {
return errors.New("connector release installation is not registered")
}
func (unavailableReleaseInstaller) UninstallRelease(context.Context, market.UninstallReleaseRequest) error {
return errors.New("connector release installation is not registered")
}
type unavailableRuntime struct{}
func (unavailableRuntime) Reconcile(context.Context, market.RuntimeReconcileRequest) (market.RuntimeReceipt, error) {
return market.RuntimeReceipt{}, errors.New("connector implementation host is not registered")
}
func (unavailableRuntime) DeactivateRuntime(context.Context, market.RuntimeDeactivationRequest) error {
return errors.New("connector runtime is not registered")
}
func (unavailableRuntime) FailClosed(context.Context, time.Time) error {
return errors.New("connector runtime is not registered")
}
type unavailableAuthorization struct{}
func (unavailableAuthorization) Begin(context.Context, market.AuthorizationStartRequest) (market.AuthorizationSession, error) {
return market.AuthorizationSession{}, errors.New("connector authorization is not registered")
}
func (unavailableAuthorization) Disconnect(context.Context, market.AuthorizationDisconnectRequest) error {
return errors.New("connector authorization is not registered")
}
type rejectingCompatibility struct{}
func (rejectingCompatibility) Evaluate(market.Manifest) market.Compatibility {
return market.Compatibility{
State: market.CompatibilityStateUnsupportedVersion,
Reason: "connector_runtime_not_registered",
}
}