package desktop_tray import ( "context" "sort" "strings" "github.com/aperturerobotics/starpc/srpc" "github.com/aperturerobotics/util/broadcast" "github.com/s4wave/spacewave/bldr/resource" resource_server "github.com/s4wave/spacewave/bldr/resource/server" ) type desktopTrayRegistration struct { resourceID uint32 entry *DesktopTrayEntry attachedActionResourceID uint32 client resource_server.ResourceClientContext } // DesktopTray is an in-memory resource-backed desktop tray registry. type DesktopTray struct { mux srpc.Invoker bcast broadcast.Broadcast registrations map[uint32]*desktopTrayRegistration } // NewDesktopTray creates a new DesktopTray. func NewDesktopTray() *DesktopTray { // Construct the tray and register its RPC service on a new mux. r := &DesktopTray{ registrations: make(map[uint32]*desktopTrayRegistration), } mux := srpc.NewMux() _ = SRPCRegisterDesktopTrayResourceService(mux, r) r.mux = mux return r } // GetMux returns the rpc mux. func (r *DesktopTray) GetMux() srpc.Invoker { return r.mux } // RegisterDesktopTrayEntry registers one resource-backed tray entry. func (r *DesktopTray) RegisterDesktopTrayEntry( ctx context.Context, req *RegisterDesktopTrayEntryRequest, ) (*RegisterDesktopTrayEntryResponse, error) { // Validate the entry reference and its identifier. entry := req.GetEntry() if entry == nil { return nil, ErrDesktopTrayEntryRequired } if entry.GetId() == "" { return nil, ErrDesktopTrayEntryIdRequired } // Resolve the registering resource client context. client, err := resource_server.MustGetResourceClientContext(ctx) if err != nil { return nil, err } // Reject an entry id that is already registered. var duplicate bool r.bcast.HoldLock(func(_ func(), _ func() <-chan struct{}) { duplicate = r.hasEntryIDLocked(entry.GetId(), 0) }) if duplicate { return nil, ErrDesktopTrayEntryDuplicate } // Construct the entry resource and its registration record. entryResource := NewDesktopTrayEntryResource(r) reg := &desktopTrayRegistration{ entry: entry.CloneVT(), attachedActionResourceID: req.GetAttachedActionResourceId(), client: client, } // Publish the entry resource and release its registration on release. var released bool var resourceID uint32 resourceID, err = client.AddResourceValue(entryResource.GetMux(), entryResource, func() { r.bcast.HoldLock(func(broadcast func(), _ func() <-chan struct{}) { released = true if _, ok := r.registrations[resourceID]; !ok { return } delete(r.registrations, resourceID) broadcast() }) }) if err != nil { return nil, err } // Record the resource on the entry and register it unless it raced. entryResource.SetResourceID(resourceID) reg.resourceID = resourceID var releasedBeforeRegistration bool duplicate = false r.bcast.HoldLock(func(broadcast func(), _ func() <-chan struct{}) { if released { releasedBeforeRegistration = true return } if r.hasEntryIDLocked(entry.GetId(), 0) { duplicate = true return } r.registrations[resourceID] = reg broadcast() }) if duplicate { client.ReleaseResource(resourceID) return nil, ErrDesktopTrayEntryDuplicate } if releasedBeforeRegistration { return nil, resource.ErrClientReleased } return &RegisterDesktopTrayEntryResponse{ ResourceId: resourceID, }, nil } // WatchDesktopTray streams the full ordered tray tree. func (r *DesktopTray) WatchDesktopTray( req *WatchDesktopTrayRequest, strm SRPCDesktopTrayResourceService_WatchDesktopTrayStream, ) error { ctx := strm.Context() // Snapshot the tray state under the lock and wait for the next change. for { var state *DesktopTrayState var waitCh <-chan struct{} r.bcast.HoldLock(func(_ func(), getWaitCh func() <-chan struct{}) { state = r.snapshotLocked() waitCh = getWaitCh() }) // Send the snapshot outside the lock and wait for change or cancel. if err := strm.Send(&WatchDesktopTrayResponse{ State: state, }); err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case <-waitCh: } } } // InvokeDesktopTrayEntry invokes an action entry by id. func (r *DesktopTray) InvokeDesktopTrayEntry( ctx context.Context, req *InvokeDesktopTrayEntryRequest, ) (*InvokeDesktopTrayEntryResponse, error) { // Validate the requested entry identifier. entryID := req.GetEntryId() if entryID == "" { return nil, ErrDesktopTrayEntryIdRequired } // Find the registration with the requested entry id. var reg *desktopTrayRegistration r.bcast.HoldLock(func(_ func(), _ func() <-chan struct{}) { for _, candidate := range r.registrations { if candidate == nil || candidate.entry == nil { continue } if candidate.entry.GetId() == entryID { reg = candidate return } } }) if reg == nil { return nil, ErrDesktopTrayEntryNotFound } // Reject an entry that is not an enabled attached-handler action. entry := reg.entry.CloneVT() if entry.GetKind() != DesktopTrayEntryKind_DESKTOP_TRAY_ENTRY_KIND_ACTION || !entry.GetEnabled() { return nil, ErrDesktopTrayEntryNotInvokable } action := entry.GetAction() if action == nil || action.GetKind() != DesktopTrayActionKind_DESKTOP_TRAY_ACTION_KIND_ATTACHED_HANDLER { return nil, ErrDesktopTrayEntryNotInvokable } if reg.attachedActionResourceID == 0 { return nil, ErrDesktopTrayActionHandlerRequired } // Forward the action to the attached handler resource. client, err := reg.client.GetAttachedResource(reg.attachedActionResourceID) if err != nil { return nil, err } handler := NewSRPCDesktopTrayActionHandlerServiceClient(client) _, err = handler.HandleDesktopTrayAction(ctx, &HandleDesktopTrayActionRequest{ EntryId: entry.GetId(), Action: action, }) if err != nil { return nil, err } return &InvokeDesktopTrayEntryResponse{}, nil } func (r *DesktopTray) setEntry(resourceID uint32, entry *DesktopTrayEntry) error { // Replace the registration's entry and reject duplicate ids. var found bool var duplicate bool r.bcast.HoldLock(func(broadcast func(), _ func() <-chan struct{}) { // Replace the entry only when the registration still exists. reg := r.registrations[resourceID] if reg == nil { return } if r.hasEntryIDLocked(entry.GetId(), resourceID) { duplicate = true return } reg.entry = entry.CloneVT() found = true broadcast() }) if duplicate { return ErrDesktopTrayEntryDuplicate } if !found { return ErrDesktopTrayEntryNotFound } return nil } func (r *DesktopTray) setActive(resourceID uint32, active bool) error { // Set the registration's active flag and broadcast a change. var found bool r.bcast.HoldLock(func(broadcast func(), _ func() <-chan struct{}) { // Update the flag only when the registration still exists. reg := r.registrations[resourceID] if reg == nil { return } if reg.entry.GetActive() == active { found = true return } reg.entry.Active = active found = true broadcast() }) if !found { return ErrDesktopTrayEntryNotFound } return nil } func (r *DesktopTray) setEnabled(resourceID uint32, enabled bool) error { // Set the registration's enabled flag and broadcast a change. var found bool r.bcast.HoldLock(func(broadcast func(), _ func() <-chan struct{}) { // Update the flag only when the registration still exists. reg := r.registrations[resourceID] if reg == nil { return } if reg.entry.GetEnabled() == enabled { found = true return } reg.entry.Enabled = enabled found = true broadcast() }) if !found { return ErrDesktopTrayEntryNotFound } return nil } func (r *DesktopTray) snapshot() *DesktopTrayState { // Snapshot the tray state under the broadcast lock. var state *DesktopTrayState r.bcast.HoldLock(func(_ func(), _ func() <-chan struct{}) { state = r.snapshotLocked() }) return state } func (r *DesktopTray) snapshotLocked() *DesktopTrayState { // Collect the valid registrations in deterministic order. regs := make([]*desktopTrayRegistration, 0, len(r.registrations)) for _, reg := range r.registrations { if reg == nil || reg.entry == nil { continue } regs = append(regs, reg) } // Order entries by path, group, order, id, then resource id. sort.Slice(regs, func(i, j int) bool { // Compare the entries by their joined path first. left := regs[i].entry right := regs[j].entry leftPath := strings.Join(left.GetPath(), "\x00") rightPath := strings.Join(right.GetPath(), "\x00") if leftPath != rightPath { return leftPath < rightPath } // Break ties by group, order, id, then resource id. if left.GetGroup() != right.GetGroup() { return left.GetGroup() < right.GetGroup() } if left.GetOrder() != right.GetOrder() { return left.GetOrder() < right.GetOrder() } if left.GetId() != right.GetId() { return left.GetId() < right.GetId() } return regs[i].resourceID < regs[j].resourceID }) // Clone the entries and fold them into the tray icon state. entries := make([]*DesktopTrayEntry, 0, len(regs)) iconState := DesktopTrayIconState_DESKTOP_TRAY_ICON_STATE_NORMAL var statusText string for _, reg := range regs { entry := reg.entry.CloneVT() entries = append(entries, entry) if entry.GetIconState() > iconState { iconState = entry.GetIconState() } if statusText == "" && entry.GetId() == "title" { statusText = desktopTrayTitleStatusText(entry.GetLabel()) } } return &DesktopTrayState{ Entries: entries, IconState: iconState, StatusText: statusText, } } func desktopTrayTitleStatusText(label string) string { return strings.TrimPrefix(label, "Spacewave: ") } func (r *DesktopTray) hasEntryIDLocked(entryID string, exceptResourceID uint32) bool { // Report whether another registration already uses the entry id. for resourceID, reg := range r.registrations { if resourceID == exceptResourceID || reg == nil || reg.entry == nil { continue } if reg.entry.GetId() == entryID { return true } } return false } // _ is a type assertion var _ SRPCDesktopTrayResourceServiceServer = (*DesktopTray)(nil)