python-mpv-jsonipc 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_mpv_jsonipc-1.3.0.dist-info → python_mpv_jsonipc-1.5.0.dist-info}/METADATA +5 -1
- python_mpv_jsonipc-1.5.0.dist-info/RECORD +6 -0
- python_mpv_jsonipc.py +639 -85
- python_mpv_jsonipc-1.3.0.dist-info/RECORD +0 -6
- {python_mpv_jsonipc-1.3.0.dist-info → python_mpv_jsonipc-1.5.0.dist-info}/WHEEL +0 -0
- {python_mpv_jsonipc-1.3.0.dist-info → python_mpv_jsonipc-1.5.0.dist-info}/licenses/LICENSE.md +0 -0
- {python_mpv_jsonipc-1.3.0.dist-info → python_mpv_jsonipc-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mpv-jsonipc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python API to MPV using JSON IPC
|
|
5
5
|
Author-email: Izzie Walton <izzie@iwalton.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -55,6 +55,10 @@ mpv.volume = 20
|
|
|
55
55
|
# You can also send commands.
|
|
56
56
|
mpv.command_name(arg1, arg2)
|
|
57
57
|
|
|
58
|
+
# Or send one without waiting for it. Returns a concurrent.futures.Future.
|
|
59
|
+
mpv.command_async("sub-add", "https://example.com/slow.srt",
|
|
60
|
+
callback=lambda error, result: print(error or "loaded"))
|
|
61
|
+
|
|
58
62
|
# Bind to key press events with a decorator
|
|
59
63
|
@mpv.on_key_press("space")
|
|
60
64
|
def space_handler():
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
python_mpv_jsonipc.py,sha256=6xQ45aP3vLNxLy6OdyReB-MPnQvmOJ00CzzNQ13iGS4,61266
|
|
2
|
+
python_mpv_jsonipc-1.5.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
|
|
3
|
+
python_mpv_jsonipc-1.5.0.dist-info/METADATA,sha256=axbDuWhKzIPoR1rvoF-b-MXVJx78LqF2MzFK6CGbmP8,2489
|
|
4
|
+
python_mpv_jsonipc-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
python_mpv_jsonipc-1.5.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
|
|
6
|
+
python_mpv_jsonipc-1.5.0.dist-info/RECORD,,
|
python_mpv_jsonipc.py
CHANGED
|
@@ -8,12 +8,105 @@ import subprocess
|
|
|
8
8
|
import random
|
|
9
9
|
import queue
|
|
10
10
|
import logging
|
|
11
|
+
from concurrent.futures import Future
|
|
11
12
|
|
|
12
13
|
log = logging.getLogger('mpv-jsonipc')
|
|
13
14
|
|
|
14
15
|
if os.name == "nt":
|
|
15
|
-
import
|
|
16
|
-
|
|
16
|
+
import ctypes
|
|
17
|
+
import ctypes.wintypes as _w
|
|
18
|
+
|
|
19
|
+
# The Windows transport calls kernel32 directly rather than going through
|
|
20
|
+
# ``_winapi`` and ``multiprocessing.connection.PipeConnection``. The second
|
|
21
|
+
# of those is a live bug, not tidiness:
|
|
22
|
+
#
|
|
23
|
+
# PipeConnection says outright that "a connection should only be used by
|
|
24
|
+
# a single thread", and this library used it from two -- a reader parked
|
|
25
|
+
# in recv_bytes, and whoever called stop(). Its _close() cancels a
|
|
26
|
+
# pending *send* before closing the handle, but has no equivalent for a
|
|
27
|
+
# pending *read*, because _recv_bytes never stores its OVERLAPPED
|
|
28
|
+
# anywhere reachable. So stop() closed the handle with an overlapped
|
|
29
|
+
# read outstanding and a kernel-owned buffer still live, which is
|
|
30
|
+
# undefined: the kernel may complete the I/O into memory Python is
|
|
31
|
+
# tearing down. CPython 3.13+ allocates its thread primitives from the
|
|
32
|
+
# parking lot, so that corruption now lands as an abort --
|
|
33
|
+
# ``Fatal Python error: _PySemaphore_Wakeup: ReleaseSemaphore failed
|
|
34
|
+
# (error: 6)`` -- seen in CI on both 3.13 and 3.14.
|
|
35
|
+
#
|
|
36
|
+
# Neither ``_winapi`` (private C extension) nor ``PipeConnection`` (absent
|
|
37
|
+
# from __all__, defined conditionally, an implementation detail of Pipe())
|
|
38
|
+
# carries a compatibility contract, so this also removes a dependency that
|
|
39
|
+
# breaks along the *Python* axis rather than ours. ctypes is stdlib and
|
|
40
|
+
# needs no PyInstaller hooks or bundled DLLs, which is the property the
|
|
41
|
+
# original _winapi choice was buying and that downstream projects such as
|
|
42
|
+
# SyncPlay and jellyfin-mpv-shim adopted this library for.
|
|
43
|
+
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
44
|
+
|
|
45
|
+
_GENERIC_READ = 0x80000000
|
|
46
|
+
_GENERIC_WRITE = 0x40000000
|
|
47
|
+
_OPEN_EXISTING = 3
|
|
48
|
+
_FILE_FLAG_OVERLAPPED = 0x40000000
|
|
49
|
+
_INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
|
50
|
+
_INFINITE = 0xFFFFFFFF
|
|
51
|
+
_WAIT_OBJECT_0 = 0
|
|
52
|
+
_ERROR_FILE_NOT_FOUND = 2
|
|
53
|
+
_ERROR_PATH_NOT_FOUND = 3
|
|
54
|
+
_ERROR_BROKEN_PIPE = 109
|
|
55
|
+
# Also "the other end is gone", and reached when the peer disconnects
|
|
56
|
+
# rather than exits. Without them a normal disconnect logs an ERROR and a
|
|
57
|
+
# traceback. Measured on real Windows, including against the old
|
|
58
|
+
# PipeConnection build, which logged it too -- it only raised EOFError for
|
|
59
|
+
# 109 -- so this is an improvement on the previous behaviour rather than a
|
|
60
|
+
# regression being repaired. Real MPV exiting gives 109, which is why no
|
|
61
|
+
# CI leg ever showed it.
|
|
62
|
+
_ERROR_NO_DATA = 232
|
|
63
|
+
_ERROR_PIPE_NOT_CONNECTED = 233
|
|
64
|
+
_ERROR_MORE_DATA = 234
|
|
65
|
+
_ERROR_OPERATION_ABORTED = 995
|
|
66
|
+
_ERROR_IO_PENDING = 997
|
|
67
|
+
|
|
68
|
+
#: Every way the far end can be gone. All of them are a clean end of
|
|
69
|
+
#: stream, not an error to log.
|
|
70
|
+
_PEER_GONE = (_ERROR_BROKEN_PIPE, _ERROR_NO_DATA, _ERROR_PIPE_NOT_CONNECTED)
|
|
71
|
+
|
|
72
|
+
class _OVERLAPPED(ctypes.Structure):
|
|
73
|
+
_fields_ = [("Internal", ctypes.c_void_p),
|
|
74
|
+
("InternalHigh", ctypes.c_void_p),
|
|
75
|
+
("Offset", _w.DWORD),
|
|
76
|
+
("OffsetHigh", _w.DWORD),
|
|
77
|
+
("hEvent", _w.HANDLE)]
|
|
78
|
+
|
|
79
|
+
# Declared explicitly: without argtypes ctypes truncates pointers to int
|
|
80
|
+
# on 64-bit, which fails in ways that look like random handle errors.
|
|
81
|
+
_kernel32.CreateFileW.argtypes = [_w.LPCWSTR, _w.DWORD, _w.DWORD,
|
|
82
|
+
ctypes.c_void_p, _w.DWORD, _w.DWORD,
|
|
83
|
+
_w.HANDLE]
|
|
84
|
+
_kernel32.CreateFileW.restype = _w.HANDLE
|
|
85
|
+
_kernel32.CreateEventW.argtypes = [ctypes.c_void_p, _w.BOOL, _w.BOOL,
|
|
86
|
+
_w.LPCWSTR]
|
|
87
|
+
_kernel32.CreateEventW.restype = _w.HANDLE
|
|
88
|
+
_kernel32.SetEvent.argtypes = [_w.HANDLE]
|
|
89
|
+
_kernel32.SetEvent.restype = _w.BOOL
|
|
90
|
+
_kernel32.ResetEvent.argtypes = [_w.HANDLE]
|
|
91
|
+
_kernel32.ResetEvent.restype = _w.BOOL
|
|
92
|
+
_kernel32.ReadFile.argtypes = [_w.HANDLE, ctypes.c_void_p, _w.DWORD,
|
|
93
|
+
_w.LPDWORD, ctypes.c_void_p]
|
|
94
|
+
_kernel32.ReadFile.restype = _w.BOOL
|
|
95
|
+
_kernel32.WriteFile.argtypes = [_w.HANDLE, ctypes.c_void_p, _w.DWORD,
|
|
96
|
+
_w.LPDWORD, ctypes.c_void_p]
|
|
97
|
+
_kernel32.WriteFile.restype = _w.BOOL
|
|
98
|
+
_kernel32.GetOverlappedResult.argtypes = [_w.HANDLE, ctypes.c_void_p,
|
|
99
|
+
_w.LPDWORD, _w.BOOL]
|
|
100
|
+
_kernel32.GetOverlappedResult.restype = _w.BOOL
|
|
101
|
+
_kernel32.CancelIoEx.argtypes = [_w.HANDLE, ctypes.c_void_p]
|
|
102
|
+
_kernel32.CancelIoEx.restype = _w.BOOL
|
|
103
|
+
_kernel32.WaitForMultipleObjects.argtypes = [_w.DWORD, ctypes.c_void_p,
|
|
104
|
+
_w.BOOL, _w.DWORD]
|
|
105
|
+
_kernel32.WaitForMultipleObjects.restype = _w.DWORD
|
|
106
|
+
_kernel32.CloseHandle.argtypes = [_w.HANDLE]
|
|
107
|
+
_kernel32.CloseHandle.restype = _w.BOOL
|
|
108
|
+
_kernel32.WaitNamedPipeW.argtypes = [_w.LPCWSTR, _w.DWORD]
|
|
109
|
+
_kernel32.WaitNamedPipeW.restype = _w.BOOL
|
|
17
110
|
|
|
18
111
|
TIMEOUT = 120
|
|
19
112
|
|
|
@@ -123,62 +216,220 @@ class WindowsSocket(threading.Thread):
|
|
|
123
216
|
*callback(json_data)* is the function for recieving events.
|
|
124
217
|
*quit_callback* is called when the socket connection dies.
|
|
125
218
|
"""
|
|
219
|
+
# First, so Thread's own attributes are in place before ours rather
|
|
220
|
+
# than landing on top of them. Thread grew a `self._handle` of its own
|
|
221
|
+
# in 3.13, so the pipe handle is `_pipe`: named `_handle` and assigned
|
|
222
|
+
# before this call, Thread silently replaced it with a
|
|
223
|
+
# _thread._ThreadHandle and every read died in ctypes argument
|
|
224
|
+
# conversion. Anything added below needs a name Thread does not use.
|
|
225
|
+
threading.Thread.__init__(self)
|
|
226
|
+
self.daemon = True
|
|
227
|
+
|
|
126
228
|
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
|
127
229
|
self.callback = callback
|
|
128
230
|
self.quit_callback = quit_callback
|
|
129
231
|
self._stopping = False
|
|
130
|
-
|
|
131
|
-
|
|
232
|
+
self._closed = False
|
|
233
|
+
# Every send is held under this lock, so holding it is proof that no
|
|
234
|
+
# write is in flight and the pipe is safe to close. The reader is the
|
|
235
|
+
# exception and must stay one -- see run().
|
|
236
|
+
self._io_lock = threading.RLock()
|
|
237
|
+
# Guards the handle *values* against being used after _close cleared
|
|
238
|
+
# them. Held only for the moment it takes to signal or close, never
|
|
239
|
+
# across a wait.
|
|
240
|
+
self._handle_lock = threading.RLock()
|
|
241
|
+
|
|
242
|
+
access = _GENERIC_READ | _GENERIC_WRITE
|
|
132
243
|
limit = 5 # Connection may fail at first. Try 5 times.
|
|
133
244
|
for _ in range(limit):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
245
|
+
# Still five attempts, and still load-bearing: _ipc_endpoint_ready
|
|
246
|
+
# does not run on the start_mpv=False attach path, and it answers
|
|
247
|
+
# True for a pipe whose instances are all busy, which CreateFile
|
|
248
|
+
# then refuses with ERROR_PIPE_BUSY.
|
|
249
|
+
handle = _kernel32.CreateFileW(
|
|
250
|
+
ipc_socket, access, 0, None, _OPEN_EXISTING,
|
|
251
|
+
_FILE_FLAG_OVERLAPPED, None)
|
|
252
|
+
if handle != _INVALID_HANDLE_VALUE:
|
|
139
253
|
break
|
|
140
|
-
|
|
141
|
-
time.sleep(1)
|
|
254
|
+
time.sleep(1)
|
|
142
255
|
else:
|
|
256
|
+
# Deliberately not an OSError: downstream and the failure tests
|
|
257
|
+
# both pin that this is what the Windows attach path raises.
|
|
143
258
|
raise MPVError("Cannot connect to pipe.")
|
|
144
|
-
self.
|
|
259
|
+
self._pipe = handle
|
|
260
|
+
|
|
261
|
+
# Manual-reset, so a stop stays visible to the reader and to any send
|
|
262
|
+
# waiting alongside it rather than waking exactly one of them.
|
|
263
|
+
self._stop_event = _kernel32.CreateEventW(None, True, False, None)
|
|
264
|
+
self._read_event = _kernel32.CreateEventW(None, True, False, None)
|
|
265
|
+
self._write_event = _kernel32.CreateEventW(None, True, False, None)
|
|
266
|
+
if not all((self._stop_event, self._read_event, self._write_event)):
|
|
267
|
+
self._close()
|
|
268
|
+
raise MPVError("Cannot create pipe events.")
|
|
145
269
|
|
|
146
270
|
if self.callback is None:
|
|
147
271
|
self.callback = lambda data: None
|
|
148
272
|
|
|
149
|
-
|
|
150
|
-
|
|
273
|
+
def _transfer(self, func, event, buf, size):
|
|
274
|
+
"""Run one overlapped operation, waiting for it or for *stop*.
|
|
275
|
+
|
|
276
|
+
Returns the bytes transferred, or None if the pipe ended or *stop* cut
|
|
277
|
+
the operation short.
|
|
278
|
+
|
|
279
|
+
The GetOverlappedResult(wait=True) below is the point of this whole
|
|
280
|
+
class. It does not return until the kernel has finished with *buf* and
|
|
281
|
+
the OVERLAPPED -- including when we cancelled it -- so neither is ever
|
|
282
|
+
released while an I/O could still write into it.
|
|
283
|
+
"""
|
|
284
|
+
ov = _OVERLAPPED()
|
|
285
|
+
ov.hEvent = event
|
|
286
|
+
_kernel32.ResetEvent(event)
|
|
287
|
+
moved = _w.DWORD(0)
|
|
288
|
+
|
|
289
|
+
# The guarded region starts *before* the call, and assumes the
|
|
290
|
+
# operation may be outstanding until proven otherwise. An
|
|
291
|
+
# asynchronous exception -- a KeyboardInterrupt, which lands on the
|
|
292
|
+
# main thread, which is where send() runs -- can arrive between the
|
|
293
|
+
# call returning ERROR_IO_PENDING and any flag recording that, so a
|
|
294
|
+
# flag set after the call is wrong in exactly the direction that
|
|
295
|
+
# matters: it would let the frame, and its buffer, go while the kernel
|
|
296
|
+
# still owns them. Assuming the worst costs nothing when nothing was
|
|
297
|
+
# started, because a fresh OVERLAPPED has Internal = STATUS_SUCCESS
|
|
298
|
+
# and GetOverlappedResult then returns immediately (measured).
|
|
299
|
+
outstanding = True
|
|
300
|
+
try:
|
|
301
|
+
if func(self._pipe, buf, size, ctypes.byref(moved),
|
|
302
|
+
ctypes.byref(ov)):
|
|
303
|
+
outstanding = False
|
|
304
|
+
return moved.value
|
|
305
|
+
|
|
306
|
+
err = ctypes.get_last_error()
|
|
307
|
+
if err != _ERROR_IO_PENDING:
|
|
308
|
+
outstanding = False
|
|
309
|
+
if err in _PEER_GONE:
|
|
310
|
+
return None
|
|
311
|
+
if err == _ERROR_MORE_DATA:
|
|
312
|
+
# A completed read that filled the buffer, not a failure.
|
|
313
|
+
# The completion path below already treats it that way;
|
|
314
|
+
# raising here would have desynchronised the two.
|
|
315
|
+
return moved.value
|
|
316
|
+
raise ctypes.WinError(err)
|
|
317
|
+
|
|
318
|
+
handles = (_w.HANDLE * 2)(event, self._stop_event)
|
|
319
|
+
waitres = _kernel32.WaitForMultipleObjects(2, handles, False,
|
|
320
|
+
_INFINITE)
|
|
321
|
+
if waitres != _WAIT_OBJECT_0:
|
|
322
|
+
# stop() wants us gone, or the wait itself failed. Either way
|
|
323
|
+
# we will not be consuming this operation. Read the wait's
|
|
324
|
+
# error *first*: CancelIoEx overwrites the thread's last error,
|
|
325
|
+
# so reading it afterwards reports the cancel's success and
|
|
326
|
+
# loses the real cause on the path hardest to reproduce.
|
|
327
|
+
waitfail = ctypes.get_last_error()
|
|
328
|
+
_kernel32.CancelIoEx(self._pipe, ctypes.byref(ov))
|
|
329
|
+
if waitres != _WAIT_OBJECT_0 + 1:
|
|
330
|
+
raise ctypes.WinError(waitfail)
|
|
331
|
+
except BaseException:
|
|
332
|
+
if outstanding:
|
|
333
|
+
_kernel32.CancelIoEx(self._pipe, ctypes.byref(ov))
|
|
334
|
+
raise
|
|
335
|
+
finally:
|
|
336
|
+
# Cancelling only *asks*. This is the wait that makes it true.
|
|
337
|
+
if outstanding:
|
|
338
|
+
completed = _kernel32.GetOverlappedResult(
|
|
339
|
+
self._pipe, ctypes.byref(ov), ctypes.byref(moved), True)
|
|
340
|
+
failure = 0 if completed else ctypes.get_last_error()
|
|
341
|
+
|
|
342
|
+
if not completed:
|
|
343
|
+
if failure in _PEER_GONE or failure == _ERROR_OPERATION_ABORTED:
|
|
344
|
+
return None
|
|
345
|
+
if failure != _ERROR_MORE_DATA:
|
|
346
|
+
raise ctypes.WinError(failure)
|
|
347
|
+
return moved.value
|
|
348
|
+
|
|
349
|
+
def _close(self):
|
|
350
|
+
"""Close the handles, once, with no I/O outstanding."""
|
|
351
|
+
# _io_lock first: holding it means no send is mid-write. _handle_lock
|
|
352
|
+
# second, to exclude a concurrent stop() signalling an event we are
|
|
353
|
+
# about to close. Nothing ever takes these in the other order, and
|
|
354
|
+
# _handle_lock is never held across a wait, so stop() cannot block.
|
|
355
|
+
with self._io_lock, self._handle_lock:
|
|
356
|
+
if self._closed:
|
|
357
|
+
return
|
|
358
|
+
self._closed = True
|
|
359
|
+
for name in ("_pipe", "_stop_event", "_read_event",
|
|
360
|
+
"_write_event"):
|
|
361
|
+
handle = getattr(self, name, None)
|
|
362
|
+
if handle:
|
|
363
|
+
_kernel32.CloseHandle(handle)
|
|
364
|
+
# Cleared, not just closed: terminate() is routinely called
|
|
365
|
+
# twice, and a closed handle value can be reused by the OS.
|
|
366
|
+
setattr(self, name, None)
|
|
151
367
|
|
|
152
368
|
def stop(self, join=True):
|
|
153
369
|
"""Terminate the thread."""
|
|
154
370
|
self._stopping = True
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
371
|
+
# Deliberately does NOT close the handle. Closing it from here with
|
|
372
|
+
# the reader's overlapped read still pending is the corruption this
|
|
373
|
+
# class was rewritten to remove; instead we wake the reader and let it
|
|
374
|
+
# close the handle it owns, once its read has provably finished.
|
|
375
|
+
with self._handle_lock:
|
|
376
|
+
if self._stop_event:
|
|
377
|
+
_kernel32.SetEvent(self._stop_event)
|
|
378
|
+
# Two ways join() raises, and both would skip the _close() below and
|
|
379
|
+
# leak all four handles. is_alive() is True for the calling thread, so
|
|
380
|
+
# it never prevented the self-join it looked like it prevented -- the
|
|
381
|
+
# reader reaches here whenever a user's quit_callback calls
|
|
382
|
+
# terminate(). And `ident is None` means start() was never called,
|
|
383
|
+
# where join() raises too.
|
|
384
|
+
if (join and self.ident is not None
|
|
385
|
+
and self is not threading.current_thread()):
|
|
161
386
|
self.join()
|
|
387
|
+
if not self.is_alive():
|
|
388
|
+
# The reader has finished, or never ran. Either way nothing can
|
|
389
|
+
# have I/O outstanding, so the handle is ours to close.
|
|
390
|
+
self._close()
|
|
162
391
|
|
|
163
392
|
def send(self, data):
|
|
164
393
|
"""Send *data* to the pipe, encoded as JSON."""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if len(ex.args) == 1 and ex.args[0] == "handle is closed":
|
|
394
|
+
blob = json.dumps(data).encode('utf-8') + b'\n'
|
|
395
|
+
with self._io_lock:
|
|
396
|
+
if self._closed:
|
|
169
397
|
raise BrokenPipeError("handle is closed")
|
|
170
|
-
|
|
398
|
+
written = self._transfer(_kernel32.WriteFile, self._write_event,
|
|
399
|
+
blob, len(blob))
|
|
400
|
+
if written is None:
|
|
401
|
+
raise BrokenPipeError("handle is closed")
|
|
171
402
|
|
|
172
403
|
def run(self):
|
|
173
404
|
"""Process pipe events. Do not run this directly. Use *start*."""
|
|
405
|
+
buf = ctypes.create_string_buffer(2048)
|
|
174
406
|
data = b''
|
|
175
407
|
try:
|
|
176
|
-
while
|
|
177
|
-
|
|
178
|
-
|
|
408
|
+
while not self._stopping:
|
|
409
|
+
# The stop event is only observed when a read *pends*, and a
|
|
410
|
+
# read that completes synchronously returns before ever
|
|
411
|
+
# reaching the wait. A peer with data always available could
|
|
412
|
+
# therefore keep stop() waiting in join(). Not reproducible --
|
|
413
|
+
# 124k messages of flooding still let the reader park -- but
|
|
414
|
+
# the flag costs nothing and closes the argument.
|
|
415
|
+
#
|
|
416
|
+
# Emphatically NOT under _io_lock: this read is parked
|
|
417
|
+
# whenever MPV has nothing to say, which is almost always, and
|
|
418
|
+
# holding the lock across it would block every send. The lock
|
|
419
|
+
# exists to keep a send and the close apart; the reader needs
|
|
420
|
+
# no such guard because it closes the handle itself, in the
|
|
421
|
+
# finally below, only once its own read has returned.
|
|
422
|
+
nread = self._transfer(_kernel32.ReadFile, self._read_event,
|
|
423
|
+
buf, len(buf))
|
|
424
|
+
if nread is None: # the peer is gone, or stop()
|
|
179
425
|
break
|
|
426
|
+
if nread == 0:
|
|
427
|
+
# A zero-length message, not the end: mpv's pipe is
|
|
428
|
+
# message-mode and it answers an async command with an
|
|
429
|
+
# empty write. Treating it as EOF closed the connection.
|
|
430
|
+
continue
|
|
180
431
|
|
|
181
|
-
data +=
|
|
432
|
+
data += buf.raw[:nread]
|
|
182
433
|
if data[-1] != 10:
|
|
183
434
|
continue
|
|
184
435
|
|
|
@@ -189,15 +440,14 @@ class WindowsSocket(threading.Thread):
|
|
|
189
440
|
json_data = json.loads(item)
|
|
190
441
|
self.callback(json_data)
|
|
191
442
|
data = b''
|
|
192
|
-
except
|
|
193
|
-
if self.quit_callback:
|
|
194
|
-
self.quit_callback()
|
|
195
|
-
except Exception as ex:
|
|
443
|
+
except Exception:
|
|
196
444
|
# Only log if not intentionally stopping
|
|
197
445
|
if not self._stopping:
|
|
198
446
|
log.error("Pipe connection died.", exc_info=1)
|
|
199
|
-
|
|
200
|
-
|
|
447
|
+
finally:
|
|
448
|
+
self._close()
|
|
449
|
+
if self.quit_callback:
|
|
450
|
+
self.quit_callback()
|
|
201
451
|
|
|
202
452
|
class UnixSocket(threading.Thread):
|
|
203
453
|
"""
|
|
@@ -229,28 +479,41 @@ class UnixSocket(threading.Thread):
|
|
|
229
479
|
def stop(self, join=True):
|
|
230
480
|
"""Terminate the thread."""
|
|
231
481
|
self._stopping = True
|
|
232
|
-
|
|
482
|
+
# Bind once. Two threads reach here -- the caller's terminate() and
|
|
483
|
+
# the reader's quit_callback -> terminate(join=False) -- and making
|
|
484
|
+
# stop() wait for MPV widened that overlap, because the wait ends on
|
|
485
|
+
# the same MPV exit that wakes the reader. Re-reading self.socket
|
|
486
|
+
# after the None check let the loser call shutdown/close on None,
|
|
487
|
+
# which is an AttributeError and so is *not* caught below.
|
|
488
|
+
sock, self.socket = self.socket, None
|
|
489
|
+
if sock is not None:
|
|
233
490
|
try:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
self.socket = None
|
|
491
|
+
sock.shutdown(socket.SHUT_WR)
|
|
492
|
+
sock.close()
|
|
237
493
|
except OSError:
|
|
238
494
|
pass # Ignore socket close failure.
|
|
239
|
-
if join
|
|
495
|
+
# Not a bare `if join`: the reader reaches here whenever a user's
|
|
496
|
+
# quit_callback calls terminate(), and joining yourself raises -- which
|
|
497
|
+
# escaped run() and leaked the event handler. `ident is None` is the
|
|
498
|
+
# never-started case, where join() raises as well.
|
|
499
|
+
if (join and self.ident is not None
|
|
500
|
+
and self is not threading.current_thread()):
|
|
240
501
|
self.join()
|
|
241
502
|
|
|
242
503
|
def send(self, data):
|
|
243
504
|
"""Send *data* to the socket, encoded as JSON."""
|
|
244
|
-
|
|
505
|
+
sock = self.socket # bind once; see stop()
|
|
506
|
+
if sock is None:
|
|
245
507
|
raise BrokenPipeError("socket is closed")
|
|
246
|
-
|
|
508
|
+
sock.send(json.dumps(data).encode('utf-8') + b'\n')
|
|
247
509
|
|
|
248
510
|
def run(self):
|
|
249
511
|
"""Process socket events. Do not run this directly. Use *start*."""
|
|
250
512
|
data = b''
|
|
513
|
+
sock = self.socket # bind once; see stop()
|
|
251
514
|
try:
|
|
252
515
|
while True:
|
|
253
|
-
current_data =
|
|
516
|
+
current_data = sock.recv(1024)
|
|
254
517
|
if current_data == b'':
|
|
255
518
|
break
|
|
256
519
|
|
|
@@ -282,26 +545,31 @@ def _ipc_endpoint_ready(ipc_socket):
|
|
|
282
545
|
"""
|
|
283
546
|
if os.name != 'nt':
|
|
284
547
|
return os.path.exists(ipc_socket)
|
|
285
|
-
|
|
286
|
-
_winapi.WaitNamedPipe(ipc_socket, 0)
|
|
548
|
+
if _kernel32.WaitNamedPipeW(ipc_socket, 0):
|
|
287
549
|
return True
|
|
288
|
-
|
|
550
|
+
# Both codes, because the old _winapi version raised FileNotFoundError and
|
|
551
|
+
# CPython maps ERROR_PATH_NOT_FOUND to ENOENT as well. Treating 3 as
|
|
552
|
+
# "present but busy" would report a successful start and then burn
|
|
553
|
+
# WindowsSocket's five CreateFileW retries before failing with the wrong
|
|
554
|
+
# exception for the caller's retry loop.
|
|
555
|
+
if ctypes.get_last_error() in (_ERROR_FILE_NOT_FOUND, _ERROR_PATH_NOT_FOUND):
|
|
289
556
|
return False
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return True
|
|
557
|
+
# Pipe exists but all instances are momentarily busy — it is present.
|
|
558
|
+
return True
|
|
293
559
|
|
|
294
560
|
|
|
295
561
|
class MPVProcess:
|
|
296
562
|
"""
|
|
297
563
|
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
|
298
564
|
"""
|
|
299
|
-
def __init__(self, ipc_socket, mpv_location=None, **kwargs):
|
|
565
|
+
def __init__(self, ipc_socket, mpv_location=None, discard_output=False, **kwargs):
|
|
300
566
|
"""
|
|
301
567
|
Create and start the MPV process. Will block until socket/pipe is available.
|
|
302
568
|
|
|
303
569
|
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
|
304
570
|
*mpv_location* is the path to mpv. If left unset it tries the one in the PATH.
|
|
571
|
+
*discard_output* sends MPV's stdout and stderr to devnull instead of inheriting
|
|
572
|
+
the caller's. (Default: False)
|
|
305
573
|
|
|
306
574
|
All other arguments are forwarded to MPV as command-line arguments.
|
|
307
575
|
"""
|
|
@@ -338,7 +606,14 @@ class MPVProcess:
|
|
|
338
606
|
args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
|
|
339
607
|
for v in arg_pairs)
|
|
340
608
|
self.argv = args
|
|
341
|
-
|
|
609
|
+
# close_fds only covers fds above 2, so MPV inherits our stdout and
|
|
610
|
+
# stderr. It never writes to them under --terminal=no, which is why
|
|
611
|
+
# this is invisible until something waits for EOF on the other end of
|
|
612
|
+
# that pipe: an MPV that outlives us holds it open and that reader
|
|
613
|
+
# blocks forever. Opt-in rather than default because callers who do
|
|
614
|
+
# want MPV's output on their terminal have had it for years.
|
|
615
|
+
stdio = subprocess.DEVNULL if discard_output else None
|
|
616
|
+
self.process = subprocess.Popen(args, stdout=stdio, stderr=stdio)
|
|
342
617
|
ipc_exists = False
|
|
343
618
|
for _ in range(100): # Give MPV 10 seconds to start.
|
|
344
619
|
time.sleep(0.1)
|
|
@@ -351,11 +626,16 @@ class MPVProcess:
|
|
|
351
626
|
log.error("MPV failed with returncode {0}.".format(self.process.returncode))
|
|
352
627
|
break
|
|
353
628
|
else:
|
|
354
|
-
|
|
629
|
+
# stop(), not a bare terminate(): a failed start orphans MPV
|
|
630
|
+
# exactly like a failed teardown did, and MPV.__init__ retries
|
|
631
|
+
# this up to start_retries times -- so a machine where MPV wedges
|
|
632
|
+
# without opening its pipe used to leave five live processes,
|
|
633
|
+
# each still holding the caller's stdout.
|
|
634
|
+
self._stop_quietly()
|
|
355
635
|
raise MPVProcessError("MPV start timed out.", argv=args)
|
|
356
636
|
|
|
357
637
|
if not ipc_exists or self.process.returncode is not None:
|
|
358
|
-
self.
|
|
638
|
+
self._stop_quietly()
|
|
359
639
|
# Message unchanged: it has been this string for years and is not
|
|
360
640
|
# ours to break. What is new is the argv riding along, which is
|
|
361
641
|
# what lets the caller ask MPV why.
|
|
@@ -379,11 +659,72 @@ class MPVProcess:
|
|
|
379
659
|
else:
|
|
380
660
|
return data
|
|
381
661
|
|
|
382
|
-
def
|
|
383
|
-
"""
|
|
662
|
+
def _stop_quietly(self):
|
|
663
|
+
"""stop(), for the paths that are already raising something better.
|
|
664
|
+
|
|
665
|
+
A failed unlink here would replace MPVProcessError -- and an OSError
|
|
666
|
+
escapes MPV.__init__'s ``except MPVError`` retry loop, so the caller
|
|
667
|
+
would lose both the retries and the argv and returncode that the
|
|
668
|
+
error carries.
|
|
669
|
+
"""
|
|
670
|
+
try:
|
|
671
|
+
self.stop()
|
|
672
|
+
except OSError:
|
|
673
|
+
log.debug("Cleanup after a failed MPV start did not complete.",
|
|
674
|
+
exc_info=1)
|
|
675
|
+
|
|
676
|
+
def stop(self, timeout=5):
|
|
677
|
+
"""Terminate the process, and do not return while it may still be alive.
|
|
678
|
+
|
|
679
|
+
*timeout* is the seconds to wait for a polite exit before killing, and
|
|
680
|
+
again for the kill to land.
|
|
681
|
+
"""
|
|
682
|
+
# terminate() only *asks*. Returning here while MPV is still running
|
|
683
|
+
# left the caller no supported way to wait for it -- and a caller that
|
|
684
|
+
# exits at that moment orphans an MPV still holding its stdout. Both
|
|
685
|
+
# waits are bounded so a wedged MPV cannot hang teardown.
|
|
384
686
|
self.process.terminate()
|
|
385
|
-
|
|
386
|
-
|
|
687
|
+
try:
|
|
688
|
+
self.process.wait(timeout=timeout)
|
|
689
|
+
except subprocess.TimeoutExpired:
|
|
690
|
+
log.warning("MPV ignored terminate for {0}s; killing it.".format(timeout))
|
|
691
|
+
self.process.kill()
|
|
692
|
+
try:
|
|
693
|
+
self.process.wait(timeout=timeout)
|
|
694
|
+
except subprocess.TimeoutExpired:
|
|
695
|
+
log.error("MPV survived kill; giving up on reaping it.")
|
|
696
|
+
if os.name != 'nt':
|
|
697
|
+
# try/except rather than exists()-then-remove, because two threads
|
|
698
|
+
# reach here: the caller's terminate(), and the reader's
|
|
699
|
+
# quit_callback -> terminate(join=False) when MPV's exit closes
|
|
700
|
+
# the socket. Waiting above made them *overlap by construction* --
|
|
701
|
+
# the wait ends when MPV dies, which is the same event that wakes
|
|
702
|
+
# the reader -- so the check-then-act window is reliably lost.
|
|
703
|
+
try:
|
|
704
|
+
os.remove(self.ipc_socket)
|
|
705
|
+
except FileNotFoundError:
|
|
706
|
+
pass # The other thread got there first, which is all we wanted.
|
|
707
|
+
|
|
708
|
+
#: Stands in a sync command's result slot when the connection closed first.
|
|
709
|
+
_CLOSED = object()
|
|
710
|
+
|
|
711
|
+
def _call_user(what, func, *args):
|
|
712
|
+
"""Call user code from a thread this library owns, logging anything it
|
|
713
|
+
raises -- SystemExit included: a caller's bug must not end the reader,
|
|
714
|
+
the close sweep or the drain. Not for EventHandler.run, which a
|
|
715
|
+
BaseException still ends (see there)."""
|
|
716
|
+
try:
|
|
717
|
+
func(*args)
|
|
718
|
+
except BaseException:
|
|
719
|
+
log.error("%s raised.", what, exc_info=1)
|
|
720
|
+
|
|
721
|
+
def _reply_outcome(data):
|
|
722
|
+
"""``(error, result)`` for one IPC reply."""
|
|
723
|
+
if data["error"] == "success":
|
|
724
|
+
return None, data.get("data")
|
|
725
|
+
if data["error"] == "property unavailable":
|
|
726
|
+
return None, None
|
|
727
|
+
return MPVError(data["error"]), None
|
|
387
728
|
|
|
388
729
|
class MPVInter:
|
|
389
730
|
"""
|
|
@@ -394,6 +735,7 @@ class MPVInter:
|
|
|
394
735
|
|
|
395
736
|
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
|
396
737
|
*callback(event_name, data)* is the function for recieving events.
|
|
738
|
+
It runs on the reader thread; an exception from it is logged.
|
|
397
739
|
*quit_callback* is called when the socket connection to MPV dies.
|
|
398
740
|
"""
|
|
399
741
|
Socket = UnixSocket
|
|
@@ -405,25 +747,64 @@ class MPVInter:
|
|
|
405
747
|
if self.callback is None:
|
|
406
748
|
self.callback = lambda event, data: None
|
|
407
749
|
|
|
408
|
-
self.socket = Socket(ipc_socket, self.event_callback, self.quit_callback)
|
|
409
|
-
self.socket.start()
|
|
410
750
|
self.command_id = 1
|
|
411
751
|
self.rid_lock = threading.Lock()
|
|
412
752
|
self.socket_lock = threading.Lock()
|
|
413
753
|
self.cid_result = {}
|
|
414
754
|
self.cid_wait = {}
|
|
755
|
+
# Before the reader starts: it can deliver a reply, or die, at once.
|
|
756
|
+
self.cid_async = {}
|
|
757
|
+
self.async_lock = threading.Lock()
|
|
758
|
+
self.closed = False
|
|
759
|
+
|
|
760
|
+
self.socket = Socket(ipc_socket, self.event_callback, self._on_quit)
|
|
761
|
+
self.socket.start()
|
|
415
762
|
|
|
416
763
|
def stop(self, join=True):
|
|
417
764
|
"""Terminate the underlying connection."""
|
|
418
765
|
self.socket.stop(join)
|
|
766
|
+
# The reader's _on_quit usually gets here first. This one does not
|
|
767
|
+
# depend on it, so nothing is left pending once stop() returns.
|
|
768
|
+
self._fail_async()
|
|
769
|
+
|
|
770
|
+
def _on_quit(self):
|
|
771
|
+
self._fail_async()
|
|
772
|
+
if self.quit_callback:
|
|
773
|
+
self.quit_callback()
|
|
774
|
+
|
|
775
|
+
def _fail_async(self):
|
|
776
|
+
"""Answer every pending command, async or not: nothing else ever will."""
|
|
777
|
+
with self.async_lock:
|
|
778
|
+
self.closed = True
|
|
779
|
+
pending, self.cid_async = self.cid_async, {}
|
|
780
|
+
for command_id, event in list(self.cid_wait.items()):
|
|
781
|
+
# setdefault: a reply that landed just before still wins.
|
|
782
|
+
self.cid_result.setdefault(command_id, _CLOSED)
|
|
783
|
+
event.set()
|
|
784
|
+
for on_reply in pending.values():
|
|
785
|
+
_call_user("command_async reply handler", on_reply,
|
|
786
|
+
BrokenPipeError("MPV connection closed."), None)
|
|
419
787
|
|
|
420
788
|
def event_callback(self, data):
|
|
421
789
|
"""Internal callback for recieving events from MPV."""
|
|
422
790
|
if "request_id" in data:
|
|
423
|
-
|
|
424
|
-
self.
|
|
791
|
+
request_id = data["request_id"]
|
|
792
|
+
with self.async_lock:
|
|
793
|
+
on_reply = self.cid_async.pop(request_id, None)
|
|
794
|
+
if on_reply is not None:
|
|
795
|
+
_call_user("command_async reply handler", on_reply,
|
|
796
|
+
*_reply_outcome(data))
|
|
797
|
+
return
|
|
798
|
+
# Not indexed: a reply nobody waits for (mpv answers id 0 to a
|
|
799
|
+
# request without one) would raise here, on the reader thread,
|
|
800
|
+
# and end the read loop as if the connection had died.
|
|
801
|
+
event = self.cid_wait.get(request_id)
|
|
802
|
+
if event is not None:
|
|
803
|
+
self.cid_result[request_id] = data
|
|
804
|
+
event.set()
|
|
425
805
|
elif "event" in data:
|
|
426
|
-
|
|
806
|
+
_call_user("MPVInter event callback", self.callback,
|
|
807
|
+
data["event"], data)
|
|
427
808
|
|
|
428
809
|
def command(self, command, *args):
|
|
429
810
|
"""
|
|
@@ -432,7 +813,8 @@ class MPVInter:
|
|
|
432
813
|
*command* is the name of the MPV command
|
|
433
814
|
|
|
434
815
|
All further arguments are forwarded to the MPV command.
|
|
435
|
-
Throws TimeoutError if timeout of 120 seconds is reached
|
|
816
|
+
Throws TimeoutError if timeout of 120 seconds is reached, and
|
|
817
|
+
BrokenPipeError as soon as the connection closes before the reply.
|
|
436
818
|
"""
|
|
437
819
|
self.rid_lock.acquire()
|
|
438
820
|
command_id = self.command_id
|
|
@@ -440,7 +822,12 @@ class MPVInter:
|
|
|
440
822
|
self.rid_lock.release()
|
|
441
823
|
|
|
442
824
|
event = threading.Event()
|
|
443
|
-
|
|
825
|
+
# Under the lock _fail_async takes, so a close either finds this
|
|
826
|
+
# waiter or is seen here first; neither way waits out TIMEOUT.
|
|
827
|
+
with self.async_lock:
|
|
828
|
+
if self.closed:
|
|
829
|
+
raise BrokenPipeError("MPV connection closed.")
|
|
830
|
+
self.cid_wait[command_id] = event
|
|
444
831
|
|
|
445
832
|
command_list = [command]
|
|
446
833
|
command_list.extend(args)
|
|
@@ -452,23 +839,59 @@ class MPVInter:
|
|
|
452
839
|
|
|
453
840
|
has_event = event.wait(timeout=TIMEOUT)
|
|
454
841
|
if has_event:
|
|
455
|
-
data = self.cid_result
|
|
456
|
-
del self.cid_result[command_id]
|
|
842
|
+
data = self.cid_result.pop(command_id)
|
|
457
843
|
del self.cid_wait[command_id]
|
|
458
|
-
if data
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
844
|
+
if data is _CLOSED:
|
|
845
|
+
raise BrokenPipeError("MPV connection closed.")
|
|
846
|
+
error, result = _reply_outcome(data)
|
|
847
|
+
if error is not None:
|
|
848
|
+
raise error
|
|
849
|
+
return result
|
|
464
850
|
else:
|
|
465
851
|
raise TimeoutError("No response from MPV.")
|
|
466
852
|
|
|
853
|
+
def command_async(self, on_reply, command, *args):
|
|
854
|
+
"""
|
|
855
|
+
Issue a command to MPV without waiting for it to finish.
|
|
856
|
+
|
|
857
|
+
*on_reply(error, result)* is called exactly once: with MPV's answer on
|
|
858
|
+
the transport's reader thread; with BrokenPipeError on the thread
|
|
859
|
+
that closes the connection, or on the calling thread if it is already
|
|
860
|
+
closed; or with the send's own error on the calling thread. An
|
|
861
|
+
exception from it is logged when the reader or the close calls it,
|
|
862
|
+
and raised to the caller otherwise. There is no timeout.
|
|
863
|
+
"""
|
|
864
|
+
with self.rid_lock:
|
|
865
|
+
command_id = self.command_id
|
|
866
|
+
self.command_id += 1
|
|
867
|
+
|
|
868
|
+
with self.async_lock:
|
|
869
|
+
closed = self.closed
|
|
870
|
+
if not closed:
|
|
871
|
+
self.cid_async[command_id] = on_reply
|
|
872
|
+
if closed:
|
|
873
|
+
on_reply(BrokenPipeError("MPV connection closed."), None)
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
command_list = [command]
|
|
877
|
+
command_list.extend(args)
|
|
878
|
+
try:
|
|
879
|
+
with self.socket_lock:
|
|
880
|
+
self.socket.send({"command": command_list,
|
|
881
|
+
"request_id": command_id, "async": True})
|
|
882
|
+
except Exception as error:
|
|
883
|
+
with self.async_lock:
|
|
884
|
+
owned = self.cid_async.pop(command_id, None)
|
|
885
|
+
if owned is not None: # else _fail_async has already answered
|
|
886
|
+
on_reply(error, None)
|
|
887
|
+
|
|
467
888
|
class EventHandler(threading.Thread):
|
|
468
889
|
"""Event handling thread. (Internal)"""
|
|
469
890
|
def __init__(self):
|
|
470
891
|
"""Create an instance of the thread."""
|
|
471
892
|
self.queue = queue.Queue()
|
|
893
|
+
self.stopped = False
|
|
894
|
+
self.stop_lock = threading.Lock()
|
|
472
895
|
threading.Thread.__init__(self)
|
|
473
896
|
self.daemon = True
|
|
474
897
|
|
|
@@ -479,24 +902,62 @@ class EventHandler(threading.Thread):
|
|
|
479
902
|
*func* is the function to call
|
|
480
903
|
|
|
481
904
|
All further arguments are forwarded to *func*.
|
|
905
|
+
|
|
906
|
+
Returns False, without queueing it, once the thread has stopped --
|
|
907
|
+
by stop(), or because a task raised SystemExit or the like.
|
|
482
908
|
"""
|
|
483
|
-
self.
|
|
909
|
+
with self.stop_lock:
|
|
910
|
+
if self.stopped:
|
|
911
|
+
return False
|
|
912
|
+
self.queue.put((func, args))
|
|
913
|
+
return True
|
|
484
914
|
|
|
485
915
|
def stop(self, join=True):
|
|
486
916
|
"""Terminate the thread."""
|
|
487
|
-
self.
|
|
488
|
-
|
|
917
|
+
with self.stop_lock:
|
|
918
|
+
self.stopped = True
|
|
919
|
+
self.queue.put("quit")
|
|
920
|
+
# A callback that calls terminate() is on this thread. The guard is
|
|
921
|
+
# the transports' (see UnixSocket.stop); `join` stays the timeout.
|
|
922
|
+
if self.ident is not None and self is not threading.current_thread():
|
|
923
|
+
self.join(join)
|
|
489
924
|
|
|
490
925
|
def run(self):
|
|
491
926
|
"""Process socket events. Do not run this directly. Use *start*."""
|
|
927
|
+
try:
|
|
928
|
+
while True:
|
|
929
|
+
event = self.queue.get()
|
|
930
|
+
if event == "quit":
|
|
931
|
+
break
|
|
932
|
+
try:
|
|
933
|
+
event[0](*event[1])
|
|
934
|
+
except Exception:
|
|
935
|
+
log.error("EventHandler caught exception from %s.", event, exc_info=1)
|
|
936
|
+
except BaseException as ex:
|
|
937
|
+
log.warning("A callback raised %r, ending the event thread: "
|
|
938
|
+
"property observers, key bindings and events will no "
|
|
939
|
+
"longer be delivered; command_async callbacks now run "
|
|
940
|
+
"on whichever thread delivers their outcome.", ex)
|
|
941
|
+
# Not _call_user, deliberately: a callback's SystemExit ends this
|
|
942
|
+
# thread, as it has since 2020.
|
|
943
|
+
raise
|
|
944
|
+
finally:
|
|
945
|
+
self._drain()
|
|
946
|
+
|
|
947
|
+
def _drain(self):
|
|
948
|
+
"""A callback's SystemExit (or similar) ends this thread, as it always
|
|
949
|
+
has. Refuse work from here on, so put_task says so, and run what was
|
|
950
|
+
already queued: a command_async future may be waiting in it."""
|
|
951
|
+
with self.stop_lock:
|
|
952
|
+
self.stopped = True
|
|
492
953
|
while True:
|
|
493
|
-
event = self.queue.get()
|
|
494
|
-
if event == "quit":
|
|
495
|
-
break
|
|
496
954
|
try:
|
|
497
|
-
event
|
|
498
|
-
except
|
|
499
|
-
|
|
955
|
+
event = self.queue.get_nowait()
|
|
956
|
+
except queue.Empty:
|
|
957
|
+
return
|
|
958
|
+
if event == "quit":
|
|
959
|
+
continue
|
|
960
|
+
_call_user("EventHandler task", event[0], *event[1])
|
|
500
961
|
|
|
501
962
|
class MPV:
|
|
502
963
|
"""
|
|
@@ -510,7 +971,7 @@ class MPV:
|
|
|
510
971
|
"""
|
|
511
972
|
def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None,
|
|
512
973
|
log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
|
|
513
|
-
diagnose_start_failures=True, **kwargs):
|
|
974
|
+
diagnose_start_failures=True, discard_output=False, **kwargs):
|
|
514
975
|
"""
|
|
515
976
|
Create the interface to MPV and process instance.
|
|
516
977
|
|
|
@@ -519,11 +980,16 @@ class MPV:
|
|
|
519
980
|
*mpv_location* is the location of MPV for *start_mpv*. (Default: Use MPV in PATH)
|
|
520
981
|
*log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
|
|
521
982
|
*loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
|
522
|
-
*quit_callback* is called when the socket connection to MPV dies.
|
|
983
|
+
*quit_callback* is called on the reader thread when the socket connection to MPV dies.
|
|
984
|
+
An exception from it is logged, and MPV is still terminated.
|
|
523
985
|
*diagnose_start_failures* re-runs MPV once, on the first failed start, to find out
|
|
524
986
|
why it refused to start. If MPV names an option it does not have, MPVProcessError is
|
|
525
987
|
raised immediately with *bad_option* set instead of spending every retry on something
|
|
526
988
|
that cannot succeed. Set it to False for the pre-1.3 behaviour. (Default: True)
|
|
989
|
+
*discard_output* sends MPV's stdout and stderr to devnull instead of letting it
|
|
990
|
+
inherit the caller's. Set it if anything waits for EOF on your stdout, since an
|
|
991
|
+
MPV that outlives *terminate* would otherwise hold that pipe open. Note this is
|
|
992
|
+
not MPV's own --quiet, which is still forwarded as an MPV option. (Default: False)
|
|
527
993
|
|
|
528
994
|
All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
|
|
529
995
|
"""
|
|
@@ -550,7 +1016,8 @@ class MPV:
|
|
|
550
1016
|
log_output = None
|
|
551
1017
|
for i in range(start_retries):
|
|
552
1018
|
try:
|
|
553
|
-
self.mpv_process = MPVProcess(ipc_socket, mpv_location,
|
|
1019
|
+
self.mpv_process = MPVProcess(ipc_socket, mpv_location,
|
|
1020
|
+
discard_output=discard_output, **kwargs)
|
|
554
1021
|
break
|
|
555
1022
|
except MPVError as error:
|
|
556
1023
|
last_error = error
|
|
@@ -639,7 +1106,7 @@ class MPV:
|
|
|
639
1106
|
Internal handler for quit events.
|
|
640
1107
|
"""
|
|
641
1108
|
if self.quit_callback:
|
|
642
|
-
self.quit_callback
|
|
1109
|
+
_call_user("quit_callback", self.quit_callback)
|
|
643
1110
|
self.terminate(join=False)
|
|
644
1111
|
|
|
645
1112
|
def bind_event(self, name, callback):
|
|
@@ -749,6 +1216,11 @@ class MPV:
|
|
|
749
1216
|
Waits for the value of a property to change.
|
|
750
1217
|
|
|
751
1218
|
*name* is the name of the property.
|
|
1219
|
+
|
|
1220
|
+
Never call it from inside a callback here: the change is delivered on
|
|
1221
|
+
the event thread, which the wait would be blocking, so it never ends.
|
|
1222
|
+
It also never returns once a callback has ended the event thread,
|
|
1223
|
+
since the change is then never delivered.
|
|
752
1224
|
"""
|
|
753
1225
|
event = threading.Event()
|
|
754
1226
|
first_event = True
|
|
@@ -782,6 +1254,14 @@ class MPV:
|
|
|
782
1254
|
def terminate(self, join=True):
|
|
783
1255
|
"""Terminate the connection to MPV and process (if *start_mpv* is used)."""
|
|
784
1256
|
if self.mpv_process:
|
|
1257
|
+
# Unconditionally, regardless of *join*. That flag only ever
|
|
1258
|
+
# reaches our own threads -- and imperfectly: the transports join
|
|
1259
|
+
# properly, while EventHandler.stop passes it as Thread.join's
|
|
1260
|
+
# *timeout*, so join=True waits at most a second there and does
|
|
1261
|
+
# not confirm the thread exited. Either way it says nothing about
|
|
1262
|
+
# MPV, and _quit_callback passes False only because a thread
|
|
1263
|
+
# cannot join itself. Letting it skip this wait would hand the
|
|
1264
|
+
# orphaned-MPV bug back to anyone who passed it.
|
|
785
1265
|
self.mpv_process.stop()
|
|
786
1266
|
if self.mpv_inter:
|
|
787
1267
|
self.mpv_inter.stop(join)
|
|
@@ -799,6 +1279,80 @@ class MPV:
|
|
|
799
1279
|
"""
|
|
800
1280
|
return self.mpv_inter.command(command, *args)
|
|
801
1281
|
|
|
1282
|
+
def command_async(self, command, *args, callback=None):
|
|
1283
|
+
"""
|
|
1284
|
+
Send a command to MPV without waiting for it to finish, for one that
|
|
1285
|
+
can take a long time (``sub-add`` of a slow URL). Unlike *command*,
|
|
1286
|
+
it holds neither the caller nor the connection: other commands are
|
|
1287
|
+
answered while it runs, and it has no timeout.
|
|
1288
|
+
|
|
1289
|
+
Returns a ``concurrent.futures.Future``. With no *callback* it holds
|
|
1290
|
+
MPV's result or the error, as *command* would have returned or raised.
|
|
1291
|
+
*callback(error, result)* is called first, with *error* None on
|
|
1292
|
+
success, and the future then holds what it returns or raises.
|
|
1293
|
+
|
|
1294
|
+
The callback runs on the event thread, like every other callback
|
|
1295
|
+
here. If an earlier callback ended that thread by raising SystemExit
|
|
1296
|
+
or the like, a warning is logged, and property observers, key
|
|
1297
|
+
bindings and events are no longer delivered at all; command_async
|
|
1298
|
+
callbacks still run, on whichever thread delivers their outcome
|
|
1299
|
+
instead -- the connection's reader while MPV is connected, or the
|
|
1300
|
+
thread that closes the connection -- and a SystemExit from one there
|
|
1301
|
+
is logged, not raised. With the event thread gone, a call made once
|
|
1302
|
+
the connection has closed (by *terminate* or by MPV exiting), or
|
|
1303
|
+
whose send fails, runs its callback on the calling thread before
|
|
1304
|
+
command_async returns, and a SystemExit from it is raised to that
|
|
1305
|
+
caller. A logging handler that raises SystemExit is not caught
|
|
1306
|
+
anywhere: as in the standard library, it ends the thread that logged,
|
|
1307
|
+
and in the close that leaves the futures after it unresolved.
|
|
1308
|
+
|
|
1309
|
+
The future resolves when MPV answers or, if the connection closes
|
|
1310
|
+
first, with BrokenPipeError -- meaning no answer will come, not that
|
|
1311
|
+
the command did not run. Until then it is pending: a command MPV is
|
|
1312
|
+
still running has no timeout.
|
|
1313
|
+
|
|
1314
|
+
Never block on the future from inside a callback here (a property
|
|
1315
|
+
observer, a key binding, an event, another command_async callback).
|
|
1316
|
+
Those run on the event thread, the thread that would resolve it, so
|
|
1317
|
+
the wait never ends. python-mpv behaves the same.
|
|
1318
|
+
|
|
1319
|
+
Once the event thread is gone, a callback must not block at all. On
|
|
1320
|
+
the reader, a sync *command* waits out TIMEOUT with nothing else read
|
|
1321
|
+
meanwhile, and a wait on a future never ends -- nor does a
|
|
1322
|
+
*terminate* called from another thread. python-mpv fails here too:
|
|
1323
|
+
once a callback's SystemExit stops its event loop, no callback or
|
|
1324
|
+
future is delivered at all.
|
|
1325
|
+
|
|
1326
|
+
The future cannot be cancelled: MPV's IPC protocol cannot abort a
|
|
1327
|
+
command. Needs MPV 0.33 or later; an older MPV ignores the request to
|
|
1328
|
+
run in the background and runs the command as *command* does, blocking
|
|
1329
|
+
every other command on the connection until it finishes.
|
|
1330
|
+
"""
|
|
1331
|
+
future = Future()
|
|
1332
|
+
future.set_running_or_notify_cancel()
|
|
1333
|
+
|
|
1334
|
+
def finish(error, result):
|
|
1335
|
+
try:
|
|
1336
|
+
if callback is not None:
|
|
1337
|
+
future.set_result(callback(error, result))
|
|
1338
|
+
elif error is not None:
|
|
1339
|
+
future.set_exception(error)
|
|
1340
|
+
else:
|
|
1341
|
+
future.set_result(result)
|
|
1342
|
+
except BaseException as ex:
|
|
1343
|
+
future.set_exception(ex)
|
|
1344
|
+
if not isinstance(ex, Exception):
|
|
1345
|
+
raise # which thread it reaches, and what happens: see the docstring
|
|
1346
|
+
log.error("command_async callback for {0} raised.".format(
|
|
1347
|
+
command), exc_info=1)
|
|
1348
|
+
|
|
1349
|
+
def on_reply(error, result):
|
|
1350
|
+
if not self.event_handler.put_task(finish, error, result):
|
|
1351
|
+
finish(error, result)
|
|
1352
|
+
|
|
1353
|
+
self.mpv_inter.command_async(on_reply, command, *args)
|
|
1354
|
+
return future
|
|
1355
|
+
|
|
802
1356
|
def __getattr__(self, name):
|
|
803
1357
|
if name in self.properties:
|
|
804
1358
|
return self.command("get_property", name.replace("_", "-"))
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
python_mpv_jsonipc.py,sha256=vsGhKYoGeAl4uw652aZZsMF2weee_i8o7sJV9Ssp80g,31762
|
|
2
|
-
python_mpv_jsonipc-1.3.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
|
|
3
|
-
python_mpv_jsonipc-1.3.0.dist-info/METADATA,sha256=1_LpKl1fCquLmZBb0qYczUSbHynCP6FLGdt92Z5jV4U,2277
|
|
4
|
-
python_mpv_jsonipc-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
-
python_mpv_jsonipc-1.3.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
|
|
6
|
-
python_mpv_jsonipc-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
{python_mpv_jsonipc-1.3.0.dist-info → python_mpv_jsonipc-1.5.0.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|