PrintQueue
The session's print queue: a CUPS scheduler run as this user under the
runtime directory, with one queue, Selkies, whose backend writes each job
into the spool. The scheduler's programs are the installed ones, reached
through a private tree that adds the backend, and its whole state lives
under root, so nothing under /etc/cups is touched and no privilege is
needed. Applications reach it with CUPS_SERVER set to socket.
Attributes
attributeroot= os.path.join(runtime, 'selkies-cups') if runtime else os.path.join(tempfile.gettempdir(), f'selkies-cups-{os.getuid()}')attributesocket= os.path.join(self.root, 'cups.sock')attributespool= spoolattributepaper= 'A4'attributeprocessOptional[asyncio.subprocess.Process]= NoneattributeSEARCH_PATH= ':/usr/sbin:/usr/local/sbin'Functions
constructor__init__(spool) -> NoneSource Code
def __init__(self, spool: str) -> None:
runtime = os.environ.get("XDG_RUNTIME_DIR")
self.root = os.path.join(runtime, "selkies-cups") if runtime else \
os.path.join(tempfile.gettempdir(), f"selkies-cups-{os.getuid()}")
self.socket = os.path.join(self.root, "cups.sock")
self.spool = spool
self.paper = "A4"
self.process: Optional[asyncio.subprocess.Process] = NoneparamspoolstrReturns
Nonefuncprograms() -> Optional[Tuple[str, str, str]](cupsd, server bin, data dir) of the installed CUPS, or None.
Source Code
@staticmethod
def programs() -> Optional[Tuple[str, str, str]]:
"""`(cupsd, server bin, data dir)` of the installed CUPS, or None."""
path = os.environ.get("PATH", "") + PrintQueue.SEARCH_PATH
cupsd = shutil.which("cupsd", path=path)
if not cupsd:
return None
prefix = os.path.dirname(os.path.dirname(os.path.realpath(cupsd)))
for lib in ("lib", "lib64", "libexec"):
server_bin = os.path.join(prefix, lib, "cups")
if os.path.isfile(os.path.join(server_bin, "daemon", "cups-exec")) \
and os.path.isdir(os.path.join(server_bin, "filter")):
return cupsd, server_bin, os.path.join(prefix, "share", "cups")
return NoneReturns
typing.Optional[typing.Tuple[str, str, str]]funclocked_scheduler() -> Optional[Tuple[str, int]](path, mode) of a cupsd on the search path that this user can
neither read nor run, which programs passes over, or None. The queue
runs a copy of the program as the session user, so a scheduler
installed for root alone cannot serve it however it is configured.
Source Code
@staticmethod
def locked_scheduler() -> Optional[Tuple[str, int]]:
"""`(path, mode)` of a cupsd on the search path that this user can
neither read nor run, which `programs` passes over, or None. The queue
runs a copy of the program as the session user, so a scheduler
installed for root alone cannot serve it however it is configured."""
path = os.environ.get("PATH", "") + PrintQueue.SEARCH_PATH
cupsd = shutil.which("cupsd", mode=os.F_OK, path=path)
if not cupsd or os.access(cupsd, os.R_OK | os.X_OK):
return None
return cupsd, stat.S_IMODE(os.stat(cupsd).st_mode)Returns
typing.Optional[typing.Tuple[str, int]]func_claim_root() -> NoneMake the state root this user's alone, or refuse it: without a runtime directory it sits in the shared temporary one under a predictable name, where another account could create it first and plant the programs and configuration the scheduler then runs as this user.
Source Code
def _claim_root(self) -> None:
"""Make the state root this user's alone, or refuse it: without a runtime
directory it sits in the shared temporary one under a predictable name,
where another account could create it first and plant the programs and
configuration the scheduler then runs as this user."""
try:
os.mkdir(self.root, 0o700)
except FileExistsError:
pass
st = os.lstat(self.root)
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or st.st_mode & 0o022:
raise PermissionError(f"{self.root} is not a directory this user alone owns and writes")Returns
Nonefunc_prepare(server_bin, data_dir) -> NoneSource Code
def _prepare(self, server_bin: str, data_dir: str) -> None:
self._claim_root()
for sub in ("ppd", "state", "cache", "spool", "tmp", "bin/backend"):
os.makedirs(os.path.join(self.root, sub), exist_ok=True)
os.makedirs(self.spool, exist_ok=True)
for sub in ("filter", "daemon"):
link = os.path.join(self.root, "bin", sub)
if os.path.islink(link):
os.unlink(link)
os.symlink(os.path.join(server_bin, sub), link)
package = files("selkies") / "cups"
backend = os.path.join(self.root, "bin", "backend", "selkies")
with open(backend, "wb") as out:
out.write((package / "backend").read_bytes())
os.chmod(backend, 0o755)
requested = os.environ.get("PAPERSIZE", "")
ppd, size = default_paper((package / "selkies.ppd").read_text("latin-1"), requested)
if requested and not size:
logger.warning("PAPERSIZE=%s names no size the Selkies queue offers; it stays on A4", requested)
self.paper = size or "A4"
with open(os.path.join(self.root, "ppd", "Selkies.ppd"), "w", encoding="latin-1") as out:
out.write(ppd)
confs = {
"cups-files.conf": f"""ServerRoot {self.root}
ServerBin {os.path.join(self.root, "bin")}
DataDir {data_dir}
StateDir {os.path.join(self.root, "state")}
CacheDir {os.path.join(self.root, "cache")}
RequestRoot {os.path.join(self.root, "spool")}
TempDir {os.path.join(self.root, "tmp")}
LogFileGroup {os.getgid()}
AccessLog {os.path.join(self.root, "access.log")}
PageLog {os.path.join(self.root, "page.log")}
ErrorLog {os.path.join(self.root, "error.log")}
""",
"cupsd.conf": f"""Listen {self.socket}
LogLevel warn
Browsing Off
WebInterface No
""",
"printers.conf": f"""<DefaultPrinter Selkies>
Info Selkies printer
Location In the browser
DeviceURI selkies:{self.spool}
State Idle
Accepting Yes
Shared No
JobSheets none none
ErrorPolicy retry-job
</DefaultPrinter>
""",
}
for name, text in confs.items():
with open(os.path.join(self.root, name), "w") as out:
out.write(text)
if os.path.exists(self.socket):
os.unlink(self.socket)paramserver_binstrparamdata_dirstrReturns
Nonefuncstart() -> boolStart the scheduler; False where CUPS is not installed.
Source Code
async def start(self) -> bool:
"""Start the scheduler; False where CUPS is not installed."""
found = self.programs()
if found is None:
locked = self.locked_scheduler()
if locked:
logger.warning("No print queue: %s is mode %o, which this user can neither read nor "
"run; a Selkies queue needs the scheduler readable (Arch's cups "
"package installs it for root alone)", *locked)
else:
logger.info("No CUPS scheduler on this host: documents printed into the spool are "
"still handed over, but there is no Selkies queue to print to "
"(cups-daemon and cups-filters provide one)")
return False
cupsd, server_bin, data_dir = found
# The scheduler runs as a copy of the program: a distribution confines
# the system scheduler, by its path, to the system's directories and
# backends, which a queue under the runtime directory has neither of.
# The copy is renamed into place, which a scheduler still running the
# old copy (left by a server that died without stopping it) never
# blocks the way writing over its program would.
program = os.path.join(self.root, "cupsd")
try:
self._prepare(server_bin, data_dir)
shutil.copy2(cupsd, program + ".new")
os.replace(program + ".new", program)
except OSError as exc:
logger.warning("No print queue: cannot set the scheduler up under %s: %s", self.root, exc)
return False
args = ["-f", "-c", os.path.join(self.root, "cupsd.conf"), "-s", os.path.join(self.root, "cups-files.conf")]
spawn = dict(stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
preexec_fn=_die_with_parent)
try:
self.process = await asyncio.create_subprocess_exec(program, *args, **spawn)
except OSError:
self.process = await asyncio.create_subprocess_exec(cupsd, *args, **spawn)
for _ in range(100):
if os.path.exists(self.socket) or self.process.returncode is not None:
break
await asyncio.sleep(0.1)
if not os.path.exists(self.socket):
logger.warning("The print queue did not come up; its log is %s",
os.path.join(self.root, "error.log"))
await self.stop()
return False
logger.info("Print queue Selkies listening on %s, spooling to %s, on %s paper", self.socket, self.spool, self.paper)
return TrueReturns
boolfuncstop() -> NoneSource Code
async def stop(self) -> None:
process, self.process = self.process, None
if process is None or process.returncode is not None:
return
process.terminate()
try:
await asyncio.wait_for(process.wait(), 5)
except asyncio.TimeoutError:
process.kill()
await process.wait()Returns
None