initial commit

This commit is contained in:
tumillanino
2025-11-12 23:59:00 +11:00
commit 991cca06bf
274 changed files with 70484 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
package cups
import (
"strings"
"time"
"github.com/AvengeMedia/danklinux/pkg/ipp"
)
func (m *Manager) GetPrinters() ([]Printer, error) {
attributes := []string{
ipp.AttributePrinterName,
ipp.AttributePrinterUriSupported,
ipp.AttributePrinterState,
ipp.AttributePrinterStateReasons,
ipp.AttributePrinterLocation,
ipp.AttributePrinterInfo,
ipp.AttributePrinterMakeAndModel,
ipp.AttributePrinterIsAcceptingJobs,
}
printerAttrs, err := m.client.GetPrinters(attributes)
if err != nil {
return nil, err
}
printers := make([]Printer, 0, len(printerAttrs))
for _, attrs := range printerAttrs {
printer := Printer{
Name: getStringAttr(attrs, ipp.AttributePrinterName),
URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported),
State: parsePrinterState(attrs),
StateReason: getStringAttr(attrs, ipp.AttributePrinterStateReasons),
Location: getStringAttr(attrs, ipp.AttributePrinterLocation),
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
Accepting: getBoolAttr(attrs, ipp.AttributePrinterIsAcceptingJobs),
}
if printer.Name != "" {
printers = append(printers, printer)
}
}
return printers, nil
}
func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, error) {
attributes := []string{
ipp.AttributeJobID,
ipp.AttributeJobName,
ipp.AttributeJobState,
ipp.AttributeJobPrinterURI,
ipp.AttributeJobOriginatingUserName,
ipp.AttributeJobKilobyteOctets,
"time-at-creation",
}
jobAttrs, err := m.client.GetJobs(printerName, "", whichJobs, false, 0, 0, attributes)
if err != nil {
return nil, err
}
jobs := make([]Job, 0, len(jobAttrs))
for _, attrs := range jobAttrs {
job := Job{
ID: getIntAttr(attrs, ipp.AttributeJobID),
Name: getStringAttr(attrs, ipp.AttributeJobName),
State: parseJobState(attrs),
User: getStringAttr(attrs, ipp.AttributeJobOriginatingUserName),
Size: getIntAttr(attrs, ipp.AttributeJobKilobyteOctets) * 1024,
}
if uri := getStringAttr(attrs, ipp.AttributeJobPrinterURI); uri != "" {
parts := strings.Split(uri, "/")
if len(parts) > 0 {
job.Printer = parts[len(parts)-1]
}
}
if ts := getIntAttr(attrs, "time-at-creation"); ts > 0 {
job.TimeCreated = time.Unix(int64(ts), 0)
}
if job.ID != 0 {
jobs = append(jobs, job)
}
}
return jobs, nil
}
func (m *Manager) CancelJob(jobID int) error {
return m.client.CancelJob(jobID, false)
}
func (m *Manager) PausePrinter(printerName string) error {
return m.client.PausePrinter(printerName)
}
func (m *Manager) ResumePrinter(printerName string) error {
return m.client.ResumePrinter(printerName)
}
func (m *Manager) PurgeJobs(printerName string) error {
return m.client.CancelAllJob(printerName, true)
}

View File

