Ctrl K
ComunidadGuías…

bldr/desktop/tray/desktop-tray.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)