mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
- Add printers - Delete printers - Use polkit APIs as fallback on auth errors - Fix ref system to conditionally subscribe to cups when wanted
391 lines
7.5 KiB
Go
391 lines
7.5 KiB
Go
package cups
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
|
)
|
|
|
|
func NewManager() (*Manager, error) {
|
|
host := os.Getenv("DMS_IPP_HOST")
|
|
if host == "" {
|
|
host = "localhost"
|
|
}
|
|
|
|
portStr := os.Getenv("DMS_IPP_PORT")
|
|
port := 631
|
|
if portStr != "" {
|
|
if p, err := strconv.Atoi(portStr); err == nil {
|
|
port = p
|
|
}
|
|
}
|
|
|
|
username := os.Getenv("DMS_IPP_USERNAME")
|
|
password := os.Getenv("DMS_IPP_PASSWORD")
|
|
|
|
client := ipp.NewCUPSClient(host, port, username, password, false)
|
|
baseURL := fmt.Sprintf("http://%s:%d", host, port)
|
|
|
|
var pkHelper PkHelper
|
|
if isLocalCUPS(host) {
|
|
var err error
|
|
pkHelper, err = NewPkHelper()
|
|
if err != nil {
|
|
log.Warnf("[CUPS] Failed to initialize pkhelper: %v", err)
|
|
}
|
|
}
|
|
|
|
m := &Manager{
|
|
state: &CUPSState{
|
|
Printers: make(map[string]*Printer),
|
|
},
|
|
client: client,
|
|
pkHelper: pkHelper,
|
|
baseURL: baseURL,
|
|
stateMutex: sync.RWMutex{},
|
|
stopChan: make(chan struct{}),
|
|
dirty: make(chan struct{}, 1),
|
|
}
|
|
|
|
if err := m.updateState(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isLocalCUPS(host) {
|
|
m.subscription = NewDBusSubscriptionManager(client, baseURL)
|
|
log.Infof("[CUPS] Using D-Bus notifications for local CUPS")
|
|
} else {
|
|
m.subscription = NewSubscriptionManager(client, baseURL)
|
|
log.Infof("[CUPS] Using IPPGET notifications for remote CUPS")
|
|
}
|
|
|
|
m.notifierWg.Add(1)
|
|
go m.notifier()
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func isLocalCUPS(host string) bool {
|
|
switch host {
|
|
case "localhost", "127.0.0.1", "::1", "":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Manager) eventHandler() {
|
|
defer m.eventWG.Done()
|
|
|
|
if m.subscription == nil {
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-m.stopChan:
|
|
return
|
|
case event, ok := <-m.subscription.Events():
|
|
if !ok {
|
|
return
|
|
}
|
|
log.Debugf("[CUPS] Received event: %s (printer: %s, job: %d)",
|
|
event.EventName, event.PrinterName, event.JobID)
|
|
|
|
if err := m.updateState(); err != nil {
|
|
log.Warnf("[CUPS] Failed to update state after event: %v", err)
|
|
} else {
|
|
m.notifySubscribers()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) updateState() error {
|
|
printers, err := m.GetPrinters()
|
|
if err != nil {
|
|
if isNoPrintersError(err) {
|
|
m.stateMutex.Lock()
|
|
m.state.Printers = make(map[string]*Printer)
|
|
m.stateMutex.Unlock()
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
printerMap := make(map[string]*Printer, len(printers))
|
|
for _, printer := range printers {
|
|
jobs, err := m.GetJobs(printer.Name, "not-completed")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.Jobs = jobs
|
|
printerMap[printer.Name] = &printer
|
|
}
|
|
|
|
m.stateMutex.Lock()
|
|
m.state.Printers = printerMap
|
|
m.stateMutex.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func isNoPrintersError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
var ippErr ipp.IPPError
|
|
if errors.As(err, &ippErr) {
|
|
return ippErr.Status == 1030
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (m *Manager) notifier() {
|
|
defer m.notifierWg.Done()
|
|
const minGap = 100 * time.Millisecond
|
|
timer := time.NewTimer(minGap)
|
|
timer.Stop()
|
|
var pending bool
|
|
for {
|
|
select {
|
|
case <-m.stopChan:
|
|
timer.Stop()
|
|
return
|
|
case <-m.dirty:
|
|
if pending {
|
|
continue
|
|
}
|
|
pending = true
|
|
timer.Reset(minGap)
|
|
case <-timer.C:
|
|
if !pending {
|
|
continue
|
|
}
|
|
|
|
currentState := m.snapshotState()
|
|
|
|
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
|
pending = false
|
|
continue
|
|
}
|
|
|
|
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
|
select {
|
|
case ch <- currentState:
|
|
default:
|
|
}
|
|
return true
|
|
})
|
|
|
|
stateCopy := currentState
|
|
m.lastNotifiedState = &stateCopy
|
|
pending = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) notifySubscribers() {
|
|
select {
|
|
case m.dirty <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *Manager) RefreshState() {
|
|
if err := m.updateState(); err != nil {
|
|
log.Warnf("[CUPS] Failed to refresh state: %v", err)
|
|
return
|
|
}
|
|
m.notifySubscribers()
|
|
}
|
|
|
|
func (m *Manager) GetState() CUPSState {
|
|
return m.snapshotState()
|
|
}
|
|
|
|
func (m *Manager) snapshotState() CUPSState {
|
|
m.stateMutex.RLock()
|
|
defer m.stateMutex.RUnlock()
|
|
|
|
s := CUPSState{
|
|
Printers: make(map[string]*Printer, len(m.state.Printers)),
|
|
}
|
|
for name, printer := range m.state.Printers {
|
|
printerCopy := *printer
|
|
s.Printers[name] = &printerCopy
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (m *Manager) Subscribe(id string) chan CUPSState {
|
|
ch := make(chan CUPSState, 64)
|
|
|
|
wasEmpty := true
|
|
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
|
wasEmpty = false
|
|
return false
|
|
})
|
|
|
|
m.subscribers.Store(id, ch)
|
|
|
|
if wasEmpty && m.subscription != nil {
|
|
if err := m.subscription.Start(); err != nil {
|
|
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
|
} else {
|
|
m.eventWG.Add(1)
|
|
go m.eventHandler()
|
|
}
|
|
}
|
|
|
|
return ch
|
|
}
|
|
|
|
func (m *Manager) Unsubscribe(id string) {
|
|
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
|
close(val)
|
|
}
|
|
|
|
isEmpty := true
|
|
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
|
isEmpty = false
|
|
return false
|
|
})
|
|
|
|
if isEmpty && m.subscription != nil {
|
|
m.subscription.Stop()
|
|
m.eventWG.Wait()
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Close() {
|
|
close(m.stopChan)
|
|
|
|
if m.subscription != nil {
|
|
m.subscription.Stop()
|
|
}
|
|
|
|
m.eventWG.Wait()
|
|
m.notifierWg.Wait()
|
|
|
|
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
|
close(ch)
|
|
m.subscribers.Delete(key)
|
|
return true
|
|
})
|
|
}
|
|
|
|
func stateChanged(old, new *CUPSState) bool {
|
|
if len(old.Printers) != len(new.Printers) {
|
|
return true
|
|
}
|
|
for name, oldPrinter := range old.Printers {
|
|
newPrinter, exists := new.Printers[name]
|
|
if !exists {
|
|
return true
|
|
}
|
|
if oldPrinter.State != newPrinter.State ||
|
|
oldPrinter.StateReason != newPrinter.StateReason ||
|
|
oldPrinter.Accepting != newPrinter.Accepting ||
|
|
len(oldPrinter.Jobs) != len(newPrinter.Jobs) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parsePrinterState(attrs ipp.Attributes) string {
|
|
if stateAttr, ok := attrs[ipp.AttributePrinterState]; ok && len(stateAttr) > 0 {
|
|
if state, ok := stateAttr[0].Value.(int); ok {
|
|
switch state {
|
|
case 3:
|
|
return "idle"
|
|
case 4:
|
|
return "processing"
|
|
case 5:
|
|
return "stopped"
|
|
default:
|
|
return fmt.Sprintf("%d", state)
|
|
}
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func parseJobState(attrs ipp.Attributes) string {
|
|
if stateAttr, ok := attrs[ipp.AttributeJobState]; ok && len(stateAttr) > 0 {
|
|
if state, ok := stateAttr[0].Value.(int); ok {
|
|
switch state {
|
|
case 3:
|
|
return "pending"
|
|
case 4:
|
|
return "pending-held"
|
|
case 5:
|
|
return "processing"
|
|
case 6:
|
|
return "processing-stopped"
|
|
case 7:
|
|
return "canceled"
|
|
case 8:
|
|
return "aborted"
|
|
case 9:
|
|
return "completed"
|
|
default:
|
|
return fmt.Sprintf("%d", state)
|
|
}
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func getStringAttr(attrs ipp.Attributes, key string) string {
|
|
if attr, ok := attrs[key]; ok && len(attr) > 0 {
|
|
if val, ok := attr[0].Value.(string); ok {
|
|
return val
|
|
}
|
|
return fmt.Sprintf("%v", attr[0].Value)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getIntAttr(attrs ipp.Attributes, key string) int {
|
|
if attr, ok := attrs[key]; ok && len(attr) > 0 {
|
|
if val, ok := attr[0].Value.(int); ok {
|
|
return val
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func getBoolAttr(attrs ipp.Attributes, key string) bool {
|
|
if attr, ok := attrs[key]; ok && len(attr) > 0 {
|
|
if val, ok := attr[0].Value.(bool); ok {
|
|
return val
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getStringSliceAttr(attrs ipp.Attributes, key string) []string {
|
|
attr, ok := attrs[key]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
result := make([]string, 0, len(attr))
|
|
for _, a := range attr {
|
|
if val, ok := a.Value.(string); ok {
|
|
result = append(result, val)
|
|
}
|
|
}
|
|
return result
|
|
}
|