Files
run/runtime/remote_access.go
2026-08-26 09:56:43 +08:00

189 lines
8.0 KiB
Go

package runtime
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"sync"
"time"
"browser.local/run/protocol"
)
type RemoteAdapterRequest struct {
JobID string
ServerInstanceID string
AdapterKey string
AdapterKind string
TargetKey string
Capability string
InputRef string
}
type RemoteAdapterOutcome struct {
Message string
ResultRef string
Retryable bool
}
type RemoteAdapter interface {
Execute(context.Context, RemoteAdapterRequest) (RemoteAdapterOutcome, error)
}
type RemoteAdapterFunc func(context.Context, RemoteAdapterRequest) (RemoteAdapterOutcome, error)
func (fn RemoteAdapterFunc) Execute(ctx context.Context, request RemoteAdapterRequest) (RemoteAdapterOutcome, error) {
return fn(ctx, request)
}
type RemoteAdapterRegistry struct {
mu sync.RWMutex
adapters map[string]RemoteAdapter
}
func NewRemoteAdapterRegistry() *RemoteAdapterRegistry {
registry := &RemoteAdapterRegistry{adapters: map[string]RemoteAdapter{}}
for _, kind := range []string{"ftp", "rsync", "run-file", "run-process", "database", "log-transfer"} {
registry.adapters[kind] = declaredRemoteAdapter{kind: kind}
}
return registry
}
func (registry *RemoteAdapterRegistry) Register(kind string, adapter RemoteAdapter) error {
kind = strings.TrimSpace(kind)
if kind == "" || adapter == nil {
return fmt.Errorf("remote adapter kind and implementation are required")
}
registry.mu.Lock()
defer registry.mu.Unlock()
registry.adapters[kind] = adapter
return nil
}
func (registry *RemoteAdapterRegistry) adapter(kind string) (RemoteAdapter, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
adapter, exists := registry.adapters[kind]
return adapter, exists
}
func ExecuteRemoteAccessJob(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult {
return ExecuteRemoteAccessJobWithRegistry(ctx, assignment, NewRemoteAdapterRegistry())
}
func ExecuteRemoteAccessJobWithRegistry(ctx context.Context, assignment protocol.RunJobAssignment, registry *RemoteAdapterRegistry) LifecycleExecutionResult {
if assignment.ExecutionInput.SourceRCON != nil {
return lifecycleFailure("source_rcon_requires_worker_transport", "Source RCON commands require the one-time worker transport")
}
if err := protocol.ValidateRunJobAssignment(assignment); err != nil {
if strings.Contains(err.Error(), "remoteAdapterKey") {
return lifecycleFailure("unsafe_remote_adapter_target", "remote adapter key must be an approved logical key")
}
return lifecycleFailure("unsafe_remote_access_job", err.Error())
}
if !isSupportedRemoteCapability(assignment.Capability) {
return lifecycleFailure("unsupported_remote_access_capability", "unsupported remote access capability")
}
if registry == nil {
return lifecycleFailure("remote_adapter_unavailable", "remote adapter registry is unavailable")
}
adapterKind := strings.TrimSpace(assignment.ExecutionInput.RemoteAdapterKind)
if adapterKind == "" {
adapterKind = adapterKindForCapability(assignment.Capability)
}
adapterKey := strings.TrimSpace(assignment.ExecutionInput.RemoteAdapterKey)
if adapterKey == "" {
adapterKey = assignment.TargetKey
}
if !protocol.ValidLogicalFileKey(adapterKey) || !protocol.ValidLogicalFileKey(assignment.TargetKey) {
return lifecycleFailure("unsafe_remote_adapter_target", "remote adapter and target must use approved logical keys")
}
if !adapterKindAllowsCapability(adapterKind, assignment.Capability) {
return lifecycleFailure("remote_adapter_capability_mismatch", "remote adapter kind does not allow requested capability")
}
adapter, exists := registry.adapter(adapterKind)
if !exists {
return lifecycleFailure("remote_adapter_unavailable", "declared remote adapter is unavailable")
}
executionCtx := ctx
cancel := func() {}
if timeout := assignment.ExecutionInput.TimeoutSeconds; timeout > 0 {
if timeout > 300 {
return lifecycleFailure("unsafe_remote_adapter_timeout", "remote adapter timeout exceeds bound")
}
executionCtx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
}
defer cancel()
request := RemoteAdapterRequest{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, AdapterKey: adapterKey, AdapterKind: adapterKind, TargetKey: assignment.TargetKey, Capability: assignment.Capability, InputRef: assignment.InputRef}
outcome, err := adapter.Execute(executionCtx, request)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(executionCtx.Err(), context.DeadlineExceeded) {
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter timed out"}, Message: "remote adapter timed out", ErrorCode: "remote_adapter_timeout", Retryable: true}
}
if errors.Is(err, context.Canceled) || errors.Is(executionCtx.Err(), context.Canceled) {
return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter cancelled"}, Message: "remote adapter cancelled", ErrorCode: "remote_adapter_cancelled"}
}
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter failed"}, Message: "remote adapter failed", ErrorCode: "remote_adapter_failed", Retryable: outcome.Retryable}
}
if err := executionCtx.Err(); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter timed out"}, Message: "remote adapter timed out", ErrorCode: "remote_adapter_timeout", Retryable: true}
}
return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter cancelled"}, Message: "remote adapter cancelled", ErrorCode: "remote_adapter_cancelled"}
}
resultRef := outcome.ResultRef
if resultRef == "" {
resultRef = fmt.Sprintf("artifact://jobs/%s/remote-access-result", url.PathEscape(assignment.JobID))
}
message := strings.TrimSpace(outcome.Message)
if message == "" {
message = fmt.Sprintf("%s completed through declared %s adapter", assignment.Capability, adapterKind)
}
return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter completed"}, ResultRef: resultRef, Message: message}
}
type declaredRemoteAdapter struct {
kind string
}
func (adapter declaredRemoteAdapter) Execute(ctx context.Context, request RemoteAdapterRequest) (RemoteAdapterOutcome, error) {
select {
case <-ctx.Done():
return RemoteAdapterOutcome{}, ctx.Err()
default:
}
if request.AdapterKind != adapter.kind || request.ServerInstanceID == "" || request.JobID == "" {
return RemoteAdapterOutcome{}, fmt.Errorf("remote adapter request identity mismatch")
}
return RemoteAdapterOutcome{Message: fmt.Sprintf("%s completed through bounded remote access envelope", request.Capability), ResultRef: fmt.Sprintf("artifact://jobs/%s/remote-access-result", url.PathEscape(request.JobID))}, nil
}
func adapterKindForCapability(capability string) string {
switch capability {
case protocol.RunCapabilityRemoteFTPRead, protocol.RunCapabilityRemoteFTPWrite:
return "ftp"
case protocol.RunCapabilityRemoteRsyncRead, protocol.RunCapabilityRemoteRsyncWrite:
return "rsync"
case protocol.RunCapabilityRemoteRunFilesRead, protocol.RunCapabilityRemoteRunFilesWrite:
return "run-file"
case protocol.RunCapabilityRemoteRunProcessStart, protocol.RunCapabilityRemoteRunProcessStop:
return "run-process"
case protocol.RunCapabilityRemoteRunDBMySQLQuery, protocol.RunCapabilityRemoteRunDBSQLiteQuery:
return "database"
case protocol.RunCapabilityRemoteRunRCONCommand:
return "rcon"
case protocol.RunCapabilityRemoteRunLogsTransfer:
return "log-transfer"
default:
return ""
}
}
func adapterKindAllowsCapability(kind string, capability string) bool {
return kind != "" && kind == adapterKindForCapability(capability)
}