initial commit
This commit is contained in:
107
internal/server/cups/actions.go
Normal file
107
internal/server/cups/actions.go
Normal 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)
|
||||
}
|
||||
285
internal/server/cups/actions_test.go
Normal file
285
internal/server/cups/actions_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
160
internal/server/cups/handlers.go
Normal file
160
internal/server/cups/handlers.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
279
internal/server/cups/handlers_test.go
Normal file
279
internal/server/cups/handlers_test.go
Normal 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)
|
||||
}
|
||||
340
internal/server/cups/manager.go
Normal file
340
internal/server/cups/manager.go
Normal 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, ¤tState) {
|
||||
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
|
||||
}
|
||||
351
internal/server/cups/manager_test.go
Normal file
351
internal/server/cups/manager_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
245
internal/server/cups/subscription.go
Normal file
245
internal/server/cups/subscription.go
Normal 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)
|
||||
}
|
||||
}
|
||||
295
internal/server/cups/subscription_dbus.go
Normal file
295
internal/server/cups/subscription_dbus.go
Normal 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)
|
||||
}
|
||||
}
|
||||
73
internal/server/cups/types.go
Normal file
73
internal/server/cups/types.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user