@@ -0,0 +1,285 @@
package cups
import (
"errors"
"testing"
"time"
mocks_cups "github.com/AvengeMedia/danklinux/internal/mocks/cups"
"github.com/AvengeMedia/danklinux/pkg/ipp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestManager_GetPrinters(t *testing.T) {
tests := []struct {
name string
mockRet map[string]ipp.Attributes
mockErr error
want int
wantErr bool
}{
{
name: "success",
mockRet: map[string]ipp.Attributes{
"printer1": {
ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}},
ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
ipp.AttributePrinterStateReasons: []ipp.Attribute{{Value: "none"}},
ipp.AttributePrinterLocation: []ipp.Attribute{{Value: "Office"}},
ipp.AttributePrinterInfo: []ipp.Attribute{{Value: "Test Printer"}},
ipp.AttributePrinterMakeAndModel: []ipp.Attribute{{Value: "Generic"}},
ipp.AttributePrinterIsAcceptingJobs: []ipp.Attribute{{Value: true}},
},
},
mockErr: nil,
want: 1,
wantErr: false,
},
{
name: "error",
mockRet: nil,
mockErr: errors.New("test error"),
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(tt.mockRet, tt.mockErr)
m := &Manager{
client: mockClient,
}
got, err := m.GetPrinters()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, len(got))
if len(got) > 0 {
assert.Equal(t, "printer1", got[0].Name)
assert.Equal(t, "idle", got[0].State)
assert.Equal(t, "Office", got[0].Location)
assert.True(t, got[0].Accepting)
}
}
})
}
}
func TestManager_GetJobs(t *testing.T) {
tests := []struct {
name string
mockRet map[int]ipp.Attributes
mockErr error
want int
wantErr bool
}{
{
name: "success",
mockRet: map[int]ipp.Attributes{
1: {
ipp.AttributeJobID: []ipp.Attribute{{Value: 1}},
ipp.AttributeJobName: []ipp.Attribute{{Value: "test-job"}},
ipp.AttributeJobState: []ipp.Attribute{{Value: 5}},
ipp.AttributeJobPrinterURI: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
ipp.AttributeJobOriginatingUserName: []ipp.Attribute{{Value: "testuser"}},
ipp.AttributeJobKilobyteOctets: []ipp.Attribute{{Value: 10}},
"time-at-creation": []ipp.Attribute{{Value: 1609459200}},
},
},
mockErr: nil,
want: 1,
wantErr: false,
},
{
name: "error",
mockRet: nil,
mockErr: errors.New("test error"),
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything).
Return(tt.mockRet, tt.mockErr)
m := &Manager{
client: mockClient,
}
got, err := m.GetJobs("printer1", "not-completed")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, len(got))
if len(got) > 0 {
assert.Equal(t, 1, got[0].ID)
assert.Equal(t, "test-job", got[0].Name)
assert.Equal(t, "processing", got[0].State)
assert.Equal(t, "testuser", got[0].User)
assert.Equal(t, "printer1", got[0].Printer)
assert.Equal(t, 10240, got[0].Size)
assert.Equal(t, time.Unix(1609459200, 0), got[0].TimeCreated)
}
}
})
}
}
func TestManager_CancelJob(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.CancelJob(1)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestManager_PausePrinter(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.PausePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestManager_ResumePrinter(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.ResumePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestManager_PurgeJobs(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.PurgeJobs("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,160 @@
package cups
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/danklinux/internal/server/models"
)
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]interface{} `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type CUPSEvent struct {
Type string `json:"type"`
Data CUPSState `json:"data"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "cups.subscribe":
handleSubscribe(conn, req, manager)
case "cups.getPrinters":
handleGetPrinters(conn, req, manager)
case "cups.getJobs":
handleGetJobs(conn, req, manager)
case "cups.pausePrinter":
handlePausePrinter(conn, req, manager)
case "cups.resumePrinter":
handleResumePrinter(conn, req, manager)
case "cups.cancelJob":
handleCancelJob(conn, req, manager)
case "cups.purgeJobs":
handlePurgeJobs(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetPrinters(conn net.Conn, req Request, manager *Manager) {
printers, err := manager.GetPrinters()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, printers)
}
func handleGetJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
jobs, err := manager.GetJobs(printerName, "not-completed")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, jobs)
}
func handlePausePrinter(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.PausePrinter(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "paused"})
}
func handleResumePrinter(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.ResumePrinter(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "resumed"})
}
func handleCancelJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobid' parameter")
return
}
jobID := int(jobIDFloat)
if err := manager.CancelJob(jobID); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job canceled"})
}
func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.PurgeJobs(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "jobs canceled"})
}
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
event := CUPSEvent{
Type: "state_changed",
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
return
}
for state := range stateChan {
event := CUPSEvent{
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
Result: &event,
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,279 @@
package cups
import (
"bytes"
"encoding/json"
"errors"
"net"
"testing"
"time"
mocks_cups "github.com/AvengeMedia/danklinux/internal/mocks/cups"
"github.com/AvengeMedia/danklinux/internal/server/models"
"github.com/AvengeMedia/danklinux/pkg/ipp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockConn struct {
*bytes.Buffer
}
func (m *mockConn) Close() error { return nil }
func (m *mockConn) LocalAddr() net.Addr { return nil }
func (m *mockConn) RemoteAddr() net.Addr { return nil }
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
func TestHandleGetPrinters(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{
"printer1": {
ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}},
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
},
}, nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.getPrinters",
}
handleGetPrinters(conn, req, m)
var resp models.Response[[]Printer]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.Equal(t, 1, len(*resp.Result))
}
func TestHandleGetPrinters_Error(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(nil, errors.New("test error"))
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.getPrinters",
}
handleGetPrinters(conn, req, m)
var resp models.Response[interface{}]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}
func TestHandleGetJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything).
Return(map[int]ipp.Attributes{
1: {
ipp.AttributeJobID: []ipp.Attribute{{Value: 1}},
ipp.AttributeJobName: []ipp.Attribute{{Value: "job1"}},
ipp.AttributeJobState: []ipp.Attribute{{Value: 5}},
},
}, nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.getJobs",
Params: map[string]interface{}{
"printerName": "printer1",
},
}
handleGetJobs(conn, req, m)
var resp models.Response[[]Job]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.Equal(t, 1, len(*resp.Result))
}
func TestHandleGetJobs_MissingParam(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.getJobs",
Params: map[string]interface{}{},
}
handleGetJobs(conn, req, m)
var resp models.Response[interface{}]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}
func TestHandlePausePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.pausePrinter",
Params: map[string]interface{}{
"printerName": "printer1",
},
}
handlePausePrinter(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleResumePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.resumePrinter",
Params: map[string]interface{}{
"printerName": "printer1",
},
}
handleResumePrinter(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleCancelJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.cancelJob",
Params: map[string]interface{}{
"jobID": float64(1),
},
}
handleCancelJob(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandlePurgeJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.purgeJobs",
Params: map[string]interface{}{
"printerName": "printer1",
},
}
handlePurgeJobs(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleRequest_UnknownMethod(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{
client: mockClient,
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.unknownMethod",
}
HandleRequest(conn, req, m)
var resp models.Response[interface{}]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}

View File

@@ -0,0 +1,340 @@
package cups
import (
"fmt"
"os"
"strconv"
"sync"
"time"
"github.com/AvengeMedia/danklinux/internal/log"
"github.com/AvengeMedia/danklinux/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)
m := &Manager{
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: client,
baseURL: baseURL,
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
}
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 {
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 (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
}
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false
continue
}
for _, ch := range m.subscribers {
select {
case ch <- currentState:
default:
}
}
m.subMutex.RUnlock()
stateCopy := currentState
m.lastNotifiedState = &stateCopy
pending = false
}
}
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
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)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0
m.subscribers[id] = ch
m.subMutex.Unlock()
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) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
close(ch)
delete(m.subscribers, id)
}
isEmpty := len(m.subscribers) == 0
m.subMutex.Unlock()
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.subMutex.Lock()
for _, ch := range m.subscribers {
close(ch)
}
m.subscribers = make(map[string]chan CUPSState)
m.subMutex.Unlock()
}
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 ||
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
}

