-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
149 lines (125 loc) · 5.09 KB
/
main.py
File metadata and controls
149 lines (125 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3
import logging
DEBUG = False
logging.basicConfig(
filename='/var/log/protoprint.log', encoding='utf-8',
format='[%(asctime)s] %(levelname)s %(funcName)s - %(message)s',
level=logging.DEBUG if DEBUG else logging.INFO)
import sys
import os
import json
from urllib import request, error
import subprocess
import tempfile
# The API endpoint to notify about a new print job.
# This server should return a 200 OK status to allow the print job.
API_ENDPOINT = "https://api.my.protospace.ca/protocoin/cups_printer_report/"
def approve_job():
"""Instructs CUPS that the job is successful."""
logging.info('Approving job, returning 0.')
sys.exit(0)
def retry_job():
"""Instructs CUPS to retry the job later."""
logging.info('Retrying job, returning 1.')
sys.exit(1)
def cancel_job():
"""Instructs CUPS to cancel the job."""
logging.info('Cancelling job, returning 4.')
sys.exit(4)
def main():
"""
CUPS backend script to control print job release via an API call.
"""
# CUPS calls backends with 0 args for discovery, or 5/6 for a print job.
# We only care about print jobs.
if len(sys.argv) < 6:
approve_job()
job_id = sys.argv[1]
user = sys.argv[2]
title = sys.argv[3]
copies = sys.argv[4]
options = sys.argv[5]
job_file = sys.argv[6] if len(sys.argv) == 7 else None
logging.info(
f"New print job received:\n"
f" Job ID: {job_id}\n"
f" User: {user}\n"
f" Title: {title}\n"
f" Copies: {copies}\n"
f" Options: {options}"
)
# The device URI for this backend must be in the format:
# printmanager:/<real_backend_uri>
# e.g., printmanager:socket://192.168.1.123:9100
device_uri = os.environ.get("DEVICE_URI")
if not device_uri or not device_uri.startswith("printmanager:"):
logging.error("Invalid DEVICE_URI. Expected 'printmanager:/<real_uri>'.")
cancel_job()
real_printer_uri = device_uri[len("printmanager:"):]
if not real_printer_uri:
logging.error("Real printer URI is missing from DEVICE_URI.")
cancel_job()
# Prepare data for the API POST request
payload = {
'job_id': job_id,
'user': user,
'title': title,
'printer': os.environ.get("PRINTER", "unknown"),
'copies': copies,
}
logging.info(f"Processing job {job_id} for user {user}. Notifying API.")
try:
data = json.dumps(payload).encode('utf-8')
req = request.Request(API_ENDPOINT, data=data, headers={'Content-Type': 'application/json'})
with request.urlopen(req, timeout=30):
# If we get here, urlopen was successful and didn't raise HTTPError.
# This implies a 2xx or 3xx response (which is followed).
logging.info(f"API approval received for job {job_id}. Releasing to printer.")
except error.HTTPError as e:
# This handles 4xx and 5xx responses, example: username not found
response_text = e.read().decode('utf-8', 'ignore')
logging.error(f"API call failed with status {e.code}. Job will not be printed.")
logging.error(f"Response: {response_text}")
cancel_job()
except error.URLError as e:
# This handles other network errors (DNS, timeout, connection refused).
logging.error(f"API request failed: {e}")
cancel_job()
# API call was successful, now release the job to the real printer.
try:
scheme = real_printer_uri.split(':', 1)[0]
except IndexError:
logging.error(f"Could not determine scheme from real printer URI: {real_printer_uri}")
cancel_job()
backend_path = f"/usr/lib/cups/backend/{scheme}"
if not os.path.exists(backend_path) or not os.access(backend_path, os.X_OK):
logging.error(f"CUPS backend for scheme '{scheme}' not found or not executable at {backend_path}.")
cancel_job()
# The print job data is on stdin if no file is given.
# We must pass it to the real backend.
# Using a temporary file ensures the data is read before we execute the next process.
temp_job_file = None
if not job_file:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(sys.stdin.buffer.read())
job_file = tmp.name
temp_job_file = job_file
# Set up environment and arguments for the real backend
backend_env = os.environ.copy()
backend_env["DEVICE_URI"] = real_printer_uri
backend_args = [backend_path, job_id, user, title, copies, options, job_file]
try:
# Execute the real backend to perform the printing
result = subprocess.run(backend_args, env=backend_env, check=False)
if result.returncode == 0:
logging.info(f"Job {job_id} successfully sent to printer.")
approve_job()
else:
logging.error(f"Real backend '{scheme}' failed with exit code {result.returncode}.")
cancel_job()
finally:
# Clean up the temporary file if we created one
if temp_job_file:
os.unlink(temp_job_file)
if __name__ == "__main__":
main()