1# This file is part of ReText 2# Copyright: 2016 Maurice van der Pot 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# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17import markups 18import multiprocessing as mp 19import os 20import pickle 21import signal 22from socket import socketpair 23import struct 24import traceback 25import weakref 26 27from PyQt5.QtCore import pyqtSignal, QObject, QSocketNotifier 28 29def recvall(sock, remaining): 30 alldata = bytearray() 31 while remaining > 0: 32 data = sock.recv(remaining) 33 if len(data) == 0: 34 raise EOFError('Received 0 bytes from socket while more bytes were expected. Did the sender process exit unexpectedly?') 35 alldata.extend(data) 36 remaining -= len(data) 37 38 return alldata 39 40def receiveObject(sock): 41 sizeBuf = recvall(sock, 4) 42 size = struct.unpack('I', sizeBuf)[0] 43 message = recvall(sock, size) 44 obj = pickle.loads(message) 45 return obj 46 47def sendObject(sock, obj): 48 message = pickle.dumps(obj) 49 sizeBuf = struct.pack('I', len(message)) 50 sock.sendall(sizeBuf) 51 sock.sendall(message) 52 53class ConversionError(Exception): 54 pass 55 56class MarkupNotAvailableError(Exception): 57 pass 58 59def _indent(text, prefix): 60 return ''.join(('%s%s\n' % (prefix, line) for line in text.splitlines())) 61 62def _converter_process_func(conn_parent, conn_child): 63 conn_parent.close() 64 65 # Ignore ctrl-C. The main application will also receive the signal and 66 # determine if the application should be stopped or not. 67 signal.signal(signal.SIGINT, signal.SIG_IGN) 68 69 current_markup = None 70 71 while True: 72 job = receiveObject(conn_child) 73 if job['command'] == 'quit': 74 break 75 elif job['command'] == 'convert': 76 try: 77 os.chdir(job['current_dir']) 78 if (not current_markup or 79 current_markup.name != job['markup_name'] or 80 current_markup.filename != job['filename']): 81 markup_class = markups.find_markup_class_by_name(job['markup_name']) 82 if not markup_class.available(): 83 raise MarkupNotAvailableError('The specified markup was not available') 84 85 current_markup = markup_class(job['filename']) 86 current_markup.requested_extensions = job['requested_extensions'] 87 88 converted = current_markup.convert(job['text']) 89 result = ('ok', converted) 90 except MarkupNotAvailableError as e: 91 result = ('markupnotavailableerror', e.args) 92 except Exception: 93 result = ('conversionerror', 94 'The background markup conversion process received this exception:\n%s' % 95 _indent(traceback.format_exc(), ' ')) 96 97 try: 98 sendObject(conn_child, result) 99 except BrokenPipeError: 100 # Continue despite the broken pipe because we expect that a 101 # 'quit' command will have been sent. If it has been then we 102 # should terminate without any error messages. If no command 103 # was queued we will get an EOFError from the read, giving us a 104 # second chance to show that something went wrong by exiting 105 # with a traceback. 106 continue 107 108 109class ConverterProcess(QObject): 110 111 conversionDone = pyqtSignal() 112 113 def __init__(self): 114 super().__init__() 115 116 conn_parent, conn_child = socketpair() 117 118 # TODO: figure out which of the two sockets should be set to 119 # inheritable and which should be passed to the child 120 if hasattr(conn_child, 'set_inheritable'): 121 conn_child.set_inheritable(True) 122 123 # Use a local variable for child so that we can talk to the child in 124 # on_finalize without needing a reference to self 125 child = mp.Process(target=_converter_process_func, args=(conn_parent, conn_child)) 126 child.daemon = True 127 child.start() 128 self.child = child 129 130 conn_child.close() 131 self.conn = conn_parent 132 133 self.busy = False 134 self.notificationPending = False 135 self.conversionNotifier = QSocketNotifier(self.conn.fileno(), 136 QSocketNotifier.Type.Read) 137 self.conversionNotifier.activated.connect(self._conversionNotifierActivated) 138 139 def on_finalize(conn): 140 sendObject(conn_parent, {'command':'quit'}) 141 conn_parent.close() 142 child.join() 143 144 weakref.finalize(self, on_finalize, conn_parent) 145 146 def _conversionNotifierActivated(self): 147 # The ready-for-read signal on the socket may be triggered multiple 148 # times, but we only send a single notification to the client as soon 149 # as the results of the conversion are starting to come in. This makes 150 # it easy for clients to avoid multiple calls to get_result for the 151 # same conversion. 152 if self.notificationPending: 153 self.notificationPending = False 154 155 # Set the socket to blocking before waking up any interested parties, 156 # because it has been set to unblocking by QSocketNotifier 157 self.conn.setblocking(True) 158 self.conversionDone.emit() 159 160 def start_conversion(self, markup_name, filename, requested_extensions, text, current_dir): 161 if self.busy: 162 raise RuntimeError('Already converting') 163 164 sendObject(self.conn, {'command': 'convert', 165 'markup_name' : markup_name, 166 'filename' : filename, 167 'current_dir': current_dir, 168 'requested_extensions' : requested_extensions, 169 'text' : text}) 170 self.busy = True 171 self.notificationPending = True 172 173 def get_result(self): 174 if not self.busy: 175 raise RuntimeError('No ongoing conversion') 176 177 self.busy = False 178 179 status, result = receiveObject(self.conn) 180 181 if status == 'markupnotavailableerror': 182 raise MarkupNotAvailableError(result) 183 elif status == 'conversionerror': 184 raise ConversionError(result) 185 186 return result 187 188 def stop(self): 189 sendObject(self.conn, {'command': 'quit'}) 190 self.conn.close() 191 192