package web import ( "net/http" "path/filepath" "runtime" "strings" ) const ( resourceInstallerMutation = "installer:mutation" resourceManagerConfig = "config:manager" resourceMPVConfig = "config:mpv" resourceManagerBinary = "install:manager-binary" resourceKeyring = "keyring:sudo-password" ) // installerOperationResources deliberately includes conservative shared // resources as well as exact identities. This prevents unlike method IDs from // overlapping on manager state, an MPV config/UI tree, an install target, or a // package-manager database while retaining a path toward finer concurrency. func installerOperationResources(methodID, appID, installPath, configDir string) []string { resources := []string{ resourceInstallerMutation, resourceManagerConfig, resourceMPVConfig, resourceKeyring, "method:" + methodID, } if appID != "" { resources = append(resources, "app:"+appID) } if installPath != "" { resources = append(resources, "install-path:"+normalizedLeasePath(installPath)) } if configDir != "" { resources = append(resources, "config-tree:"+normalizedLeasePath(configDir)) } if packageResource := packageManagerResource(methodID); packageResource != "" { resources = append(resources, packageResource) } return resources } func packageManagerResource(methodID string) string { lower := strings.ToLower(methodID) switch { case strings.Contains(lower, "flatpak"): return "package-manager:flatpak" case strings.Contains(lower, "snap"): return "package-manager:snap" case strings.Contains(lower, "brew"): return "package-manager:brew" case strings.Contains(lower, "package"): return "package-manager:system" default: return "" } } func normalizedLeasePath(path string) string { if absolute, err := filepath.Abs(path); err == nil { path = absolute } path = filepath.Clean(path) if runtime.GOOS == "windows" { path = strings.ToLower(path) } return path } // withOperationLease includes synchronous config/UI mutations in the same // coordinator as background jobs. Read-only methods do not acquire leases. func (s *Server) withOperationLease(handler http.HandlerFunc, resources ...string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions { handler(w, r) return } owner := "request:" + newJobID() release, ok := s.jobManager.TryAcquireResources(owner, resources...) if !ok { sendJSONError(w, http.StatusConflict, "A conflicting install or configuration operation is already in progress") return } defer release() handler(w, r) } }