1# Copyright 2014 Christoph Reiter
2#           2017 Nick Boultbee
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import os
10import errno
11import signal
12import stat
13
14from quodlibet import print_e
15
16try:
17    import fcntl
18    fcntl
19except ImportError:
20    fcntl = None
21
22from gi.repository import GLib
23from senf import mkstemp, fsn2bytes
24
25from quodlibet.util.path import mkdir
26from quodlibet.util import print_d
27
28FIFO_TIMEOUT = 10
29"""time in seconds until we give up writing/reading"""
30
31
32def _write_fifo(fifo_path, data):
33    """Writes the data to the FIFO or raises `EnvironmentError`"""
34
35    assert isinstance(data, bytes)
36
37    # This will raise if the FIFO doesn't exist or there is no reader
38    try:
39        fifo = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK)
40    except OSError:
41        try:
42            os.unlink(fifo_path)
43        except OSError:
44            pass
45        raise
46    else:
47        try:
48            os.close(fifo)
49        except OSError:
50            pass
51
52    try:
53        # This is a total abuse of Python! Hooray!
54        signal.signal(signal.SIGALRM, lambda: "" + 2)
55        signal.alarm(FIFO_TIMEOUT)
56        with open(fifo_path, "wb") as f:
57            signal.signal(signal.SIGALRM, signal.SIG_IGN)
58            f.write(data)
59    except (OSError, IOError, TypeError):
60        # Unable to write to the fifo. Removing it.
61        try:
62            os.unlink(fifo_path)
63        except OSError:
64            pass
65        raise EnvironmentError("Couldn't write to fifo %r" % fifo_path)
66
67
68def split_message(data):
69    """Split incoming data in pairs of (command, FIFO path or `None`)
70
71    This supports two data formats:
72    Newline-separated commands without a return FIFO path.
73    and "NULL<command>NULL<fifo-path>NULL"
74
75    Args:
76        data (bytes)
77    Returns:
78        Tuple[bytes, bytes]
79    Raises:
80        ValueError
81    """
82
83    assert isinstance(data, bytes)
84
85    arg = 0
86    args = []
87    while data:
88        if arg == 0:
89            index = data.find(b"\x00")
90            if index == 0:
91                arg = 1
92                data = data[1:]
93                continue
94            if index == -1:
95                elm = data
96                data = b""
97            else:
98                elm, data = data[:index], data[index:]
99            for l in elm.splitlines():
100                yield (l, None)
101        elif arg == 1:
102            elm, data = data.split(b"\x00", 1)
103            args.append(elm)
104            arg = 2
105        elif arg == 2:
106            elm, data = data.split(b"\x00", 1)
107            args.append(elm)
108            yield tuple(args)
109            del args[:]
110            arg = 0
111
112
113def write_fifo(fifo_path, data):
114    """Writes the data to the FIFO and returns a response.
115
116    Args:
117        fifo_path (pathlike)
118        data (bytes)
119    Returns:
120        bytes
121    Raises:
122        EnvironmentError: In case of timeout and other errors
123    """
124
125    assert isinstance(data, bytes)
126
127    fd, filename = mkstemp()
128    try:
129        os.close(fd)
130        os.unlink(filename)
131        # mkfifo fails if the file exists, so this is safe.
132        os.mkfifo(filename, 0o600)
133
134        _write_fifo(
135            fifo_path,
136            b"\x00" + data + b"\x00" + fsn2bytes(filename, None) + b"\x00")
137
138        try:
139            signal.signal(signal.SIGALRM, lambda: "" + 2)
140            signal.alarm(FIFO_TIMEOUT)
141            with open(filename, "rb") as h:
142                signal.signal(signal.SIGALRM, signal.SIG_IGN)
143                return h.read()
144        except TypeError:
145            # In case the main instance deadlocks we can write to it, but
146            # reading will time out. Assume it is broken and delete the
147            # fifo.
148            try:
149                os.unlink(fifo_path)
150            except OSError:
151                pass
152            raise EnvironmentError("timeout")
153    finally:
154        try:
155            os.unlink(filename)
156        except EnvironmentError:
157            pass
158
159
160def fifo_exists(fifo_path):
161    """Returns whether a FIFO exists (and is writeable).
162
163    Args:
164        fifo_path (pathlike)
165    Returns:
166        bool
167    """
168
169    # https://github.com/quodlibet/quodlibet/issues/1131
170    # FIXME: There is a race where control() creates a new file
171    # instead of writing to the FIFO, confusing the next QL instance.
172    # Remove non-FIFOs here for now.
173    try:
174        if not stat.S_ISFIFO(os.stat(fifo_path).st_mode):
175            print_d("%r not a FIFO. Removing it." % fifo_path)
176            os.remove(fifo_path)
177    except OSError:
178        pass
179    return os.path.exists(fifo_path)
180
181
182class FIFOError(Exception):
183    pass
184
185
186class FIFO(object):
187    """Creates and reads from a FIFO"""
188
189    def __init__(self, path, callback):
190        """
191        Args:
192            path (pathlike)
193            callback (Callable[[bytes], None])
194        """
195
196        self._callback = callback
197        self._path = path
198
199    def open(self):
200        """Create the FIFO and listen to it.
201
202        Raises:
203            FIFOError in case another process is already using it.
204        """
205
206        self._open(False, None)
207
208    def destroy(self):
209        """After destroy() the callback will no longer be called
210        and the FIFO can no longer be used. Can be called multiple
211        times.
212        """
213
214        if self._id is not None:
215            GLib.source_remove(self._id)
216            self._id = None
217
218        try:
219            os.unlink(self._path)
220        except EnvironmentError:
221            pass
222
223    def _open(self, ignore_lock, *args):
224        from quodlibet import qltk
225
226        self._id = None
227        mkdir(os.path.dirname(self._path))
228        try:
229            os.mkfifo(self._path, 0o600)
230        except OSError:
231            # maybe exists, we'll fail below otherwise
232            pass
233
234        try:
235            fifo = os.open(self._path, os.O_NONBLOCK)
236        except OSError:
237            return
238
239        while True:
240            try:
241                fcntl.flock(fifo, fcntl.LOCK_EX | fcntl.LOCK_NB)
242            except IOError as e:
243                # EINTR on linux
244                if e.errno == errno.EINTR:
245                    continue
246                if ignore_lock:
247                    break
248                # OSX doesn't support FIFO locking, so check errno
249                if e.errno == errno.EWOULDBLOCK:
250                    raise FIFOError("fifo already locked")
251                else:
252                    print_d("fifo locking failed: %r" % e)
253            break
254
255        try:
256            f = os.fdopen(fifo, "rb", 4096)
257        except OSError as e:
258            print_e("Couldn't open FIFO (%s)" % e)
259        else:
260            self._id = qltk.io_add_watch(
261                f, GLib.PRIORITY_DEFAULT,
262                GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
263                self._process, *args)
264
265    def _process(self, source, condition, *args):
266        if condition in (GLib.IO_ERR, GLib.IO_HUP):
267            self._open(True, *args)
268            return False
269
270        while True:
271            try:
272                data = source.read()
273            except (IOError, OSError) as e:
274                if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN):
275                    return True
276                elif e.errno == errno.EINTR:
277                    continue
278                else:
279                    self.__open(*args)
280                    return False
281            break
282
283        if not data:
284            self._open(*args)
285            return False
286
287        self._callback(data)
288
289        return True
290