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

799 lines
28 KiB
Go

package workspace
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"path/filepath"
"strings"
"sync"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/httpx"
workspacefiles "github.com/tutti-os/tutti/packages/workspace/files"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
builtinapps "github.com/tutti-os/tutti/services/tuttid/builtin-apps"
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
appcliservice "github.com/tutti-os/tutti/services/tuttid/service/cli/appcli"
)
type AppCenterService struct {
Store workspacedata.AppStore
AppFactoryStore workspacedata.AppFactoryStore
WorkspaceStore workspacedata.CatalogStore
PreferencesStore workspacedata.PreferencesStore
Runner *AppRunner
ShellAdapter AppShellAdapter
AppCLIRegistry *appcliservice.Registry
StateDir string
HostTuttiVersion string
HostTuttiCapabilities []string
Publisher WorkspaceAppEventPublisher
BuiltinCatalog func() ([]builtinapps.App, error)
ArtifactFetcher AppArtifactFetcher
RemoteCatalogRefresher func(context.Context, string, builtinapps.CatalogHost) (builtinapps.CatalogSnapshot, error)
runnerMu sync.Mutex
mu sync.Mutex
stateRevisions map[string]int64
appProjectionKeys map[string]workspaceAppProjectionKey
installMu sync.Mutex
installJobs map[string]workspaceAppInstallJob
activeInstallTrackers map[string]*installProgressTracker
installProgressSent map[string]uint64
installGeneration uint64
installPublishLocks keyedOperationLocks
remoteBuiltinInstallLocks keyedOperationLocks
uploadMu sync.Mutex
uploadSessions map[string]*workspaceAppUploadSession
uploadJanitorOnce sync.Once
uploadJanitorStop chan struct{}
uploadJanitorInterval time.Duration
}
type workspaceAppInstallJobStatus string
const (
workspaceAppInstallJobInstalling workspaceAppInstallJobStatus = "installing"
workspaceAppInstallJobFailed workspaceAppInstallJobStatus = "failed"
)
type workspaceAppInstallJob struct {
Generation uint64
WorkspaceID string
AppID string
Status workspaceAppInstallJobStatus
FailureReason string
FailurePhase workspacebiz.AppFailurePhase
CurrentPhase workspacebiz.AppInstallUserPhase
RestartRunning bool
Progress *workspacebiz.AppInstallProgress
BaselineRuntimeStatus workspacebiz.AppRuntimeStatus
BaselineRuntimeUpdatedAtUnixMs *int64
}
type WorkspaceRootResolver interface {
ResolveWorkspaceRoot(context.Context, string) (workspacefiles.WorkspaceRoot, error)
}
type WorkspaceAppEventPublisher interface {
PublishWorkspaceAppUpdated(context.Context, string, workspacebiz.WorkspaceApp) error
}
type AppArtifactFetcher interface {
FetchAppArtifact(context.Context, string, string) error
}
type HTTPAppArtifactFetcher struct {
Client *http.Client
}
func (f HTTPAppArtifactFetcher) FetchAppArtifact(ctx context.Context, artifactURL string, destinationPath string) error {
client := f.Client
if client == nil {
client = httpx.Default()
}
return downloadAppArtifact(ctx, client, artifactURL, destinationPath)
}
func (s *AppCenterService) InitBuiltinPackages(ctx context.Context) error {
if s.Store == nil {
return errors.New("workspace app store is not configured")
}
builtins, err := s.builtinCatalog(ctx)
if err != nil {
return err
}
for _, builtin := range builtins {
switch builtin.Distribution.Kind {
case builtinapps.DistributionRemote:
continue
case builtinapps.DistributionEmbeddedArchive:
if _, err := s.materializeEmbeddedArchiveBuiltinPackage(ctx, builtin); err != nil {
return fmt.Errorf("initialize embedded builtin app archive %q: %w", builtin.Manifest.AppID, err)
}
continue
}
packageDir := s.packageCacheDir(builtin.Manifest.AppID, builtin.Manifest.Version)
if err := builtinapps.CopyTo(builtin, packageDir); err != nil {
return fmt.Errorf("initialize builtin app package %q: %w", builtin.Manifest.AppID, err)
}
manifest, manifestJSON, err := workspacebiz.ReadAppManifestFile(filepath.Join(packageDir, "tutti.app.json"))
if err != nil {
return fmt.Errorf("read initialized builtin app manifest %q: %w", builtin.Manifest.AppID, err)
}
if manifest.AppID != builtin.Manifest.AppID || manifest.Version != builtin.Manifest.Version {
return fmt.Errorf("initialized builtin app manifest mismatch for %q", builtin.Manifest.AppID)
}
if err := s.Store.PutAppPackage(ctx, workspacebiz.AppPackage{
AppID: manifest.AppID,
Version: manifest.Version,
PackageDir: packageDir,
Manifest: manifest,
ManifestJSON: manifestJSON,
Source: workspacebiz.AppPackageSourceBuiltin,
}); err != nil {
return err
}
}
return nil
}
func (s *AppCenterService) List(ctx context.Context, workspaceID string) ([]workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return nil, err
}
builtins, err := s.builtinCatalog(ctx)
if err != nil {
return nil, err
}
return s.listWithBuiltins(ctx, workspaceID, builtins)
}
func (s *AppCenterService) listWithBuiltins(ctx context.Context, workspaceID string, builtins []builtinapps.App) ([]workspacebiz.WorkspaceApp, error) {
packages, err := s.Store.ListAppPackages(ctx)
if err != nil {
return nil, err
}
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
return nil, err
}
installationsByAppID := make(map[string]workspacebiz.AppInstallation, len(installations))
for _, installation := range installations {
installationsByAppID[installation.AppID] = installation
}
packages = s.visibleAppPackagesForCatalog(packages, builtins, installationsByAppID)
apps, err := s.resolveWorkspaceAppCatalog(packages, builtins, installationsByAppID, workspaceID)
if err != nil {
return nil, err
}
apps = s.withInstallJobProjections(apps, workspaceID)
return apps, nil
}
func (s *AppCenterService) RefreshCatalog(ctx context.Context, workspaceID string) ([]workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return nil, err
}
if s.BuiltinCatalog == nil {
if _, err := s.refreshBuiltinCatalogAndWait(ctx); err != nil {
return nil, err
}
}
return s.List(ctx, workspaceID)
}
func (s *AppCenterService) Install(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
return s.InstallWithOptions(ctx, workspaceID, appID, InstallOptions{})
}
type InstallOptions struct {
RestartRunning bool
}
type appInstallPackageResolver func(context.Context) (workspacebiz.AppPackage, error)
func (s *AppCenterService) InstallWithOptions(ctx context.Context, workspaceID string, appID string, options InstallOptions) (workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
app, err := s.workspaceAppProjectionForInstall(ctx, workspaceID, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
runtimeProfileHint := s.installRuntimeProfileHint(ctx, appID, app)
s.startInstallJob(workspaceID, appID, options, runtimeProfileHint, func(ctx context.Context) (workspacebiz.AppPackage, error) {
return s.packageForInstall(ctx, appID)
})
return app, nil
}
func (s *AppCenterService) startInstallJob(workspaceID string, appID string, options InstallOptions, runtimeProfileHint string, resolvePackage appInstallPackageResolver) bool {
if !s.beginInstallJob(workspaceID, appID, options) {
return false
}
go s.runInstallJob(workspaceID, appID, runtimeProfileHint, resolvePackage)
return true
}
func (s *AppCenterService) installRuntimeProfileHint(ctx context.Context, appID string, app workspacebiz.WorkspaceApp) string {
remoteBuiltin, ok, err := s.remoteBuiltinForAppID(ctx, appID)
if err == nil && ok && shouldUseRemoteBuiltin(app.Package, remoteBuiltin) {
return appRuntimeProfileForManifest(remoteBuiltin.Manifest)
}
return appRuntimeProfileForPackage(app.Package)
}
func (s *AppCenterService) installPackage(ctx context.Context, workspaceID string, appPackage workspacebiz.AppPackage, options InstallOptions) (workspacebiz.WorkspaceApp, error) {
installation := workspacebiz.AppInstallation{
WorkspaceID: workspaceID,
AppID: appPackage.AppID,
Enabled: true,
}
if err := s.Store.PutWorkspaceAppInstallation(ctx, installation); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
runtimeState, err := s.startPackage(ctx, workspaceID, appPackage, options.RestartRunning)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if err := s.Store.SetActiveAppPackageVersion(ctx, appPackage.AppID, appPackage.Version); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
runtimeState = runtimeStateForActivePackage(runtimeState, appPackage)
if err := s.pruneInactiveAppPackageVersions(ctx, appPackage); err != nil {
slog.Warn(
"workspace app inactive package version prune failed",
"workspaceId", workspaceID,
"appId", appPackage.AppID,
"activeVersion", appPackage.Version,
"error", err,
)
}
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Installation: &installation,
Runtime: runtimeState,
}
app.CLI = s.appCLIState(workspaceID, app)
return s.publishAppIfChanged(ctx, workspaceID, appPackage.AppID, app), nil
}
func (s *AppCenterService) runInstallJob(workspaceID string, appID string, runtimeProfileHint string, resolvePackage appInstallPackageResolver) {
startedAt := time.Now()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
job, ok := s.installJob(workspaceID, appID)
if !ok || job.Status != workspaceAppInstallJobInstalling {
return
}
generation := job.Generation
options := InstallOptions{RestartRunning: job.RestartRunning}
plan := s.buildInstallProgressPlan(ctx, appID)
tracker := s.newInstallProgressTracker(workspaceID, appID, generation, plan)
s.registerActiveInstallTracker(workspaceID, appID, tracker)
defer func() {
s.unregisterActiveInstallTracker(workspaceID, appID, tracker)
tracker.clear()
}()
var appPackage workspacebiz.AppPackage
type installJobResult struct {
appPackage workspacebiz.AppPackage
err error
kind string
}
results := make(chan installJobResult, 2)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
pkg, err := resolvePackage(tracker.packageProgressContext(ctx))
results <- installJobResult{
appPackage: pkg,
err: err,
kind: "package",
}
}()
wg.Add(1)
go func() {
defer wg.Done()
var err error
if appRuntimeProfileIsStandalone(runtimeProfileHint) {
err = nil
} else if strings.TrimSpace(runtimeProfileHint) == workspaceAppNodeRuntimePreloadProfile {
err = s.runner().PreloadRuntimeForProfile(tracker.runtimeProgressContext(ctx), workspaceAppNodeRuntimePreloadProfile)
} else {
err = s.runner().PreloadRuntime(tracker.runtimeProgressContext(ctx))
}
results <- installJobResult{
err: err,
kind: "runtime",
}
}()
for completed := 0; completed < 2; completed += 1 {
result := <-results
if result.kind == "package" {
appPackage = result.appPackage
}
if result.err != nil {
cancel()
wg.Wait()
s.handleInstallJobFailure(context.Background(), workspaceID, appID, generation, appPackage, result.err, startedAt)
return
}
}
wg.Wait()
if err := ctx.Err(); err != nil {
s.handleInstallJobFailure(ctx, workspaceID, appID, generation, appPackage, err, startedAt)
return
}
tracker.beginInstalling()
if _, err := s.installPackage(ctx, workspaceID, appPackage, options); err != nil {
s.handleInstallJobFailure(ctx, workspaceID, appID, generation, appPackage, err, startedAt)
return
}
tracker.finishInstalling()
tracker.finishStarting()
s.completeInstallJob(workspaceID, appID, generation)
slog.Info("workspace_app_install_job_succeeded", "workspaceId", workspaceID, "appId", appID, "packageSource", appPackage.Source, "version", appPackage.Version, "packageDir", appPackage.PackageDir, "durationMs", time.Since(startedAt).Milliseconds())
}
func (s *AppCenterService) completeInstallJob(workspaceID string, appID string, generation uint64) bool {
unlock := s.installPublishLocks.Lock(appRuntimeKey(workspaceID, appID))
defer unlock()
job, ok := s.installJob(workspaceID, appID)
if !ok || job.Generation != generation || job.Status != workspaceAppInstallJobInstalling {
return false
}
s.clearInstallProgressLocked(workspaceID, appID, generation)
return s.finishInstallJob(workspaceID, appID, generation)
}
func (s *AppCenterService) handleInstallJobFailure(ctx context.Context, workspaceID string, appID string, generation uint64, appPackage workspacebiz.AppPackage, err error, startedAt time.Time) {
unlock := s.installPublishLocks.Lock(appRuntimeKey(workspaceID, appID))
defer unlock()
slog.Warn("workspace_app_install_job_failed", "workspaceId", workspaceID, "appId", appID, "packageSource", appPackage.Source, "version", appPackage.Version, "packageDir", appPackage.PackageDir, "failureReason", err.Error(), "durationMs", time.Since(startedAt).Milliseconds(), "error", err)
if !s.failInstallJob(workspaceID, appID, generation, err) {
return
}
// Stop accepting tracker callbacks and remove any transient progress before
// publishing the failure terminal. The deferred tracker cleanup must never
// be able to publish a newer idle/running projection over this failure.
s.clearInstallProgressLocked(workspaceID, appID, generation)
if failedApp, projectionErr := s.failedInstallAppProjection(ctx, workspaceID, appID, err); projectionErr == nil {
_ = s.publishAppIfChanged(ctx, workspaceID, appID, failedApp)
} else {
slog.Warn("workspace app install failure projection failed", "workspaceId", workspaceID, "appId", appID, "error", projectionErr)
}
}
func (s *AppCenterService) packageForInstall(ctx context.Context, appID string) (workspacebiz.AppPackage, error) {
appPackage, err := s.Store.GetAppPackage(ctx, appID)
if err != nil {
if !errors.Is(err, workspacedata.ErrWorkspaceAppNotFound) {
return workspacebiz.AppPackage{}, err
}
return s.materializeRemoteBuiltinPackage(ctx, appID)
}
remoteBuiltin, ok, err := s.remoteBuiltinForAppID(ctx, appID)
if err != nil {
return appPackage, nil
}
if ok && s.shouldMaterializeRemoteBuiltin(appPackage, remoteBuiltin) {
return s.downloadRemoteBuiltinPackage(ctx, remoteBuiltin)
}
return appPackage, nil
}
func (s *AppCenterService) Add(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
return s.Install(ctx, workspaceID, appID)
}
func (s *AppCenterService) Uninstall(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
appPackage, _, err := s.installedPackage(ctx, workspaceID, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if _, err := s.runner().Stop(ctx, workspaceID, appPackage.AppID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
s.deactivateAppCLI(workspaceID, appPackage.AppID)
if err := s.removeWorkspaceAppStateRoot(workspaceID, appPackage.AppID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
deletePackage, err := s.shouldDeleteRemoteBuiltinPackageAfterUninstall(ctx, workspaceID, appPackage)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if deletePackage {
if err := s.deleteRemoteBuiltinPackageFilesAndRecord(ctx, appPackage); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
} else {
if err := s.Store.DeleteWorkspaceAppInstallation(ctx, workspaceID, appPackage.AppID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
}
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Runtime: workspacebiz.AppRuntimeState{
Status: workspacebiz.AppRuntimeStatusIdle,
},
}
app.CLI = s.appCLIState(workspaceID, app)
return s.publishAppIfChanged(ctx, workspaceID, appPackage.AppID, app), nil
}
func (s *AppCenterService) Remove(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
return s.Uninstall(ctx, workspaceID, appID)
}
func (s *AppCenterService) Rollback(ctx context.Context, workspaceID string, appID string, version string) (workspacebiz.WorkspaceApp, error) {
if _, err := s.workspaceSummary(ctx, workspaceID); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if strings.TrimSpace(appID) == "" || strings.TrimSpace(version) == "" {
return workspacebiz.WorkspaceApp{}, errors.New("workspace app id and version are required")
}
if err := s.Store.SetActiveAppPackageVersion(ctx, appID, version); err != nil {
return workspacebiz.WorkspaceApp{}, err
}
appPackage, err := s.Store.GetAppPackage(ctx, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
var installationPtr *workspacebiz.AppInstallation
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
for _, installation := range installations {
if installation.AppID == appPackage.AppID {
installationCopy := installation
installationPtr = &installationCopy
break
}
}
runtimeState := workspacebiz.AppRuntimeState{Status: workspacebiz.AppRuntimeStatusIdle}
if installationPtr != nil {
_, _ = s.runner().Stop(ctx, workspaceID, appPackage.AppID)
started, err := s.startPackage(ctx, workspaceID, appPackage, true)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
runtimeState = started
}
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Installation: installationPtr,
Runtime: runtimeState,
}
app.CLI = s.appCLIState(workspaceID, app)
return s.publishAppIfChanged(ctx, workspaceID, appPackage.AppID, app), nil
}
func (s *AppCenterService) workspaceSummary(ctx context.Context, workspaceID string) (workspacebiz.Summary, error) {
if s.Store == nil {
return workspacebiz.Summary{}, errors.New("workspace app store is not configured")
}
if s.WorkspaceStore == nil {
return workspacebiz.Summary{}, errors.New("workspace catalog store is not configured")
}
workspaceID = strings.TrimSpace(workspaceID)
if workspaceID == "" {
return workspacebiz.Summary{}, errors.New("workspace id is required")
}
return s.WorkspaceStore.Get(ctx, workspaceID)
}
func (s *AppCenterService) runner() *AppRunner {
s.runnerMu.Lock()
defer s.runnerMu.Unlock()
if s.Runner == nil {
s.Runner = &AppRunner{}
}
if s.Runner.OnStateChanged == nil {
s.Runner.OnStateChanged = s.handleRunnerStateChanged
}
return s.Runner
}
func (s *AppCenterService) handleRunnerStateChanged(workspaceID string, appID string, state workspacebiz.AppRuntimeState) {
if s.Store == nil {
return
}
s.syncInstallProgressFromRuntimeStatus(workspaceID, appID, state.Status)
ctx := context.Background()
appPackage, err := s.Store.GetAppPackage(ctx, appID)
if err != nil {
slog.Warn("workspace app runtime state changed for unknown package", "workspaceId", workspaceID, "appId", appID, "error", err)
return
}
var installationPtr *workspacebiz.AppInstallation
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
slog.Warn("workspace app runtime state installation lookup failed", "workspaceId", workspaceID, "appId", appID, "error", err)
return
}
for _, installation := range installations {
if installation.AppID == appPackage.AppID {
installationCopy := installation
installationPtr = &installationCopy
break
}
}
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Installation: installationPtr,
Runtime: runtimeStateForActivePackage(state, appPackage),
}
if state.Status == workspacebiz.AppRuntimeStatusRunning && state.LaunchURL != nil && installationPtr != nil && installationPtr.Enabled {
app.CLI = s.activateAppCLI(ctx, workspaceID, appPackage, *state.LaunchURL)
} else {
app.CLI = s.appCLIState(workspaceID, app)
}
_ = s.publishAppIfChanged(ctx, workspaceID, appPackage.AppID, app)
}
func (s *AppCenterService) publishAppIfChanged(ctx context.Context, workspaceID string, appID string, app workspacebiz.WorkspaceApp) workspacebiz.WorkspaceApp {
app = s.withActiveInstallJobProgress(app, workspaceID, appID)
app, changed := s.withChangedRevision(app, workspaceID, appID)
if !changed {
return app
}
if s.Publisher == nil {
return app
}
if err := s.Publisher.PublishWorkspaceAppUpdated(ctx, workspaceID, app); err != nil {
slog.Warn("workspace app updated event publish failed", "workspaceId", workspaceID, "appId", appID, "stateRevision", app.StateRevision, "error", err)
}
return app
}
func (s *AppCenterService) withChangedRevision(app workspacebiz.WorkspaceApp, workspaceID string, appID string) (workspacebiz.WorkspaceApp, bool) {
key := appRuntimeKey(workspaceID, appID)
projection := projectionKeyFromWorkspaceApp(app)
s.mu.Lock()
defer s.mu.Unlock()
s.ensureRevisionStateLocked()
if previous, ok := s.appProjectionKeys[key]; ok {
staleInstallProgress := projection.hasInstallProgress &&
previous.hasUpdatedAt &&
(!projection.hasUpdatedAt || projection.updatedAtUnixMs < previous.updatedAtUnixMs) &&
(previous.status == workspacebiz.AppRuntimeStatusFailed ||
previous.status == workspacebiz.AppRuntimeStatusStopping ||
previous.status == workspacebiz.AppRuntimeStatusRunning ||
previous.status == workspacebiz.AppRuntimeStatusInstalledPendingRestart)
if previous == projection ||
staleInstallProgress {
app.StateRevision = s.stateRevisions[key]
return app, false
}
}
s.stateRevisions[key] += 1
s.appProjectionKeys[key] = projection
app.StateRevision = s.stateRevisions[key]
return app, true
}
func (s *AppCenterService) withCurrentRevision(app workspacebiz.WorkspaceApp, workspaceID string, appID string) workspacebiz.WorkspaceApp {
key := appRuntimeKey(workspaceID, appID)
s.mu.Lock()
defer s.mu.Unlock()
s.ensureRevisionStateLocked()
app.StateRevision = s.stateRevisions[key]
return app
}
func (s *AppCenterService) materializeRemoteBuiltinPackage(ctx context.Context, appID string) (workspacebiz.AppPackage, error) {
builtins, err := s.builtinCatalog(ctx)
if err != nil {
return workspacebiz.AppPackage{}, err
}
for _, builtin := range builtins {
if builtin.Manifest.AppID != appID || builtin.Distribution.Kind != builtinapps.DistributionRemote {
continue
}
return s.downloadRemoteBuiltinPackage(ctx, builtin)
}
return workspacebiz.AppPackage{}, workspacedata.ErrWorkspaceAppNotFound
}
func (s *AppCenterService) remoteBuiltinForAppID(ctx context.Context, appID string) (builtinapps.App, bool, error) {
builtins, err := s.builtinCatalog(ctx)
if err != nil {
return builtinapps.App{}, false, err
}
for _, builtin := range builtins {
if builtin.Manifest.AppID == appID && builtin.Distribution.Kind == builtinapps.DistributionRemote {
return builtin, true, nil
}
}
return builtinapps.App{}, false, nil
}
func (s *AppCenterService) remoteBuiltinWorkspaceApp(builtin builtinapps.App, workspaceID string) (workspacebiz.WorkspaceApp, error) {
app, err := remoteBuiltinWorkspaceApp(builtin)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
return s.withCurrentRevision(app, workspaceID, app.Package.AppID), nil
}
func (s *AppCenterService) workspaceAppProjectionForInstall(ctx context.Context, workspaceID string, appID string) (workspacebiz.WorkspaceApp, error) {
appPackage, packageErr := s.Store.GetAppPackage(ctx, appID)
remoteBuiltin, hasRemoteBuiltin, remoteErr := s.remoteBuiltinForAppID(ctx, appID)
if packageErr != nil {
if !errors.Is(packageErr, workspacedata.ErrWorkspaceAppNotFound) {
return workspacebiz.WorkspaceApp{}, packageErr
}
if remoteErr != nil {
return workspacebiz.WorkspaceApp{}, remoteErr
}
if hasRemoteBuiltin {
return s.remoteBuiltinWorkspaceApp(remoteBuiltin, workspaceID)
}
return workspacebiz.WorkspaceApp{}, workspacedata.ErrWorkspaceAppNotFound
}
installation, installed, err := s.workspaceAppInstallation(ctx, workspaceID, appID)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if remoteErr == nil && hasRemoteBuiltin && shouldUseRemoteBuiltin(appPackage, remoteBuiltin) {
if installed {
return workspaceAppWithRemoteBuiltinUpdate(
s.workspaceAppFromPackage(appPackage, installation, installed, workspaceID),
remoteBuiltin,
)
}
return s.remoteBuiltinWorkspaceApp(remoteBuiltin, workspaceID)
}
return s.workspaceAppFromPackage(appPackage, installation, installed, workspaceID), nil
}
func (s *AppCenterService) workspaceAppInstallation(ctx context.Context, workspaceID string, appID string) (workspacebiz.AppInstallation, bool, error) {
installations, err := s.Store.ListWorkspaceAppInstallations(ctx, workspaceID)
if err != nil {
return workspacebiz.AppInstallation{}, false, err
}
for _, installation := range installations {
if installation.AppID == appID {
return installation, true, nil
}
}
return workspacebiz.AppInstallation{}, false, nil
}
func (s *AppCenterService) resolveWorkspaceAppCatalog(packages []workspacebiz.AppPackage, builtins []builtinapps.App, installationsByAppID map[string]workspacebiz.AppInstallation, workspaceID string) ([]workspacebiz.WorkspaceApp, error) {
packagesByAppID := make(map[string]workspacebiz.AppPackage, len(packages))
for _, appPackage := range packages {
packagesByAppID[appPackage.AppID] = appPackage
}
apps := make([]workspacebiz.WorkspaceApp, 0, len(packages)+len(builtins))
emittedAppIDs := make(map[string]struct{}, len(packages)+len(builtins))
for _, builtin := range builtins {
if builtin.Distribution.Kind != builtinapps.DistributionRemote {
continue
}
appID := builtin.Manifest.AppID
installation, installed := installationsByAppID[appID]
if appPackage, ok := packagesByAppID[appID]; ok && !shouldDisplayRemoteBuiltinCatalog(appPackage, builtin, installed) {
app, err := workspaceAppWithRemoteBuiltinUpdate(
s.workspaceAppFromPackage(appPackage, installation, installed, workspaceID),
builtin,
)
if err != nil {
return nil, err
}
apps = append(apps, app)
} else {
app, err := s.remoteBuiltinWorkspaceApp(builtin, workspaceID)
if err != nil {
return nil, err
}
apps = append(apps, app)
}
emittedAppIDs[appID] = struct{}{}
}
for _, appPackage := range packages {
if _, ok := emittedAppIDs[appPackage.AppID]; ok {
continue
}
installation, installed := installationsByAppID[appPackage.AppID]
apps = append(apps, s.workspaceAppFromPackage(appPackage, installation, installed, workspaceID))
}
return apps, nil
}
func (s *AppCenterService) workspaceAppFromPackage(appPackage workspacebiz.AppPackage, installation workspacebiz.AppInstallation, installed bool, workspaceID string) workspacebiz.WorkspaceApp {
app := workspacebiz.WorkspaceApp{
Package: appPackage,
Runtime: workspacebiz.AppRuntimeState{
Status: workspacebiz.AppRuntimeStatusIdle,
},
}
if installed {
installationCopy := installation
app.Installation = &installationCopy
app.Runtime = s.runner().State(workspaceID, appPackage.AppID)
app.Runtime = runtimeStateForActivePackage(app.Runtime, appPackage)
}
app.CLI = s.appCLIState(workspaceID, app)
return s.withCurrentRevision(app, workspaceID, appPackage.AppID)
}
func runtimeStateForActivePackage(state workspacebiz.AppRuntimeState, appPackage workspacebiz.AppPackage) workspacebiz.AppRuntimeState {
if state.Status != workspacebiz.AppRuntimeStatusRunning {
return state
}
if strings.TrimSpace(state.PackageDir) == "" || strings.TrimSpace(appPackage.PackageDir) == "" {
return state
}
if filepath.Clean(state.PackageDir) == filepath.Clean(appPackage.PackageDir) {
return state
}
state.Status = workspacebiz.AppRuntimeStatusInstalledPendingRestart
return state
}
func shouldDisplayRemoteBuiltinCatalog(appPackage workspacebiz.AppPackage, builtin builtinapps.App, installed bool) bool {
return !installed && shouldUseRemoteBuiltin(appPackage, builtin)
}
func workspaceAppWithRemoteBuiltinUpdate(app workspacebiz.WorkspaceApp, builtin builtinapps.App) (workspacebiz.WorkspaceApp, error) {
catalogIconURL, err := remoteBuiltinIconURL(builtin)
if err != nil {
return workspacebiz.WorkspaceApp{}, err
}
if app.Installation == nil {
app.IconURL = catalogIconURL
}
if !shouldUseRemoteBuiltin(app.Package, builtin) {
return app, nil
}
availableVersion := builtin.Manifest.Version
app.AvailableVersion = &availableVersion
app.AvailableIconURL = catalogIconURL
app.UpdateAvailable = true
return app, nil
}