View File

@@ -0,0 +1,351 @@
package cups
import (
"testing"
mocks_cups "github.com/AvengeMedia/danklinux/internal/mocks/cups"
"github.com/AvengeMedia/danklinux/pkg/ipp"
"github.com/stretchr/testify/assert"
)
func TestNewManager(t *testing.T) {
m := &Manager{
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: nil,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
}
assert.NotNil(t, m)
assert.NotNil(t, m.state)
}
func TestManager_GetState(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{
state: &CUPSState{
Printers: map[string]*Printer{
"test-printer": {
Name: "test-printer",
State: "idle",
},
},
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
}
state := m.GetState()
assert.Equal(t, 1, len(state.Printers))
assert.Equal(t, "test-printer", state.Printers["test-printer"].Name)
}
func TestManager_Subscribe(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
}
ch := m.Subscribe("test-client")
assert.NotNil(t, ch)
assert.Equal(t, 1, len(m.subscribers))
m.Unsubscribe("test-client")
assert.Equal(t, 0, len(m.subscribers))
}
func TestManager_Close(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
}
m.eventWG.Add(1)
go func() {
defer m.eventWG.Done()
<-m.stopChan
}()
m.notifierWg.Add(1)
go func() {
defer m.notifierWg.Done()
<-m.stopChan
}()
m.Close()
assert.Equal(t, 0, len(m.subscribers))
}
func TestStateChanged(t *testing.T) {
tests := []struct {
name string
oldState *CUPSState
newState *CUPSState
want bool
}{
{
name: "no change",
oldState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "idle"},
},
},
newState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "idle"},
},
},
want: false,
},
{
name: "state changed",
oldState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "idle"},
},
},
newState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "processing"},
},
},
want: true,
},
{
name: "printer added",
oldState: &CUPSState{
Printers: map[string]*Printer{},
},
newState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "idle"},
},
},
want: true,
},
{
name: "printer removed",
oldState: &CUPSState{
Printers: map[string]*Printer{
"p1": {Name: "p1", State: "idle"},
},
},
newState: &CUPSState{
Printers: map[string]*Printer{},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := stateChanged(tt.oldState, tt.newState)
assert.Equal(t, tt.want, got)
})
}
}
func TestParsePrinterState(t *testing.T) {
tests := []struct {
name string
attrs ipp.Attributes
want string
}{
{
name: "idle",
attrs: ipp.Attributes{
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
},
want: "idle",
},
{
name: "processing",
attrs: ipp.Attributes{
ipp.AttributePrinterState: []ipp.Attribute{{Value: 4}},
},
want: "processing",
},
{
name: "stopped",
attrs: ipp.Attributes{
ipp.AttributePrinterState: []ipp.Attribute{{Value: 5}},
},
want: "stopped",
},
{
name: "unknown",
attrs: ipp.Attributes{},
want: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parsePrinterState(tt.attrs)
assert.Equal(t, tt.want, got)
})
}
}
func TestParseJobState(t *testing.T) {
tests := []struct {
name string
attrs ipp.Attributes
want string
}{
{
name: "pending",
attrs: ipp.Attributes{
ipp.AttributeJobState: []ipp.Attribute{{Value: 3}},
},
want: "pending",
},
{
name: "processing",
attrs: ipp.Attributes{
ipp.AttributeJobState: []ipp.Attribute{{Value: 5}},
},
want: "processing",
},
{
name: "completed",
attrs: ipp.Attributes{
ipp.AttributeJobState: []ipp.Attribute{{Value: 9}},
},
want: "completed",
},
{
name: "unknown",
attrs: ipp.Attributes{},
want: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseJobState(tt.attrs)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetStringAttr(t *testing.T) {
tests := []struct {
name string
attrs ipp.Attributes
key string
want string
}{
{
name: "string value",
attrs: ipp.Attributes{
"test-key": []ipp.Attribute{{Value: "test-value"}},
},
key: "test-key",
want: "test-value",
},
{
name: "missing key",
attrs: ipp.Attributes{},
key: "missing",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getStringAttr(tt.attrs, tt.key)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetIntAttr(t *testing.T) {
tests := []struct {
name string
attrs ipp.Attributes
key string
want int
}{
{
name: "int value",
attrs: ipp.Attributes{
"test-key": []ipp.Attribute{{Value: 42}},
},
key: "test-key",
want: 42,
},
{
name: "missing key",
attrs: ipp.Attributes{},
key: "missing",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getIntAttr(tt.attrs, tt.key)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetBoolAttr(t *testing.T) {
tests := []struct {
name string
attrs ipp.Attributes
key string
want bool
}{
{
name: "true value",
attrs: ipp.Attributes{
"test-key": []ipp.Attribute{{Value: true}},
},
key: "test-key",
want: true,
},
{
name: "false value",
attrs: ipp.Attributes{
"test-key": []ipp.Attribute{{Value: false}},
},
key: "test-key",
want: false,
},
{
name: "missing key",
attrs: ipp.Attributes{},
key: "missing",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getBoolAttr(tt.attrs, tt.key)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,245 @@
package cups
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/danklinux/internal/log"
"github.com/AvengeMedia/danklinux/pkg/ipp"
)
type SubscriptionManager struct {
client CUPSClientInterface
subscriptionID int
sequenceNumber int
eventChan chan SubscriptionEvent
stopChan chan struct{}
wg sync.WaitGroup
baseURL string
running bool
mu sync.Mutex
}
func NewSubscriptionManager(client CUPSClientInterface, baseURL string) *SubscriptionManager {
return &SubscriptionManager{
client: client,
eventChan: make(chan SubscriptionEvent, 100),
stopChan: make(chan struct{}),
baseURL: baseURL,
}
}
func (sm *SubscriptionManager) Start() error {
sm.mu.Lock()
if sm.running {
sm.mu.Unlock()
return fmt.Errorf("subscription manager already running")
}
sm.running = true
sm.mu.Unlock()
subID, err := sm.createSubscription()
if err != nil {
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return fmt.Errorf("failed to create subscription: %w", err)
}
sm.subscriptionID = subID
log.Infof("[CUPS] Created IPP subscription with ID %d", subID)
sm.wg.Add(1)
go sm.notificationLoop()
return nil
}
func (sm *SubscriptionManager) createSubscription() (int, error) {
req := ipp.NewRequest(ipp.OperationCreatePrinterSubscriptions, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
// Subscription attributes go in SubscriptionAttributes (subscription-attributes-tag in IPP)
req.SubscriptionAttributes = map[string]interface{}{
"notify-events": []string{
"printer-state-changed",
"printer-added",
"printer-deleted",
"job-created",
"job-completed",
"job-state-changed",
},
"notify-pull-method": "ippget",
"notify-lease-duration": 0,
}
// Send to root IPP endpoint
resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil)
if err != nil {
return 0, fmt.Errorf("SendRequest failed: %w", err)
}
// Check for IPP errors
if err := resp.CheckForErrors(); err != nil {
return 0, fmt.Errorf("IPP error: %w", err)
}
// Subscription ID comes back in SubscriptionAttributes
if len(resp.SubscriptionAttributes) > 0 {
if idAttr, ok := resp.SubscriptionAttributes[0]["notify-subscription-id"]; ok && len(idAttr) > 0 {
if val, ok := idAttr[0].Value.(int); ok {
return val, nil
}
}
}
return 0, fmt.Errorf("no subscription ID returned")
}
func (sm *SubscriptionManager) notificationLoop() {
defer sm.wg.Done()
backoff := 1 * time.Second
for {
select {
case <-sm.stopChan:
return
default:
}
gotAny, err := sm.fetchNotificationsWithWait()
if err != nil {
log.Warnf("[CUPS] Error fetching notifications: %v", err)
jitter := time.Duration(50+(time.Now().UnixNano()%200)) * time.Millisecond
sleepTime := backoff + jitter
if sleepTime > 30*time.Second {
sleepTime = 30 * time.Second
}
select {
case <-sm.stopChan:
return
case <-time.After(sleepTime):
}
if backoff < 30*time.Second {
backoff *= 2
}
continue
}
backoff = 1 * time.Second
if gotAny {
continue
}
select {
case <-sm.stopChan:
return
case <-time.After(2 * time.Second):
}
}
}
func (sm *SubscriptionManager) fetchNotificationsWithWait() (bool, error) {
req := ipp.NewRequest(ipp.OperationGetNotifications, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
req.OperationAttributes["notify-subscription-ids"] = sm.subscriptionID
if sm.sequenceNumber > 0 {
req.OperationAttributes["notify-sequence-numbers"] = sm.sequenceNumber
}
resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil)
if err != nil {
return false, err
}
gotAny := false
for _, eventGroup := range resp.SubscriptionAttributes {
if seqAttr, ok := eventGroup["notify-sequence-number"]; ok && len(seqAttr) > 0 {
if seqNum, ok := seqAttr[0].Value.(int); ok {
sm.sequenceNumber = seqNum + 1
}
}
event := sm.parseEvent(eventGroup)
gotAny = true
select {
case sm.eventChan <- event:
case <-sm.stopChan:
return gotAny, nil
default:
log.Warn("[CUPS] Event channel full, dropping event")
}
}
return gotAny, nil
}
func (sm *SubscriptionManager) parseEvent(attrs ipp.Attributes) SubscriptionEvent {
event := SubscriptionEvent{
SubscribedAt: time.Now(),
}
if attr, ok := attrs["notify-subscribed-event"]; ok && len(attr) > 0 {
if val, ok := attr[0].Value.(string); ok {
event.EventName = val
}
}
if attr, ok := attrs["printer-name"]; ok && len(attr) > 0 {
if val, ok := attr[0].Value.(string); ok {
event.PrinterName = val
}
}
if attr, ok := attrs["notify-job-id"]; ok && len(attr) > 0 {
if val, ok := attr[0].Value.(int); ok {
event.JobID = val
}
}
return event
}
func (sm *SubscriptionManager) Events() <-chan SubscriptionEvent {
return sm.eventChan
}
func (sm *SubscriptionManager) Stop() {
sm.mu.Lock()
if !sm.running {
sm.mu.Unlock()
return
}
sm.running = false
sm.mu.Unlock()
close(sm.stopChan)
sm.wg.Wait()
if sm.subscriptionID != 0 {
sm.cancelSubscription()
sm.subscriptionID = 0
sm.sequenceNumber = 0
}
sm.stopChan = make(chan struct{})
}
func (sm *SubscriptionManager) cancelSubscription() {
req := ipp.NewRequest(ipp.OperationCancelSubscription, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
req.OperationAttributes["notify-subscription-id"] = sm.subscriptionID
_, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil)
if err != nil {
log.Warnf("[CUPS] Failed to cancel subscription %d: %v", sm.subscriptionID, err)
} else {
log.Infof("[CUPS] Cancelled subscription %d", sm.subscriptionID)
}
}

View File

@@ -0,0 +1,295 @@
package cups
import (
"fmt"
"strings"
"sync"
"github.com/AvengeMedia/danklinux/internal/log"
"github.com/AvengeMedia/danklinux/pkg/ipp"
"github.com/godbus/dbus/v5"
)
type DBusSubscriptionManager struct {
client CUPSClientInterface
subscriptionID int
eventChan chan SubscriptionEvent
stopChan chan struct{}
wg sync.WaitGroup
baseURL string
running bool
mu sync.Mutex
conn *dbus.Conn
}
func NewDBusSubscriptionManager(client CUPSClientInterface, baseURL string) *DBusSubscriptionManager {
return &DBusSubscriptionManager{
client: client,
eventChan: make(chan SubscriptionEvent, 100),
stopChan: make(chan struct{}),
baseURL: baseURL,
}
}
func (sm *DBusSubscriptionManager) Start() error {
sm.mu.Lock()
if sm.running {
sm.mu.Unlock()
return fmt.Errorf("subscription manager already running")
}
sm.running = true
sm.mu.Unlock()
conn, err := dbus.ConnectSystemBus()
if err != nil {
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return fmt.Errorf("connect to system bus: %w", err)
}
sm.conn = conn
subID, err := sm.createDBusSubscription()
if err != nil {
sm.conn.Close()
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return fmt.Errorf("failed to create D-Bus subscription: %w", err)
}
sm.subscriptionID = subID
log.Infof("[CUPS] Created D-Bus subscription with ID %d", subID)
if err := sm.conn.AddMatchSignal(
dbus.WithMatchInterface("org.cups.cupsd.Notifier"),
); err != nil {
sm.cancelSubscription()
sm.conn.Close()
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return fmt.Errorf("failed to add D-Bus match: %w", err)
}
sm.wg.Add(1)
go sm.dbusListenerLoop()
return nil
}
func (sm *DBusSubscriptionManager) createDBusSubscription() (int, error) {
req := ipp.NewRequest(ipp.OperationCreatePrinterSubscriptions, 2)
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
req.SubscriptionAttributes = map[string]interface{}{
"notify-events": []string{
"printer-state-changed",
"printer-added",
"printer-deleted",
"job-created",
"job-completed",
"job-state-changed",
},
"notify-recipient-uri": "dbus:/",
"notify-lease-duration": 86400,
}
resp, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil)
if err != nil {
return 0, fmt.Errorf("SendRequest failed: %w", err)
}
if err := resp.CheckForErrors(); err != nil {
return 0, fmt.Errorf("IPP error: %w", err)
}
if len(resp.SubscriptionAttributes) > 0 {
if idAttr, ok := resp.SubscriptionAttributes[0]["notify-subscription-id"]; ok && len(idAttr) > 0 {
if val, ok := idAttr[0].Value.(int); ok {
return val, nil
}
}
}
return 0, fmt.Errorf("no subscription ID returned")
}
func (sm *DBusSubscriptionManager) dbusListenerLoop() {
defer sm.wg.Done()
signalChan := make(chan *dbus.Signal, 10)
sm.conn.Signal(signalChan)
defer sm.conn.RemoveSignal(signalChan)
for {
select {
case <-sm.stopChan:
return
case sig := <-signalChan:
if sig == nil {
continue
}
event := sm.parseDBusSignal(sig)
if event.EventName == "" {
continue
}
select {
case sm.eventChan <- event:
case <-sm.stopChan:
return
default:
log.Warn("[CUPS] Event channel full, dropping event")
}
}
}
}
func (sm *DBusSubscriptionManager) parseDBusSignal(sig *dbus.Signal) SubscriptionEvent {
event := SubscriptionEvent{}
switch sig.Name {
case "org.cups.cupsd.Notifier.JobStateChanged":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "job-state-changed"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" {
if idx := strings.LastIndex(printerURI, "/"); idx != -1 {
event.PrinterName = printerURI[idx+1:]
}
}
if jobID, ok := sig.Body[3].(uint32); ok {
event.JobID = int(jobID)
}
}
case "org.cups.cupsd.Notifier.JobCreated":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "job-created"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" {
if idx := strings.LastIndex(printerURI, "/"); idx != -1 {
event.PrinterName = printerURI[idx+1:]
}
}
if jobID, ok := sig.Body[3].(uint32); ok {
event.JobID = int(jobID)
}
}
case "org.cups.cupsd.Notifier.JobCompleted":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "job-completed"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" {
if idx := strings.LastIndex(printerURI, "/"); idx != -1 {
event.PrinterName = printerURI[idx+1:]
}
}
if jobID, ok := sig.Body[3].(uint32); ok {
event.JobID = int(jobID)
}
}
case "org.cups.cupsd.Notifier.PrinterStateChanged":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "printer-state-changed"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
if printerURI, ok := sig.Body[1].(string); ok && event.PrinterName == "" {
if idx := strings.LastIndex(printerURI, "/"); idx != -1 {
event.PrinterName = printerURI[idx+1:]
}
}
}
case "org.cups.cupsd.Notifier.PrinterAdded":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "printer-added"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
}
case "org.cups.cupsd.Notifier.PrinterDeleted":
if len(sig.Body) >= 6 {
if text, ok := sig.Body[0].(string); ok {
event.EventName = "printer-deleted"
parts := strings.Split(text, " ")
if len(parts) >= 2 {
event.PrinterName = parts[0]
}
}
}
}
return event
}
func (sm *DBusSubscriptionManager) Events() <-chan SubscriptionEvent {
return sm.eventChan
}
func (sm *DBusSubscriptionManager) Stop() {
sm.mu.Lock()
if !sm.running {
sm.mu.Unlock()
return
}
sm.running = false
sm.mu.Unlock()
close(sm.stopChan)
sm.wg.Wait()
if sm.subscriptionID != 0 {
sm.cancelSubscription()
sm.subscriptionID = 0
}
if sm.conn != nil {
sm.conn.Close()
sm.conn = nil
}
sm.stopChan = make(chan struct{})
}
func (sm *DBusSubscriptionManager) cancelSubscription() {
req := ipp.NewRequest(ipp.OperationCancelSubscription, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
req.OperationAttributes["notify-subscription-id"] = sm.subscriptionID
_, err := sm.client.SendRequest(fmt.Sprintf("%s/", sm.baseURL), req, nil)
if err != nil {
log.Warnf("[CUPS] Failed to cancel subscription %d: %v", sm.subscriptionID, err)
} else {
log.Infof("[CUPS] Cancelled subscription %d", sm.subscriptionID)
}
}

View File

@@ -0,0 +1,73 @@
package cups
import (
"io"
"sync"
"time"
"github.com/AvengeMedia/danklinux/pkg/ipp"
)
type CUPSState struct {
Printers map[string]*Printer `json:"printers"`
}
type Printer struct {
Name string `json:"name"`
URI string `json:"uri"`
State string `json:"state"`
StateReason string `json:"stateReason"`
Location string `json:"location"`
Info string `json:"info"`
MakeModel string `json:"makeModel"`
Accepting bool `json:"accepting"`
Jobs []Job `json:"jobs"`
}
type Job struct {
ID int `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Printer string `json:"printer"`
User string `json:"user"`
Size int `json:"size"`
TimeCreated time.Time `json:"timeCreated"`
}
type Manager struct {
state *CUPSState
client CUPSClientInterface
subscription SubscriptionManagerInterface
stateMutex sync.RWMutex
subscribers map[string]chan CUPSState
subMutex sync.RWMutex
stopChan chan struct{}
eventWG sync.WaitGroup
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotifiedState *CUPSState
baseURL string
}
type SubscriptionManagerInterface interface {
Start() error
Stop()
Events() <-chan SubscriptionEvent
}
type CUPSClientInterface interface {
GetPrinters(attributes []string) (map[string]ipp.Attributes, error)
GetJobs(printer, class string, whichJobs string, myJobs bool, firstJobId, limit int, attributes []string) (map[int]ipp.Attributes, error)
CancelJob(jobID int, purge bool) error
PausePrinter(printer string) error
ResumePrinter(printer string) error
CancelAllJob(printer string, purge bool) error
SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error)
}
type SubscriptionEvent struct {
EventName string
PrinterName string
JobID int
SubscribedAt time.Time
}