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