generated from itdominator/Python-With-Gtk-Template
993 lines
40 KiB
Python
993 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import print_function
|
|
|
|
import itertools as it, operator as op, functools as ft
|
|
from collections import defaultdict
|
|
from contextlib import contextmanager
|
|
import os, sys, inspect, traceback
|
|
|
|
from . import _pulsectl as c
|
|
|
|
|
|
if sys.version_info.major >= 3:
|
|
long, unicode = int, str
|
|
print_err = ft.partial(print, file=sys.stderr, flush=True)
|
|
def wrapper_with_sig_info(func, wrapper, index_arg=False):
|
|
sig = inspect.signature(func or (lambda: None))
|
|
if index_arg:
|
|
sig = sig.replace(parameters=[inspect.Parameter( 'index',
|
|
inspect.Parameter.POSITIONAL_OR_KEYWORD )] + list(sig.parameters.values()))
|
|
wrapper.__name__, wrapper.__signature__, wrapper.__doc__ = '', sig, func.__doc__
|
|
return wrapper
|
|
|
|
else:
|
|
range, map = xrange, it.imap
|
|
def print_err(*args, **kws):
|
|
kws.setdefault('file', sys.stderr)
|
|
print(*args, **kws)
|
|
kws['file'].flush()
|
|
def wrapper_with_sig_info(func, wrapper, index_arg=False):
|
|
func_args = list(inspect.getargspec(func or (lambda: None)))
|
|
func_args[0] = list(func_args[0])
|
|
if index_arg: func_args[0] = ['index'] + func_args[0]
|
|
wrapper.__name__ = '...'
|
|
wrapper.__doc__ = 'Signature: func' + inspect.formatargspec(*func_args)
|
|
if func.__doc__: wrapper.__doc__ += '\n\n' + func.__doc__
|
|
return wrapper
|
|
|
|
|
|
is_str = lambda v,ext=None,native=False: (
|
|
isinstance(v, ( (unicode, bytes)
|
|
if not native else (str,) ) + ((ext,) if ext else ())) )
|
|
is_str_native = ft.partial(is_str, native=True)
|
|
is_num = lambda v: isinstance(v, (int, float, long))
|
|
is_list = lambda v: isinstance(v, (tuple, list))
|
|
is_dict = lambda v: isinstance(v, dict)
|
|
|
|
def assert_pulse_object(obj):
|
|
if not isinstance(obj, PulseObject):
|
|
raise TypeError( 'Pulse<something>Info'
|
|
' object is required instead of value: [{}] {}', type(obj), obj )
|
|
|
|
class FakeLock():
|
|
def __enter__(self): return self
|
|
def __exit__(self, *err): pass
|
|
|
|
|
|
@ft.total_ordering
|
|
class EnumValue(object):
|
|
'String-based enum value, comparable to native strings.'
|
|
__slots__ = '_t', '_value', '_c_val'
|
|
def __init__(self, t, value, c_value=None):
|
|
self._t, self._value, self._c_val = t, value, c_value
|
|
def __repr__(self): return '<EnumValue {}={}>'.format(self._t, self._value)
|
|
def __eq__(self, val):
|
|
if isinstance(val, EnumValue): val = val._value
|
|
return self._value == val
|
|
def __ne__(self, val): return not (self == val)
|
|
def __lt__(self, val):
|
|
if isinstance(val, EnumValue): val = val._value
|
|
return self._value < val
|
|
def __hash__(self): return hash(self._value)
|
|
|
|
class Enum(object):
|
|
|
|
def __init__(self, name, values_or_map):
|
|
vals = values_or_map
|
|
if is_str_native(vals): vals = vals.split()
|
|
if is_list(vals): vals = zip(it.repeat(None), vals)
|
|
if is_dict(vals): vals = vals.items()
|
|
self._name, self._values, self._c_vals = name, dict(), dict()
|
|
for c_val, k in vals:
|
|
v = EnumValue(name, k, c_val)
|
|
setattr(self, k.replace('-', '_'), v)
|
|
self._c_vals[c_val] = self._values[k] = v
|
|
|
|
def __getitem__(self, k, *default):
|
|
if isinstance(k, EnumValue):
|
|
t, k, v = k._t, k._value, k
|
|
if t != self._name: raise KeyError(v)
|
|
try: return getattr(self, k.replace('-', '_'), *default)
|
|
except AttributeError: raise KeyError(k)
|
|
|
|
def _get(self, k, default=None): return self.__getitem__(k, default)
|
|
def __contains__(self, k): return self._get(k) is not None
|
|
|
|
def _c_val(self, c_val, default=KeyError):
|
|
v = self._c_vals.get(c_val)
|
|
if v is not None: return v
|
|
if default is not KeyError:
|
|
return EnumValue(self._name, default, c_val)
|
|
raise KeyError(c_val)
|
|
|
|
def __repr__(self):
|
|
return '<Enum {} [{}]>'.format(self._name, ' '.join(sorted(self._values.keys())))
|
|
|
|
|
|
PulseEventTypeEnum = Enum('event-type', c.PA_EVENT_TYPE_MAP)
|
|
PulseEventFacilityEnum = Enum('event-facility', c.PA_EVENT_FACILITY_MAP)
|
|
PulseEventMaskEnum = Enum('event-mask', c.PA_EVENT_MASK_MAP)
|
|
|
|
PulseStateEnum = Enum('sink/source-state', c.PA_OBJ_STATE_MAP)
|
|
PulseUpdateEnum = Enum('update-type', c.PA_UPDATE_MAP)
|
|
PulsePortAvailableEnum = Enum('available', c.PA_PORT_AVAILABLE_MAP)
|
|
PulseDirectionEnum = Enum('direction', c.PA_DIRECTION_MAP)
|
|
|
|
|
|
class PulseError(Exception): pass
|
|
class PulseOperationFailed(PulseError): pass
|
|
class PulseOperationInvalid(PulseOperationFailed): pass
|
|
class PulseIndexError(PulseError): pass
|
|
|
|
class PulseLoopStop(Exception): pass
|
|
class PulseDisconnected(Exception): pass
|
|
|
|
class PulseObject(object):
|
|
|
|
c_struct_wrappers = dict()
|
|
|
|
def __init__(self, struct=None, *field_data_list, **field_data_dict):
|
|
field_data, fields = dict(), getattr(self, 'c_struct_fields', list())
|
|
if is_str_native(fields): fields = self.c_struct_fields = fields.split()
|
|
if field_data_list: field_data.update(zip(fields, field_data_list))
|
|
if field_data_dict: field_data.update(field_data_dict)
|
|
if struct is None: field_data, struct = dict(), field_data
|
|
assert not set(field_data.keys()).difference(fields)
|
|
if field_data: self._copy_struct_fields(field_data, fields=field_data.keys())
|
|
self._copy_struct_fields(struct, fields=set(fields).difference(field_data.keys()))
|
|
|
|
if struct:
|
|
if hasattr(struct, 'proplist'):
|
|
self.proplist, state = dict(), c.c_void_p()
|
|
while True:
|
|
k = c.pa.proplist_iterate(struct.proplist, c.byref(state))
|
|
if not k: break
|
|
self.proplist[c.force_str(k)] = c.force_str(c.pa.proplist_gets(struct.proplist, k))
|
|
if hasattr(struct, 'volume'):
|
|
self.volume = self._get_wrapper(PulseVolumeInfo)(struct.volume)
|
|
if hasattr(struct, 'base_volume'):
|
|
self.base_volume = struct.base_volume / c.PA_VOLUME_NORM
|
|
if hasattr(struct, 'n_ports'):
|
|
cls_port = self._get_wrapper(PulsePortInfo)
|
|
self.port_list = list(
|
|
cls_port(struct.ports[n].contents) for n in range(struct.n_ports) )
|
|
if hasattr(struct, 'active_port'):
|
|
cls_port = self._get_wrapper(PulsePortInfo)
|
|
self.port_active = (
|
|
None if not struct.active_port else cls_port(struct.active_port.contents) )
|
|
if hasattr(struct, 'channel_map'):
|
|
self.channel_count, self.channel_list = struct.channel_map.channels, list()
|
|
self.channel_list_raw = struct.channel_map.map[:self.channel_count]
|
|
if self.channel_count > 0:
|
|
s = c.create_string_buffer(b'\0' * 512)
|
|
c.pa.channel_map_snprint(s, len(s), struct.channel_map)
|
|
self.channel_list.extend(map(c.force_str, s.value.strip().split(b',')))
|
|
if hasattr(struct, 'state'):
|
|
self.state = PulseStateEnum._c_val(
|
|
struct.state, u'state.{}'.format(struct.state) )
|
|
self.state_values = sorted(PulseStateEnum._values.values())
|
|
if hasattr(struct, 'corked'): self.corked = bool(struct.corked)
|
|
self._init_from_struct(struct)
|
|
|
|
def _get_wrapper(self, cls_base):
|
|
return self.c_struct_wrappers.get(cls_base, cls_base)
|
|
|
|
def _copy_struct_fields(self, struct, fields=None, str_errors='strict'):
|
|
if not fields: fields = self.c_struct_fields
|
|
for k in fields:
|
|
setattr(self, k, c.force_str( getattr(struct, k)
|
|
if not is_dict(struct) else struct[k], str_errors ))
|
|
|
|
def _init_from_struct(self, struct): pass # to parse fields in subclasses
|
|
|
|
def _as_str(self, ext=None, fields=None, **kws):
|
|
kws = list(it.starmap('{}={}'.format, kws.items()))
|
|
if fields:
|
|
if is_str_native(fields): fields = fields.split()
|
|
kws.extend('{}={!r}'.format(k, getattr(self, k)) for k in fields)
|
|
kws = sorted(kws)
|
|
if ext: kws.append(str(ext))
|
|
return ', '.join(kws)
|
|
|
|
def __str__(self):
|
|
return self._as_str(fields=self.c_struct_fields)
|
|
|
|
def __repr__(self):
|
|
return '<{} at {:x} - {}>'.format(self.__class__.__name__, id(self), str(self))
|
|
|
|
|
|
class PulsePortInfo(PulseObject):
|
|
c_struct_fields = 'name description available priority'
|
|
|
|
def _init_from_struct(self, struct):
|
|
self.available = PulsePortAvailableEnum._c_val(struct.available)
|
|
self.available_state = self.available # for compatibility with <=17.6.0
|
|
|
|
def __eq__(self, o):
|
|
if not isinstance(o, PulsePortInfo): raise TypeError(o)
|
|
return self.name == o.name
|
|
|
|
def __hash__(self): return hash(self.name)
|
|
|
|
class PulseClientInfo(PulseObject):
|
|
c_struct_fields = 'name index driver owner_module'
|
|
|
|
class PulseServerInfo(PulseObject):
|
|
c_struct_fields = ( 'user_name host_name'
|
|
' server_version server_name default_sink_name default_source_name cookie' )
|
|
|
|
class PulseModuleInfo(PulseObject):
|
|
c_struct_fields = 'index name argument n_used auto_unload'
|
|
|
|
class PulseSinkInfo(PulseObject):
|
|
c_struct_fields = ( 'index name mute'
|
|
' description sample_spec owner_module latency driver'
|
|
' monitor_source monitor_source_name flags configured_latency card' )
|
|
|
|
def __str__(self):
|
|
return self._as_str(self.volume, fields='index name description mute')
|
|
|
|
class PulseSinkInputInfo(PulseObject):
|
|
c_struct_fields = ( 'index name mute corked client'
|
|
' owner_module sink sample_spec'
|
|
' buffer_usec sink_usec resample_method driver' )
|
|
|
|
def __str__(self):
|
|
return self._as_str(fields='index name mute')
|
|
|
|
class PulseSourceInfo(PulseObject):
|
|
c_struct_fields = ( 'index name mute'
|
|
' description sample_spec owner_module latency driver monitor_of_sink'
|
|
' monitor_of_sink_name flags configured_latency card' )
|
|
|
|
def __str__(self):
|
|
return self._as_str(self.volume, fields='index name description mute')
|
|
|
|
class PulseSourceOutputInfo(PulseObject):
|
|
c_struct_fields = ( 'index name mute corked client'
|
|
' owner_module source sample_spec'
|
|
' buffer_usec source_usec resample_method driver' )
|
|
|
|
def __str__(self):
|
|
return self._as_str(fields='index name mute')
|
|
|
|
class PulseCardProfileInfo(PulseObject):
|
|
c_struct_fields = 'name description n_sinks n_sources priority available'
|
|
|
|
class PulseCardPortInfo(PulsePortInfo):
|
|
c_struct_fields = 'name description available priority direction latency_offset'
|
|
|
|
def _init_from_struct(self, struct):
|
|
super(PulseCardPortInfo, self)._init_from_struct(struct)
|
|
self.direction = PulseDirectionEnum._c_val(struct.direction)
|
|
|
|
class PulseCardInfo(PulseObject):
|
|
c_struct_fields = 'name index driver owner_module n_profiles'
|
|
c_struct_wrappers = {PulsePortInfo: PulseCardPortInfo}
|
|
|
|
def __init__(self, struct):
|
|
super(PulseCardInfo, self).__init__(struct)
|
|
self.profile_list = list(
|
|
PulseCardProfileInfo(struct.profiles2[n][0]) for n in range(self.n_profiles) )
|
|
self.profile_active = PulseCardProfileInfo(struct.active_profile2.contents)
|
|
|
|
def __str__(self):
|
|
return self._as_str(
|
|
fields='name index driver n_profiles',
|
|
profile_active='[{}]'.format(self.profile_active.name) )
|
|
|
|
class PulseVolumeInfo(PulseObject):
|
|
|
|
def __init__(self, struct_or_values=None, channels=None):
|
|
if is_num(struct_or_values):
|
|
assert channels is not None, 'Channel count specified if volume value is not a list.'
|
|
self.values = [struct_or_values] * channels
|
|
elif is_list(struct_or_values): self.values = struct_or_values
|
|
else:
|
|
self.values = list( (x / c.PA_VOLUME_NORM)
|
|
for x in map(float, struct_or_values.values[:struct_or_values.channels]) )
|
|
|
|
@property
|
|
def value_flat(self): return (sum(self.values) / float(len(self.values))) if self.values else 0
|
|
@value_flat.setter
|
|
def value_flat(self, v): self.values = [v] * len(self.values)
|
|
|
|
def to_struct(self):
|
|
return c.PA_CVOLUME(
|
|
len(self.values), tuple(min( c.PA_VOLUME_UI_MAX,
|
|
int(round(v * c.PA_VOLUME_NORM)) ) for v in self.values) )
|
|
|
|
def __str__(self):
|
|
return self._as_str(
|
|
channels=len(self.values), volumes='[{}]'.format(
|
|
' '.join('{}%'.format(int(round(v*100))) for v in self.values) ) )
|
|
|
|
class PulseExtStreamRestoreInfo(PulseObject):
|
|
c_struct_fields = 'name channel_map volume mute device'
|
|
|
|
@classmethod
|
|
def struct_from_value( cls, name, volume,
|
|
channel_list=None, mute=False, device=None ):
|
|
'Same arguments as with class instance init.'
|
|
chan_map = c.PA_CHANNEL_MAP()
|
|
if not channel_list: c.pa.channel_map_init_mono(chan_map)
|
|
else:
|
|
if not is_str(channel_list):
|
|
channel_list = b','.join(map(c.force_bytes, channel_list))
|
|
c.pa.channel_map_parse(chan_map, channel_list)
|
|
if not isinstance(volume, PulseVolumeInfo):
|
|
volume = PulseVolumeInfo(volume, chan_map.channels)
|
|
struct = c.PA_EXT_STREAM_RESTORE_INFO(
|
|
name=c.force_bytes(name),
|
|
mute=int(bool(mute)), device=c.force_bytes(device),
|
|
channel_map=chan_map, volume=volume.to_struct() )
|
|
return struct
|
|
|
|
def __init__( self, struct_or_name=None,
|
|
volume=None, channel_list=None, mute=False, device=None ):
|
|
'''If string name is passed instead of C struct, will be initialized from args/kws.
|
|
"volume" can be either a float number
|
|
(same level for all channels) or list (value per channel).
|
|
"channel_list" can be a pulse channel map string (comma-separated) or list
|
|
of channel names. Defaults to stereo map, should probably match volume channels.
|
|
"device" - name of sink/source or None (default).'''
|
|
if is_str(struct_or_name):
|
|
struct_or_name = self.struct_from_value(
|
|
struct_or_name, volume, channel_list, mute, device )
|
|
super(PulseExtStreamRestoreInfo, self).__init__(struct_or_name)
|
|
|
|
def to_struct(self):
|
|
return self.struct_from_value(**dict(
|
|
(k, getattr(self, k)) for k in 'name volume channel_list mute device'.split() ))
|
|
|
|
def __str__(self):
|
|
return self._as_str(self.volume, fields='name mute device')
|
|
|
|
class PulseEventInfo(PulseObject):
|
|
|
|
def __init__(self, ev_t, facility, index):
|
|
self.t, self.facility, self.index = ev_t, facility, index
|
|
|
|
def __str__(self):
|
|
return self._as_str(fields='t facility index'.split())
|
|
|
|
|
|
class Pulse(object):
|
|
|
|
_ctx = None
|
|
|
|
def __init__(self, client_name=None, server=None, connect=True, threading_lock=False):
|
|
'''Connects to specified pulse server by default.
|
|
Specifying "connect=False" here prevents that, but be sure to call connect() later.
|
|
"connect=False" can also be used here to
|
|
have control over options passed to connect() method.
|
|
"threading_lock" option (either bool or lock instance) can be used to wrap
|
|
non-threadsafe eventloop polling (can only be done from one thread at a time)
|
|
into a mutex lock, and should only be needed if same-instance methods
|
|
will/should/might be called from different threads at the same time.'''
|
|
self.name = client_name or 'pulsectl'
|
|
self.server, self.connected = server, None
|
|
self._ret = self._ctx = self._loop = self._api = None
|
|
self._actions, self._action_ids = dict(),\
|
|
it.chain.from_iterable(map(range, it.repeat(2**30)))
|
|
self.init()
|
|
if threading_lock:
|
|
if threading_lock is True:
|
|
import threading
|
|
threading_lock = threading.Lock()
|
|
self._loop_lock = threading_lock
|
|
if connect:
|
|
try: self.connect(autospawn=True)
|
|
except PulseError:
|
|
self.close()
|
|
raise
|
|
|
|
def init(self):
|
|
self._pa_state_cb = c.PA_STATE_CB_T(self._pulse_state_cb)
|
|
self._pa_subscribe_cb = c.PA_SUBSCRIBE_CB_T(self._pulse_subscribe_cb)
|
|
|
|
self._loop, self._loop_lock = c.pa.mainloop_new(), FakeLock()
|
|
self._loop_running = self._loop_closed = False
|
|
self._api = c.pa.mainloop_get_api(self._loop)
|
|
self._ret = c.pa.return_value()
|
|
|
|
self._ctx_init()
|
|
self.event_types = sorted(PulseEventTypeEnum._values.values())
|
|
self.event_facilities = sorted(PulseEventFacilityEnum._values.values())
|
|
self.event_masks = sorted(PulseEventMaskEnum._values.values())
|
|
self.event_callback = None
|
|
|
|
chan_names = dict()
|
|
for n in range(256):
|
|
name = c.pa.channel_position_to_string(n)
|
|
if name is None: break
|
|
chan_names[n] = name
|
|
self.channel_list_enum = Enum('channel_pos', chan_names)
|
|
|
|
def _ctx_init(self):
|
|
if self._ctx:
|
|
with self._loop_lock:
|
|
self.disconnect()
|
|
c.pa.context_unref(self._ctx)
|
|
self._ctx = c.pa.context_new(self._api, self.name)
|
|
c.pa.context_set_state_callback(self._ctx, self._pa_state_cb, None)
|
|
c.pa.context_set_subscribe_callback(self._ctx, self._pa_subscribe_cb, None)
|
|
|
|
def connect(self, autospawn=False, wait=False, timeout=None):
|
|
'''Connect to pulseaudio server.
|
|
"autospawn" option will start new pulse daemon, if necessary.
|
|
Specifying "wait" option will make function block until pulseaudio server appears.
|
|
"timeout" (in seconds) will raise PulseError if connection not established within it.'''
|
|
if self._loop_closed:
|
|
raise PulseError('Eventloop object was already'
|
|
' destroyed and cannot be reused from this instance.')
|
|
if self.connected is not None: self._ctx_init()
|
|
flags, self.connected = 0, None
|
|
if not autospawn: flags |= c.PA_CONTEXT_NOAUTOSPAWN
|
|
if wait: flags |= c.PA_CONTEXT_NOFAIL
|
|
try: c.pa.context_connect(self._ctx, self.server, flags, None)
|
|
except c.pa.CallError: self.connected = False
|
|
if not timeout: # simplier process
|
|
while self.connected is None: self._pulse_iterate()
|
|
else:
|
|
self._loop_stop, delta, ts_deadline = True, 1, c.mono_time() + timeout
|
|
while self.connected is None:
|
|
delta = ts_deadline - c.mono_time()
|
|
self._pulse_poll(delta)
|
|
if delta <= 0: break
|
|
self._loop_stop = False
|
|
if not self.connected:
|
|
c.pa.context_disconnect(self._ctx)
|
|
while self.connected is not False: self._pulse_iterate()
|
|
raise PulseError('Timed-out connecting to pulseaudio server [{:,.1f}s]'.format(timeout))
|
|
if self.connected is False: raise PulseError('Failed to connect to pulseaudio server')
|
|
|
|
def disconnect(self):
|
|
if not self._ctx or not self.connected: return
|
|
c.pa.context_disconnect(self._ctx)
|
|
|
|
def close(self):
|
|
if not self._loop: return
|
|
if self._loop_running: # called from another thread
|
|
self._loop_closed = True
|
|
c.pa.mainloop_quit(self._loop, 0)
|
|
return # presumably will be closed in a thread that's running it
|
|
with self._loop_lock:
|
|
try:
|
|
self.disconnect()
|
|
c.pa.context_unref(self._ctx)
|
|
c.pa.mainloop_free(self._loop)
|
|
finally: self._ctx = self._loop = None
|
|
|
|
def __enter__(self): return self
|
|
def __exit__(self, err_t, err, err_tb): self.close()
|
|
|
|
|
|
def _pulse_state_cb(self, ctx, userdata):
|
|
state = c.pa.context_get_state(ctx)
|
|
if state >= c.PA_CONTEXT_READY:
|
|
if state == c.PA_CONTEXT_READY: self.connected = True
|
|
elif state in [c.PA_CONTEXT_FAILED, c.PA_CONTEXT_TERMINATED]:
|
|
self.connected, self._loop_stop = False, True
|
|
|
|
def _pulse_subscribe_cb(self, ctx, ev, idx, userdata):
|
|
if not self.event_callback: return
|
|
n = ev & c.PA_SUBSCRIPTION_EVENT_FACILITY_MASK
|
|
ev_fac = PulseEventFacilityEnum._c_val(n, 'ev.facility.{}'.format(n))
|
|
n = ev & c.PA_SUBSCRIPTION_EVENT_TYPE_MASK
|
|
ev_t = PulseEventTypeEnum._c_val(n, 'ev.type.{}'.format(n))
|
|
try: self.event_callback(PulseEventInfo(ev_t, ev_fac, idx))
|
|
except PulseLoopStop: self._loop_stop = True
|
|
|
|
def _pulse_poll_cb(self, func, func_err, ufds, nfds, timeout, userdata):
|
|
fd_list = list(ufds[n] for n in range(nfds))
|
|
try: nfds = func(fd_list, timeout / 1000.0)
|
|
except Exception as err:
|
|
func_err(*sys.exc_info())
|
|
return -1
|
|
return nfds
|
|
|
|
@contextmanager
|
|
def _pulse_loop(self):
|
|
with self._loop_lock:
|
|
if not self._loop: return
|
|
if self._loop_running:
|
|
raise PulseError(
|
|
'Running blocking pulse operations from pulse eventloop callbacks'
|
|
' or other threads while loop is running is not supported by this python module.'
|
|
' Supporting this would require threads or proper asyncio/twisted-like async code.'
|
|
' Workaround can be to stop the loop'
|
|
' (raise PulseLoopStop in callback or event_loop_stop() from another thread),'
|
|
' doing whatever pulse calls synchronously and then resuming event_listen() loop.' )
|
|
self._loop_running, self._loop_stop = True, False
|
|
try: yield self._loop
|
|
finally:
|
|
self._loop_running = False
|
|
if self._loop_closed: self.close() # to free() after stopping it
|
|
|
|
def _pulse_run(self):
|
|
with self._pulse_loop() as loop: c.pa.mainloop_run(loop, self._ret)
|
|
|
|
def _pulse_iterate(self, block=True):
|
|
with self._pulse_loop() as loop: c.pa.mainloop_iterate(loop, int(block), self._ret)
|
|
|
|
@contextmanager
|
|
def _pulse_op_cb(self, raw=False):
|
|
act_id = next(self._action_ids)
|
|
self._actions[act_id] = None
|
|
try:
|
|
cb = lambda s=True,k=act_id: self._actions.update({k: bool(s)})
|
|
if not raw: cb = c.PA_CONTEXT_SUCCESS_CB_T(lambda ctx,s,d,cb=cb: cb(s))
|
|
yield cb
|
|
while self.connected and self._actions[act_id] is None: self._pulse_iterate()
|
|
if not self._actions[act_id]: raise PulseOperationFailed(act_id)
|
|
finally: self._actions.pop(act_id, None)
|
|
|
|
def _pulse_poll(self, timeout=None):
|
|
'''timeout should be in seconds (float),
|
|
0 for non-blocking poll and None (default) for no timeout.'''
|
|
with self._pulse_loop() as loop:
|
|
ts = c.mono_time()
|
|
ts_deadline = timeout and (ts + timeout)
|
|
while True:
|
|
delay = max(0, int((ts_deadline - ts) * 1000000)) if ts_deadline else -1
|
|
c.pa.mainloop_prepare(loop, delay) # delay in us
|
|
c.pa.mainloop_poll(loop)
|
|
if self._loop_closed: break # interrupted by close() or such
|
|
c.pa.mainloop_dispatch(loop)
|
|
if self._loop_stop: break
|
|
ts = c.mono_time()
|
|
if ts_deadline and ts >= ts_deadline: break
|
|
|
|
|
|
def _pulse_info_cb(self, info_cls, data_list, done_cb, ctx, info, eof, userdata):
|
|
# No idea where callbacks with "userdata != NULL" come from,
|
|
# but "info" pointer in them is always invalid, so they are discarded here.
|
|
# Looks like some kind of mixup or corruption in libpulse memory?
|
|
# See also: https://github.com/mk-fg/python-pulse-control/issues/35
|
|
if userdata is not None: return
|
|
# Empty result list and conn issues are checked elsewhere.
|
|
# Errors here are non-descriptive (errno), so should not be useful anyway.
|
|
# if eof < 0: done_cb(s=False)
|
|
if eof: done_cb()
|
|
else: data_list.append(info_cls(info[0]))
|
|
|
|
def _pulse_get_list(cb_t, pulse_func, info_cls, singleton=False, index_arg=True):
|
|
def _wrapper_method(self, index=None):
|
|
data = list()
|
|
with self._pulse_op_cb(raw=True) as cb:
|
|
cb = cb_t(
|
|
ft.partial(self._pulse_info_cb, info_cls, data, cb) if not singleton else
|
|
lambda ctx, info, userdata, cb=cb: data.append(info_cls(info[0])) or cb() )
|
|
pa_op = pulse_func( self._ctx,
|
|
*([index, cb, None] if index is not None else [cb, None]) )
|
|
c.pa.operation_unref(pa_op)
|
|
data = data or list()
|
|
if index is not None or singleton:
|
|
if not data: raise PulseIndexError(index)
|
|
data, = data
|
|
return data
|
|
return wrapper_with_sig_info( None, _wrapper_method,
|
|
not (pulse_func.__name__.endswith('_list') or singleton or not index_arg) )
|
|
|
|
get_sink_by_name = _pulse_get_list(
|
|
c.PA_SINK_INFO_CB_T,
|
|
c.pa.context_get_sink_info_by_name, PulseSinkInfo )
|
|
get_source_by_name = _pulse_get_list(
|
|
c.PA_SOURCE_INFO_CB_T,
|
|
c.pa.context_get_source_info_by_name, PulseSourceInfo )
|
|
get_card_by_name = _pulse_get_list(
|
|
c.PA_CARD_INFO_CB_T,
|
|
c.pa.context_get_card_info_by_name, PulseCardInfo )
|
|
|
|
sink_input_list = _pulse_get_list(
|
|
c.PA_SINK_INPUT_INFO_CB_T,
|
|
c.pa.context_get_sink_input_info_list, PulseSinkInputInfo )
|
|
sink_input_info = _pulse_get_list(
|
|
c.PA_SINK_INPUT_INFO_CB_T,
|
|
c.pa.context_get_sink_input_info, PulseSinkInputInfo )
|
|
source_output_list = _pulse_get_list(
|
|
c.PA_SOURCE_OUTPUT_INFO_CB_T,
|
|
c.pa.context_get_source_output_info_list, PulseSourceOutputInfo )
|
|
source_output_info = _pulse_get_list(
|
|
c.PA_SOURCE_OUTPUT_INFO_CB_T,
|
|
c.pa.context_get_source_output_info, PulseSourceOutputInfo )
|
|
|
|
sink_list = _pulse_get_list(
|
|
c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_list, PulseSinkInfo )
|
|
sink_info = _pulse_get_list(
|
|
c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_by_index, PulseSinkInfo )
|
|
source_list = _pulse_get_list(
|
|
c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_list, PulseSourceInfo )
|
|
source_info = _pulse_get_list(
|
|
c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_by_index, PulseSourceInfo )
|
|
card_list = _pulse_get_list(
|
|
c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_list, PulseCardInfo )
|
|
card_info = _pulse_get_list(
|
|
c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_by_index, PulseCardInfo )
|
|
client_list = _pulse_get_list(
|
|
c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info_list, PulseClientInfo )
|
|
client_info = _pulse_get_list(
|
|
c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info, PulseClientInfo )
|
|
server_info = _pulse_get_list(
|
|
c.PA_SERVER_INFO_CB_T, c.pa.context_get_server_info, PulseServerInfo, singleton=True )
|
|
module_info = _pulse_get_list(
|
|
c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info, PulseModuleInfo )
|
|
module_list = _pulse_get_list(
|
|
c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info_list, PulseModuleInfo )
|
|
|
|
|
|
def _pulse_method_call(pulse_op, func=None, index_arg=True):
|
|
'''Creates following synchronous wrapper for async pa_operation callable:
|
|
wrapper(index, ...) -> pulse_op(index, [*]args_func(...))
|
|
index_arg=False: wrapper(...) -> pulse_op([*]args_func(...))'''
|
|
def _wrapper(self, *args, **kws):
|
|
if index_arg:
|
|
if 'index' in kws: index = kws.pop('index')
|
|
else: index, args = args[0], args[1:]
|
|
pulse_args = func(*args, **kws) if func else list()
|
|
if not is_list(pulse_args): pulse_args = [pulse_args]
|
|
if index_arg: pulse_args = [index] + list(pulse_args)
|
|
with self._pulse_op_cb() as cb:
|
|
try: pulse_op(self._ctx, *(list(pulse_args) + [cb, None]))
|
|
except c.ArgumentError as err: raise TypeError(err.args)
|
|
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
|
|
return wrapper_with_sig_info(func, _wrapper, index_arg)
|
|
|
|
card_profile_set_by_index = _pulse_method_call(
|
|
c.pa.context_set_card_profile_by_index, lambda profile_name: profile_name )
|
|
|
|
sink_default_set = _pulse_method_call(
|
|
c.pa.context_set_default_sink, index_arg=False,
|
|
func=lambda sink: sink.name if isinstance(sink, PulseSinkInfo) else sink )
|
|
source_default_set = _pulse_method_call(
|
|
c.pa.context_set_default_source, index_arg=False,
|
|
func=lambda source: source.name if isinstance(source, PulseSourceInfo) else source )
|
|
|
|
sink_input_mute = _pulse_method_call(
|
|
c.pa.context_set_sink_input_mute, lambda mute=True: mute )
|
|
sink_input_move = _pulse_method_call(
|
|
c.pa.context_move_sink_input_by_index, lambda sink_index: sink_index )
|
|
sink_mute = _pulse_method_call(
|
|
c.pa.context_set_sink_mute_by_index, lambda mute=True: mute )
|
|
sink_input_volume_set = _pulse_method_call(
|
|
c.pa.context_set_sink_input_volume, lambda vol: vol.to_struct() )
|
|
sink_volume_set = _pulse_method_call(
|
|
c.pa.context_set_sink_volume_by_index, lambda vol: vol.to_struct() )
|
|
sink_suspend = _pulse_method_call(
|
|
c.pa.context_suspend_sink_by_index, lambda suspend=True: suspend )
|
|
sink_port_set = _pulse_method_call(
|
|
c.pa.context_set_sink_port_by_index,
|
|
lambda port: port.name if isinstance(port, PulsePortInfo) else port )
|
|
|
|
source_output_mute = _pulse_method_call(
|
|
c.pa.context_set_source_output_mute, lambda mute=True: mute )
|
|
source_output_move = _pulse_method_call(
|
|
c.pa.context_move_source_output_by_index, lambda sink_index: sink_index )
|
|
source_mute = _pulse_method_call(
|
|
c.pa.context_set_source_mute_by_index, lambda mute=True: mute )
|
|
source_output_volume_set = _pulse_method_call(
|
|
c.pa.context_set_source_output_volume, lambda vol: vol.to_struct() )
|
|
source_volume_set = _pulse_method_call(
|
|
c.pa.context_set_source_volume_by_index, lambda vol: vol.to_struct() )
|
|
source_suspend = _pulse_method_call(
|
|
c.pa.context_suspend_source_by_index, lambda suspend=True: suspend )
|
|
source_port_set = _pulse_method_call(
|
|
c.pa.context_set_source_port_by_index,
|
|
lambda port: port.name if isinstance(port, PulsePortInfo) else port )
|
|
|
|
|
|
def module_load(self, name, args=''):
|
|
if is_list(args): args = ' '.join(args)
|
|
name, args = map(c.force_bytes, [name, args])
|
|
data = list()
|
|
with self._pulse_op_cb(raw=True) as cb:
|
|
cb = c.PA_CONTEXT_INDEX_CB_T(
|
|
lambda ctx, index, userdata, cb=cb: data.append(index) or cb() )
|
|
try: c.pa.context_load_module(self._ctx, name, args, cb, None)
|
|
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
|
|
index, = data
|
|
if index == c.PA_INVALID:
|
|
raise PulseError('Failed to load module: {} {}'.format(name, args))
|
|
return index
|
|
|
|
module_unload = _pulse_method_call(c.pa.context_unload_module, None)
|
|
|
|
|
|
def stream_restore_test(self):
|
|
'Returns module-stream-restore version int (e.g. 1) or None if it is unavailable.'
|
|
data = list()
|
|
with self._pulse_op_cb(raw=True) as cb:
|
|
cb = c.PA_EXT_STREAM_RESTORE_TEST_CB_T(
|
|
lambda ctx, version, userdata, cb=cb: data.append(version) or cb() )
|
|
try: c.pa.ext_stream_restore_test(self._ctx, cb, None)
|
|
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
|
|
version, = data
|
|
return version if version != c.PA_INVALID else None
|
|
|
|
stream_restore_read = _pulse_get_list(
|
|
c.PA_EXT_STREAM_RESTORE_READ_CB_T,
|
|
c.pa.ext_stream_restore_read, PulseExtStreamRestoreInfo, index_arg=False )
|
|
stream_restore_list = stream_restore_read # for consistency with other *_list methods
|
|
|
|
@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_write, index_arg=False)
|
|
def stream_restore_write( obj_name_or_list,
|
|
mode='merge', apply_immediately=False, **obj_kws ):
|
|
'''Update module-stream-restore db entry for specified name.
|
|
Can be passed PulseExtStreamRestoreInfo object or list of them as argument,
|
|
or name string there and object init keywords (e.g. volume, mute, channel_list, etc).
|
|
"mode" is PulseUpdateEnum value of
|
|
'merge' (default), 'replace' or 'set' (replaces ALL entries!!!).'''
|
|
mode = PulseUpdateEnum[mode]._c_val
|
|
if is_str(obj_name_or_list):
|
|
obj_name_or_list = PulseExtStreamRestoreInfo(obj_name_or_list, **obj_kws)
|
|
if isinstance(obj_name_or_list, PulseExtStreamRestoreInfo):
|
|
obj_name_or_list = [obj_name_or_list]
|
|
# obj_array is an array of structs, laid out contiguously in memory, not pointers
|
|
obj_array = (c.PA_EXT_STREAM_RESTORE_INFO * len(obj_name_or_list))()
|
|
for n, obj in enumerate(obj_name_or_list):
|
|
obj_struct, dst_struct = obj.to_struct(), obj_array[n]
|
|
for k,t in obj_struct._fields_: setattr(dst_struct, k, getattr(obj_struct, k))
|
|
return mode, obj_array, len(obj_array), int(bool(apply_immediately))
|
|
|
|
@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_delete, index_arg=False)
|
|
def stream_restore_delete(obj_name_or_list):
|
|
'''Can be passed string name,
|
|
PulseExtStreamRestoreInfo object or a list of any of these.'''
|
|
if is_str(obj_name_or_list, PulseExtStreamRestoreInfo):
|
|
obj_name_or_list = [obj_name_or_list]
|
|
name_list = list((obj.name if isinstance( obj,
|
|
PulseExtStreamRestoreInfo ) else obj) for obj in obj_name_or_list)
|
|
name_struct = (c.c_char_p * len(name_list))()
|
|
name_struct[:] = list(map(c.force_bytes, name_list))
|
|
return [name_struct]
|
|
|
|
|
|
def default_set(self, obj):
|
|
'Set passed sink or source to be used as default one by pulseaudio server.'
|
|
assert_pulse_object(obj)
|
|
method = {
|
|
PulseSinkInfo: self.sink_default_set,
|
|
PulseSourceInfo: self.source_default_set }.get(type(obj))
|
|
if not method: raise NotImplementedError(type(obj))
|
|
method(obj)
|
|
|
|
def mute(self, obj, mute=True):
|
|
assert_pulse_object(obj)
|
|
method = {
|
|
PulseSinkInfo: self.sink_mute,
|
|
PulseSinkInputInfo: self.sink_input_mute,
|
|
PulseSourceInfo: self.source_mute,
|
|
PulseSourceOutputInfo: self.source_output_mute }.get(type(obj))
|
|
if not method: raise NotImplementedError(type(obj))
|
|
method(obj.index, mute)
|
|
obj.mute = mute
|
|
|
|
def port_set(self, obj, port):
|
|
assert_pulse_object(obj)
|
|
method = {
|
|
PulseSinkInfo: self.sink_port_set,
|
|
PulseSourceInfo: self.source_port_set }.get(type(obj))
|
|
if not method: raise NotImplementedError(type(obj))
|
|
method(obj.index, port)
|
|
obj.port_active = port
|
|
|
|
def card_profile_set(self, card, profile):
|
|
assert_pulse_object(card)
|
|
if is_str(profile):
|
|
profile_dict = dict((p.name, p) for p in card.profile_list)
|
|
if profile not in profile_dict:
|
|
raise PulseIndexError( 'Card does not have'
|
|
' profile with specified name: {!r}'.format(profile) )
|
|
profile = profile_dict[profile]
|
|
self.card_profile_set_by_index(card.index, profile.name)
|
|
card.profile_active = profile
|
|
|
|
def volume_set(self, obj, vol):
|
|
assert_pulse_object(obj)
|
|
method = {
|
|
PulseSinkInfo: self.sink_volume_set,
|
|
PulseSinkInputInfo: self.sink_input_volume_set,
|
|
PulseSourceInfo: self.source_volume_set,
|
|
PulseSourceOutputInfo: self.source_output_volume_set }.get(type(obj))
|
|
if not method: raise NotImplementedError(type(obj))
|
|
method(obj.index, vol)
|
|
obj.volume = vol
|
|
|
|
def volume_set_all_chans(self, obj, vol):
|
|
assert_pulse_object(obj)
|
|
obj.volume.value_flat = vol
|
|
self.volume_set(obj, obj.volume)
|
|
|
|
def volume_change_all_chans(self, obj, inc):
|
|
assert_pulse_object(obj)
|
|
obj.volume.values = [max(0, v + inc) for v in obj.volume.values]
|
|
self.volume_set(obj, obj.volume)
|
|
|
|
def volume_get_all_chans(self, obj):
|
|
# Purpose of this func can be a bit confusing, being here next to set/change ones
|
|
'''Get "flat" volume float value for info-object as a mean of all channel values.
|
|
Note that this DOES NOT query any kind of updated values from libpulse,
|
|
and simply returns value(s) stored in passed object, i.e. same ones for same object.'''
|
|
assert_pulse_object(obj)
|
|
return obj.volume.value_flat
|
|
|
|
|
|
def event_mask_set(self, *masks):
|
|
mask = 0
|
|
for m in masks: mask |= PulseEventMaskEnum[m]._c_val
|
|
with self._pulse_op_cb() as cb:
|
|
c.pa.context_subscribe(self._ctx, mask, cb, None)
|
|
|
|
def event_callback_set(self, func):
|
|
'''Call event_listen() to start receiving these,
|
|
and be sure to raise PulseLoopStop in a callback to stop the loop.
|
|
Callback should accept single argument - PulseEventInfo instance.
|
|
Passing None will disable the thing.'''
|
|
self.event_callback = func
|
|
|
|
def event_listen(self, timeout=None, raise_on_disconnect=True):
|
|
'''Does not return until PulseLoopStop
|
|
gets raised in event callback or timeout passes.
|
|
timeout should be in seconds (float),
|
|
0 for non-blocking poll and None (default) for no timeout.
|
|
raise_on_disconnect causes PulseDisconnected exceptions by default.
|
|
Do not run any pulse operations from these callbacks.'''
|
|
assert self.event_callback
|
|
try: self._pulse_poll(timeout)
|
|
except c.pa.CallError: pass # e.g. from mainloop_dispatch() on disconnect
|
|
if raise_on_disconnect and not self.connected: raise PulseDisconnected()
|
|
|
|
def event_listen_stop(self):
|
|
'''Stop event_listen() loop from e.g. another thread.
|
|
Does nothing if libpulse poll is not running yet, so might be racey with
|
|
event_listen() - be sure to call it in a loop until event_listen returns or something.'''
|
|
self._loop_stop = True
|
|
c.pa.mainloop_wakeup(self._loop)
|
|
|
|
|
|
def set_poll_func(self, func, func_err_handler=None):
|
|
'''Can be used to integrate pulse client into existing eventloop.
|
|
Function will be passed a list of pollfd structs and timeout value (seconds, float),
|
|
which it is responsible to use and modify (set poll flags) accordingly,
|
|
returning int value >= 0 with number of fds that had any new events within timeout.
|
|
func_err_handler defaults to traceback.print_exception(),
|
|
and will be called on any exceptions from callback (to e.g. log these),
|
|
returning poll error code (-1) to libpulse after that.'''
|
|
if not func_err_handler: func_err_handler = traceback.print_exception
|
|
self._pa_poll_cb = c.PA_POLL_FUNC_T(ft.partial(self._pulse_poll_cb, func, func_err_handler))
|
|
c.pa.mainloop_set_poll_func(self._loop, self._pa_poll_cb, None)
|
|
|
|
|
|
def get_peak_sample(self, source, timeout, stream_idx=None):
|
|
'''Returns peak (max) value in 0-1.0 range for samples in source/stream within timespan.
|
|
"source" can be either int index of pulseaudio source
|
|
(i.e. source.index), its name (source.name), or None to use default source.
|
|
Resulting value is what pulseaudio returns as
|
|
PA_SAMPLE_FLOAT32NE float after "timeout" seconds.
|
|
If specified source does not exist, 0 should be returned after timeout.
|
|
This can be used to detect if there's any sound
|
|
on the microphone or any sound played through a sink via its monitor_source index,
|
|
or same for any specific stream connected to these (if "stream_idx" is passed).
|
|
Sample stream masquerades as
|
|
application.id=org.PulseAudio.pavucontrol to avoid being listed in various mixer apps.
|
|
Example - get peak for specific sink input "si" for 0.8 seconds:
|
|
pulse.get_peak_sample(pulse.sink_info(si.sink).monitor_source, 0.8, si.index)'''
|
|
samples, proplist = [0], c.pa.proplist_from_string('application.id=org.PulseAudio.pavucontrol')
|
|
ss = c.PA_SAMPLE_SPEC(format=c.PA_SAMPLE_FLOAT32NE, rate=25, channels=1)
|
|
s = c.pa.stream_new_with_proplist(self._ctx, 'peak detect', c.byref(ss), None, proplist)
|
|
c.pa.proplist_free(proplist)
|
|
|
|
@c.PA_STREAM_REQUEST_CB_T
|
|
def read_cb(s, bs, userdata):
|
|
buff, bs = c.c_void_p(), c.c_int(bs)
|
|
c.pa.stream_peek(s, buff, c.byref(bs))
|
|
try:
|
|
if not buff or bs.value < 4: return
|
|
# This assumes that native byte order for floats is BE, same as pavucontrol
|
|
samples[0] = max(samples[0], c.cast(buff, c.POINTER(c.c_float))[0])
|
|
finally:
|
|
# stream_drop() flushes buffered data (incl. buff=NULL "hole" data)
|
|
# stream.h: "should not be called if the buffer is empty"
|
|
if bs.value: c.pa.stream_drop(s)
|
|
|
|
if stream_idx is not None: c.pa.stream_set_monitor_stream(s, stream_idx)
|
|
c.pa.stream_set_read_callback(s, read_cb, None)
|
|
if source is not None: source = unicode(source).encode('utf-8')
|
|
try:
|
|
c.pa.stream_connect_record( s, source,
|
|
c.PA_BUFFER_ATTR(fragsize=4, maxlength=2**32-1),
|
|
c.PA_STREAM_DONT_MOVE | c.PA_STREAM_PEAK_DETECT |
|
|
c.PA_STREAM_ADJUST_LATENCY | c.PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND )
|
|
except c.pa.CallError:
|
|
c.pa.stream_unref(s)
|
|
raise
|
|
|
|
try: self._pulse_poll(timeout)
|
|
finally:
|
|
try: c.pa.stream_disconnect(s)
|
|
except c.pa.CallError: pass # stream was removed
|
|
|
|
c.pa.stream_unref(s)
|
|
return min(1.0, samples[0])
|
|
|
|
def play_sample(self, name, sink=None, volume=1.0, proplist_str=None):
|
|
'''Play specified sound sample,
|
|
with an optional sink object/name/index, volume and proplist string parameters.
|
|
Sample must be stored on the server in advance, see e.g. "pacmd list-samples".
|
|
See also libcanberra for an easy XDG theme sample loading, storage and playback API.'''
|
|
if isinstance(sink, PulseSinkInfo): sink = sink.index
|
|
sink = str(sink) if sink is not None else None
|
|
proplist = c.pa.proplist_from_string(proplist_str) if proplist_str else None
|
|
volume = int(round(volume*c.PA_VOLUME_NORM))
|
|
with self._pulse_op_cb() as cb:
|
|
try:
|
|
if not proplist:
|
|
c.pa.context_play_sample(self._ctx, name, sink, volume, cb, None)
|
|
else:
|
|
c.pa.context_play_sample_with_proplist(
|
|
self._ctx, name, sink, volume, proplist, cb, None )
|
|
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
|
|
|
|
|
|
def connect_to_cli(server=None, as_file=True, socket_timeout=1.0, attempts=5, retry_delay=0.3):
|
|
'''Returns connected CLI interface socket (as file object, unless as_file=False),
|
|
where one can send same commands (as lines) as to "pacmd" tool
|
|
or pulseaudio startup files (e.g. "default.pa").
|
|
"server" option can be specified to use non-standard unix socket path
|
|
(when passed absolute path string) or remote tcp socket,
|
|
when passed remote host address (to use default port) or (host, port) tuple.
|
|
Be sure to adjust "socket_timeout" option for tcp sockets over laggy internet.
|
|
Returned file object has line-buffered output,
|
|
so there should be no need to use flush() after every command.
|
|
Be sure to read from the socket line-by-line until
|
|
"### EOF" or timeout for commands that have output (e.g. "dump\\n").
|
|
If default server socket is used (i.e. not specified),
|
|
server pid will be signaled to load module-cli between connection attempts.
|
|
Completely separate protocol from the regular API, as wrapped by libpulse.
|
|
PulseError is raised on any failure.'''
|
|
import socket, errno, signal, time
|
|
s, n = None, attempts if attempts > 0 else None
|
|
try:
|
|
pid_path, sock_af, sock_t = None, socket.AF_UNIX, socket.SOCK_STREAM
|
|
if not server: server, pid_path = map(c.pa.runtime_path, ['cli', 'pid'])
|
|
else:
|
|
if not is_list(server):
|
|
server = c.force_str(server)
|
|
if not server.startswith('/'): server = server, 4712 # default port
|
|
if is_list(server):
|
|
try:
|
|
addrinfo = socket.getaddrinfo(
|
|
server[0], server[1], 0, sock_t, socket.IPPROTO_TCP )
|
|
if not addrinfo: raise socket.gaierror('No addrinfo for socket: {}'.format(server))
|
|
except (socket.gaierror, socket.error) as err:
|
|
raise PulseError( 'Failed to resolve socket parameters'
|
|
' (address, family) via getaddrinfo: {!r} - {} {}'.format(server, type(err), err) )
|
|
sock_af, sock_t, _, _, server = addrinfo[0]
|
|
|
|
s = socket.socket(sock_af, sock_t)
|
|
s.settimeout(socket_timeout)
|
|
while True:
|
|
ts = c.mono_time()
|
|
try: s.connect(server)
|
|
except socket.error as err:
|
|
if err.errno not in [errno.ECONNREFUSED, errno.ENOENT, errno.ECONNABORTED]: raise
|
|
else: break
|
|
if n:
|
|
n -= 1
|
|
if n <= 0: raise PulseError('Number of connection attempts ({}) exceeded'.format(attempts))
|
|
if pid_path:
|
|
with open(pid_path) as src: os.kill(int(src.read().strip()), signal.SIGUSR2)
|
|
time.sleep(max(0, retry_delay - (c.mono_time() - ts)))
|
|
|
|
if as_file: res = s.makefile('rw', 1)
|
|
else: res, s = s, None # to avoid closing this socket
|
|
return res
|
|
|
|
except Exception as err: # CallError, socket.error, IOError (pidfile), OSError (os.kill)
|
|
raise PulseError( 'Failed to connect to pulse'
|
|
' cli socket {!r}: {} {}'.format(server, type(err), err) )
|
|
|
|
finally:
|
|
if s: s.close()
|