Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 255 additions & 8 deletions authconn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import threading
import queue
import time

import config
import cups
import cupspk
Expand All @@ -33,6 +36,11 @@
import gettext
gettext.install(domain=config.PACKAGE, localedir=config.localedir)
N_ = lambda x: x
class WorkerTimeoutError(RuntimeError):
pass

class WorkerUnavailableError(RuntimeError):
pass

cups.require("1.9.60")
class AuthDialog(Gtk.Dialog):
Expand Down Expand Up @@ -154,6 +162,26 @@ def remove_auth_info (self, host=None, port=None):

global_authinfocache = _AuthInfoCache ()

def _dispatch_worker_op(op, conn, args, kwds):
"""
Execute a worker operation against the worker-owned cups.Connection
or as a standalone callable.
"""
if isinstance(op, str):
fn = getattr(conn, op)
return fn(*args, **kwds)

if hasattr(op, '_op_name') and getattr(op, '_target_conn', None) is not None:
fn = getattr(conn, op._op_name)
return fn(*args, **kwds)

if callable(op):
if getattr(op, '_with_connection', False):
return op(conn, *args, **kwds)
return op(*args, **kwds)

raise TypeError(f"Operation {op!r} is not callable or a valid method name")

class Connection:
def __init__ (self, parent=None, try_as_root=True, lock=False,
host=None, port=None, encryption=None):
Expand All @@ -175,9 +203,16 @@ def __init__ (self, parent=None, try_as_root=True, lock=False,
self._operation_stack = []
self._lock = lock
self._gui_event = threading.Event ()
self._auth_lock = threading.Lock ()
self._cancel = False
self._cannot_auth = False
self._worker_lock = threading.Lock()

self._connect ()

def cancel (self):
self._cancel = True

def _begin_operation (self, operation):
debugprint ("%s: Operation += %s" % (self, operation))
self._operation_stack.append (operation)
Expand Down Expand Up @@ -229,9 +264,208 @@ def _using_polkit (self):
return isinstance (self._connection, cupspk.Connection)

def _make_binding (self, fname, fn):
return lambda *args, **kwds: self._authloop (fname, fn, *args, **kwds)
binding = lambda *args, **kwds: self._authloop (fname, fn, *args, **kwds)
binding._op_name = fname
binding._target_conn = self
return binding

def run_on_worker (self, op, feedback=None, spinner=None, delay_ms=250, with_connection=False):
"""
Returns a callable wrapper that executes the specified CUPS operation
on the dedicated background worker thread while pumping the GLib
main loop.

:param op: Either a method name string (e.g. 'getServerPPD', 'getPPD'),
a bound Connection method (self.cups.getServerPPD),
or a callable: op(*args, **kwds) or op(conn, *args, **kwds) if with_connection.
:param feedback: Optional DelayedSpinner or WorkerFeedback instance.
:param spinner: Optional VectorSpinner or Gtk.Spinner widget.
:param delay_ms: Milliseconds to delay spinner appearance (default 250ms).
:param with_connection: If True, pass the worker cups.Connection to op.
"""
if feedback is None and spinner is not None:
from vectorspinner import DelayedSpinner
feedback = DelayedSpinner(spinner, delay_ms=delay_ms)

target_op = getattr(op, '_op_name', op)
if with_connection and callable(target_op):
try:
target_op._with_connection = True
except AttributeError:
pass

is_conn_method = False
if isinstance(target_op, str) and hasattr(self._connection, target_op):
is_conn_method = True
elif hasattr(op, '_op_name') and getattr(op, '_target_conn', None) is self:
is_conn_method = True

def wrapper (*args, **kwds):
# Worker-thread re-entry: execute directly on worker connection to avoid deadlock
if hasattr(self, '_worker_thread') and threading.current_thread() == self._worker_thread:
conn = getattr(self, '_worker_conn', None)
if conn is None:
with self._auth_lock:
user = self._use_user
cups.setUser(user)
conn = cups.Connection(host=self._server,
port=self._port,
encryption=self._encryption)
self._worker_conn = conn
return _dispatch_worker_op(target_op, conn, args, kwds)

# Execution on GTK / main thread:
if is_conn_method:
fn = getattr(self._connection, target_op)
return self._authloop(target_op, fn, *args, _on_worker=True,
_feedback=feedback, **kwds)
else:
return self._execute_in_worker(target_op, *args, _feedback=feedback, **kwds)

return wrapper

def _execute_in_worker(self, fname, *args, **kwds):
timeout_val = kwds.pop('_timeout', 30.0)
feedback = kwds.pop('_feedback', None)

if not hasattr(self, '_worker_queue'):
self._worker_lock = getattr(self, '_worker_lock', threading.Lock())
self._worker_state = 'IDLE'
self._worker_conn = None
self._worker_queue = queue.Queue()
self._worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
self._worker_thread.start()

with self._worker_lock:
if getattr(self, '_worker_unresponsive', False):
raise WorkerUnavailableError("Worker is unavailable or busy")

box = []
req_state = {'completed': False, 'timed_out': False}
self._worker_queue.put((fname, args, kwds, box, req_state))

loop = GLib.MainLoop()
source_id = [None]
start_time = time.monotonic()

if feedback is not None:
feedback.start()

def poll():
with self._worker_lock:
is_completed = req_state['completed']

if is_completed:
if loop.is_running():
loop.quit()
source_id[0] = None
return False

if time.monotonic() - start_time > timeout_val:
with self._worker_lock:
if req_state['completed']:
if loop.is_running():
loop.quit()
source_id[0] = None
return False

req_state['timed_out'] = True
self._worker_unresponsive = True

if loop.is_running():
loop.quit()
source_id[0] = None
return False
return True

source_id[0] = GLib.timeout_add(20, poll)

try:
loop.run()
finally:
if source_id[0] is not None:
GLib.source_remove(source_id[0])
source_id[0] = None
if feedback is not None:
feedback.stop()

if req_state['timed_out']:
raise WorkerTimeoutError("CUPS worker timeout")

if not box:
raise WorkerUnavailableError("CUPS worker returned no result")

def _authloop (self, fname, fn, *args, **kwds):
ok, res, exc = box[0]
if not ok:
raise exc
return res

def _worker_loop(self):
conn = None
conn_user = None

while True:
item = self._worker_queue.get()
if item is None:
break
fname, args, kwds, box, req_state = item

with self._worker_lock:
self._worker_state = 'BUSY'

try:
with self._auth_lock:
target_user = self._use_user

if conn is None or conn_user != target_user:
cups.setUser(target_user)
conn = cups.Connection(host=self._server,
port=self._port,
encryption=self._encryption)
conn_user = target_user
self._worker_conn = conn

cups.setUser(target_user)
res = _dispatch_worker_op(fname, conn, args, kwds)

res_tuple = (True, res, None)
except Exception as e:
res_tuple = (False, None, e)
finally:
with self._worker_lock:
if req_state['timed_out']:
# Clean up temporary files if a file path was returned
if (res_tuple[0] and isinstance(res_tuple[1], str) and
os.path.isabs(res_tuple[1]) and os.path.exists(res_tuple[1])):
try:
os.unlink(res_tuple[1])
except OSError:
pass
conn = None
conn_user = None
self._worker_conn = None
self._worker_unresponsive = False
else:
req_state['completed'] = True
box.append(res_tuple)
self._worker_state = 'IDLE'

# Clear references to avoid retaining connection or data
fn = None
res = None
res_tuple = None
item = None
args = None
kwds = None
box = None
req_state = None
fname = None

self._worker_queue.task_done()

self._worker_conn = None

def _authloop (self, fname, fn, *args, _on_worker=False, _feedback=None, **kwds):
self._passes = 0
# remove signature if dbus is not being used and signature is provided
if not self._using_polkit():
Expand All @@ -241,7 +475,12 @@ def _authloop (self, fname, fn, *args, **kwds):
retry = False
while True:
try:
if self._perform_authentication () == 0:
auth_stat = self._perform_authentication ()
if auth_stat == 0:
break
if auth_stat < 0 or self._cancel:
if self._cancel and not self._cannot_auth:
raise cups.IPPError (0, _("Operation canceled"))
break

if c != self._connection:
Expand All @@ -251,7 +490,12 @@ def _authloop (self, fname, fn, *args, **kwds):

cups.setUser (self._use_user)

result = fn.__call__ (*args, **kwds)
if _on_worker:
result = self._execute_in_worker (fname, *args, _feedback=_feedback, **kwds)
else:
if fn is None:
fn = getattr (self._connection, fname)
result = fn.__call__ (*args, **kwds)

if fname == 'adminGetServerSettings':
# Special case for a rubbish bit of API.
Expand Down Expand Up @@ -340,7 +584,8 @@ def _password_callback (self, prompt):

self._auth_called = True
self._prompt = prompt
return self._use_password
with self._auth_lock:
return self._use_password

def _perform_authentication (self):
self._passes += 1
Expand Down Expand Up @@ -492,9 +737,11 @@ def _perform_authentication_with_dialog (self):
self._on_authentication_response (d, response)

def _on_authentication_response (self, dialog, response):
(user, self._use_password) = dialog.get_auth_info ()
if user != '':
self._use_user = user
(user, password) = dialog.get_auth_info ()
with self._auth_lock:
if user != '':
self._use_user = user
self._use_password = password
global_authinfocache.cache_auth_info ((self._use_user,
self._use_password),
host=self._server,
Expand Down
Loading
Loading