class QProcessExecutionManager(ExecutionManager): """Class to manage tool instance execution using a PySide2 QProcess.""" def __init__(self, logger, program=None, args=None, silent=False, semisilent=False): """Class constructor. Args: logger (LoggerInterface): a logger instance program (str): Path to program to run in the subprocess (e.g. julia.exe) args (list): List of argument for the program (e.g. path to script file) silent (bool): Whether or not to emit logger msg signals """ super().__init__(logger) self._program = program self._args = args self._silent = silent # Do not show Event Log nor Process Log messages self._semisilent = semisilent # Do not show Event Log messages but show Process Log messages self.process_failed = False self.process_failed_to_start = False self._user_stopped = False self._process = QProcess(self) self.process_output = None # stdout when running silent self.error_output = None # stderr when running silent def program(self): """Program getter method.""" return self._program def args(self): """Program argument getter method.""" return self._args # noinspection PyUnresolvedReferences def start_execution(self, workdir=None): """Starts the execution of a command in a QProcess. Args: workdir (str): Work directory """ if workdir is not None: self._process.setWorkingDirectory(workdir) self._process.started.connect(self.process_started) self._process.finished.connect(self.on_process_finished) if not self._silent and not self._semisilent: # Loud self._process.readyReadStandardOutput.connect(self.on_ready_stdout) self._process.readyReadStandardError.connect(self.on_ready_stderr) self._process.error.connect(self.on_process_error) # errorOccurred available in Qt 5.6 self._process.stateChanged.connect(self.on_state_changed) elif self._semisilent: # semi-silent self._process.readyReadStandardOutput.connect(self.on_ready_stdout) self._process.readyReadStandardError.connect(self.on_ready_stderr) self._process.start(self._program, self._args) if not self._process.waitForStarted(msecs=10000): # This blocks until process starts or timeout happens self.process_failed = True self.process_failed_to_start = True self._process.deleteLater() self._process = None self.execution_finished.emit(-9998) def wait_for_process_finished(self, msecs=30000): """Wait for subprocess to finish. Return: True if process finished successfully, False otherwise """ if not self._process: return False if self.process_failed or self.process_failed_to_start: return False if not self._process.waitForFinished(msecs): self.process_failed = True self._process.close() self._process = None return False return True @Slot(name="process_started") def process_started(self): """Run when subprocess has started.""" @Slot("QProcess::ProcessState", name="on_state_changed") def on_state_changed(self, new_state): """Runs when QProcess state changes. Args: new_state (QProcess::ProcessState): Process state number """ if new_state == QProcess.Starting: self._logger.msg.emit("\tStarting program <b>{0}</b>".format(self._program)) arg_str = " ".join(self._args) self._logger.msg.emit("\tArguments: <b>{0}</b>".format(arg_str)) elif new_state == QProcess.Running: self._logger.msg_warning.emit( "\tExecution is in progress. See Process Log for messages " "(stdout&stderr)" ) elif new_state == QProcess.NotRunning: # logging.debug("QProcess is not running") pass else: self._logger.msg_error.emit("Process is in an unspecified state") logging.error("QProcess unspecified state: %s", new_state) @Slot("QProcess::ProcessError", name="'on_process_error") def on_process_error(self, process_error): """Run if there is an error in the running QProcess. Args: process_error (QProcess::ProcessError): Process error number """ if process_error == QProcess.FailedToStart: self.process_failed = True self.process_failed_to_start = True elif process_error == QProcess.Timedout: self.process_failed = True self._logger.msg_error.emit("Timed out") elif process_error == QProcess.Crashed: self.process_failed = True if not self._user_stopped: self._logger.msg_error.emit("Process crashed") elif process_error == QProcess.WriteError: self._logger.msg_error.emit("Process WriteError") elif process_error == QProcess.ReadError: self._logger.msg_error.emit("Process ReadError") elif process_error == QProcess.UnknownError: self._logger.msg_error.emit("Unknown error in process") else: self._logger.msg_error.emit("Unspecified error in process: {0}".format(process_error)) def stop_execution(self): """See base class.""" self._logger.msg_error.emit("Terminating process") self._user_stopped = True self.process_failed = True if not self._process: return try: self._process.terminate() except Exception as ex: # pylint: disable=broad-except self._logger.msg_error.emit("[{0}] exception when terminating process".format(ex)) logging.exception("Exception in closing QProcess: %s", ex) finally: self._process.deleteLater() self._process = None @Slot(int) def on_process_finished(self, exit_code): """Runs when subprocess has finished. Args: exit_code (int): Return code from external program (only valid for normal exits) """ # logging.debug("Error that occurred last: {0}".format(self._process.error())) if not self._process: return exit_status = self._process.exitStatus() # Normal or crash exit if exit_status == QProcess.CrashExit: if not self._silent: self._logger.msg_error.emit("\tProcess crashed") exit_code = -1 elif exit_status == QProcess.NormalExit: pass else: if not self._silent: self._logger.msg_error.emit("Unknown QProcess exit status [{0}]".format(exit_status)) exit_code = -1 if not exit_code == 0: self.process_failed = True if not self._user_stopped: out = str(self._process.readAllStandardOutput().data(), "utf-8") errout = str(self._process.readAllStandardError().data(), "utf-8") if out is not None: if not self._silent: self._logger.msg_proc.emit(out.strip()) else: self.process_output = out.strip() self.error_output = errout.strip() else: self._logger.msg.emit("*** Terminating process ***") # Delete QProcess self._process.deleteLater() self._process = None self.execution_finished.emit(exit_code) @Slot(name="on_ready_stdout") def on_ready_stdout(self): """Emit data from stdout.""" if not self._process: return out = str(self._process.readAllStandardOutput().data(), "utf-8") self._logger.msg_proc.emit(out.strip()) @Slot(name="on_ready_stderr") def on_ready_stderr(self): """Emit data from stderr.""" if not self._process: return err = str(self._process.readAllStandardError().data(), "utf-8") self._logger.msg_proc_error.emit(err.strip())
class WebMediaView(MediaView): def __init__(self, media, parent): super(WebMediaView, self).__init__(media, parent) self.widget = QWidget(parent) self.process = QProcess(self.widget) self.process.setObjectName('%s-process' % self.objectName()) self.std_out = [] self.errors = [] self.stopping = False self.mute = False self.widget.setGeometry(media['geometry']) self.connect(self.process, SIGNAL('error()'), self.process_error) self.connect(self.process, SIGNAL('finished()'), self.process_finished) self.connect(self.process, SIGNAL('started()'), self.process_started) self.set_default_widget_prop() self.stop_timer = QTimer(self) self.stop_timer.setSingleShot(True) self.stop_timer.setInterval(1000) self.stop_timer.timeout.connect(self.process_timeout) self.rect = self.widget.geometry() @Slot() def process_timeout(self): os.kill(self.process.pid(), signal.SIGTERM) self.stopping = False if not self.is_started(): self.started_signal.emit() super(WebMediaView, self).stop() @Slot(object) def process_error(self, err): print('---- process error ----') self.errors.append(err) self.stop() @Slot() def process_finished(self): self.stop() @Slot() def process_started(self): self.stop_timer.stop() if float(self.duration) > 0: self.play_timer.setInterval(int(float(self.duration) * 1000)) self.play_timer.start() self.started_signal.emit() pass @Slot() def play(self): self.finished = 0 self.widget.show() self.widget.raise_() #---- kong ---- url = self.options['uri'] args = [ str(self.rect.left()), str(self.rect.top()), str(self.rect.width()), str(self.rect.height()), QUrl.fromPercentEncoding(QByteArray(url.encode('utf-8'))) ] #self.process.start('dist/web.exe', args) # for windows #self.process.start('./dist/web', args) # for RPi self.stop_timer.start() #---- @Slot() def stop(self, delete_widget=False): #---- kong ---- if not self.widget: return False if self.stopping or self.is_finished(): return False self.stop_timer.start() self.stopping = True if self.process.state() == QProcess.ProcessState.Running: #---- kill process ---- self.process.terminate() # for windows self.process.kill() # for linux #os.system('pkill web') # for RPi #---- self.process.waitForFinished() self.process.close() super(WebMediaView, self).stop(delete_widget) self.stopping = False self.stop_timer.stop() return True
class VidConvertWindow(QWidget, Ui_Form): """this is the main class for video converter""" def __init__(self): super().__init__() self.setupUi(self) # class variable declarations self.available_formats = [] self.selected_files = [] self.process_argument = "" self.current_file_duration = None self.duration_re = re.compile(r'Duration: ([0-9:.]+)') self.time_re = re.compile(r'time=\s*([0-9:.]+) ') self.process = QProcess() self.kill_process = QProcess() self.file_picker = QFileDialog(self) self.output_dir = "" self.current_file_idx = 0 self.conversion_started = False self.output_folder_picker = QFileDialog(self) self.output_folder_picker.setFileMode(QFileDialog.DirectoryOnly) # listview & models self.file_list_model = QStandardItemModel(self.listViewFiles) self.listViewFiles.setModel(self.file_list_model) self.type_list_model = QStandardItemModel(self.listViewTypes) self.listViewTypes.setModel(self.type_list_model) # signals & slots self.process.readyReadStandardError.connect(self.read_output) self.file_list_model.itemChanged.connect(self.update_selected_files) self.btnStop.clicked.connect(self.stop_convertion) self.btnAdd.clicked.connect(self.add_files) self.btnConvert.clicked.connect(self.start_convertion) # call post_init self.post_init() def post_init(self): """runs after init""" self.btnConvert.setIcon(QPixmap('./icons/start.ico')) self.btnAdd.setIcon(QPixmap('./icons/file.ico')) self.btnStop.setIcon(QPixmap('./icons/stop.ico')) self.progressBarCurrent.setValue(0) self.progressBarTotal.setValue(0) self.setWindowTitle("Simple Video Converter") self.setGeometry(100, 100, 640, 480) for filetype in self.available_formats: self.add_item2model(filetype, self.type_list_model) def add_item2model(self, filename: str, model: QStandardItemModel): """sample listview code""" list_item = QStandardItem(filename) list_item.setCheckable(True) list_item.setEditable(False) list_item.setSelectable(False) model.appendRow(list_item) def update_selected_files(self, item): """selects the checked items""" if item.checkState() == Qt.CheckState.Checked and item.text( ) not in self.selected_files: self.selected_files.append(item.text()) else: self.selected_files.remove(item.text()) if DEBUG: print(self.selected_files) def add_files(self): """opens file picker for choosing files""" self.file_picker.setFileMode(QFileDialog.ExistingFiles) self.file_picker.setNameFilter("Videos (*.mp4 *.mkv *.mov)") self.file_picker.setViewMode(QFileDialog.Detail) if self.file_picker.exec_(): files_selected = self.file_picker.selectedFiles() for file in files_selected: self.add_item2model(file, self.file_list_model) if DEBUG: print(file) def get_file_name(self, idx: int, suffix: str): """returns the filename from path (static)""" return Path(self.selected_files[idx]).with_suffix(suffix).name def start_convertion(self): """implement conversion task""" # check if output dir is already defined else get outdir if not self.conversion_started: self.get_output_dir() self.progressBarTotal.setMaximum(len(self.selected_files)) self.btnConvert.setEnabled(False) self.conversion_started = True # setting up arguments current_file_name = self.selected_files[self.current_file_idx] current_outfile_name = Path(self.output_dir).joinpath( self.get_file_name(self.current_file_idx, ".avi")) self.process_argument = " -i {} {}".format(current_file_name, current_outfile_name) # create a process everytime it's called # NOTE: this is a local instance of the process so it changes after every call process = QProcess() process.readyReadStandardError.connect( lambda: self.parse_output(process)) process.finished.connect(self.recursion_handler) process.started.connect(lambda: self.ref_process(process)) process.start("ffmpeg", self.process_argument.split()) def ref_process(self, process): self.process = process def get_output_dir(self): """ get the output directory """ if self.output_folder_picker.exec_(): self.output_dir = self.output_folder_picker.selectedFiles()[0] def recursion_handler(self): """controls the multiple process iterations""" # prepare next file for xonversion self.current_file_idx += 1 self.current_file_duration = None self.progressBarCurrent.setValue(0) self.progressBarTotal.setValue(self.current_file_idx) # check if the number of files converted exceed total number if self.current_file_idx == len(self.selected_files): print("conversion complete!") self.btnConvert.setEnabled(True) self.conversion_started = False return # if everything okay, start conversion again self.start_convertion() @staticmethod def parse_time(time): """parsing time format to second""" _t = list(map(float, time.split(":"))) return _t[0] * 3600 + _t[1] * 60 + _t[2] def parse_output(self, process): """ parses current progress """ # update progress data = process.readAllStandardError().data().decode("utf-8") if self.current_file_duration is None: match = self.duration_re.search(data) if match: self.current_file_duration = self.parse_time(match.group(1)) else: match = self.time_re.search(data) if match: current_progress = self.parse_time(match.group(1)) self.progressBarCurrent.setValue( min((current_progress / self.current_file_duration) * 100, 100)) # deprecated def read_output(self): """process the output of ffmpeg""" data = self.process.readAllStandardError().data().decode("utf-8") if self.current_file_duration is None: match = self.duration_re.search(data) if match: self.current_file_duration = self.parse_time(match.group(1)) else: match = self.time_re.search(data) if match: current_progress = self.parse_time(match.group(1)) self.progressBarCurrent.setValue( min((current_progress / self.current_file_duration) * 100, 100)) def stop_convertion(self): """stop running coversion task""" # print("Not implemented") print("testing stop") # pid = self.process.processId() if sys.platform == 'linux': self.kill_process.start("pkill", "ffmpeg".split()) self.process.terminate()
class Lnd(object): bitcoin: Bitcoin client: LndClient file: ConfigurationFile software: LndSoftware process: QProcess def __init__(self, configuration_file_path: str, bitcoin: Bitcoin): self.running = False self.is_unlocked = False self.bitcoin = bitcoin self.file = ConfigurationFile(configuration_file_path) self.software = LndSoftware() self.lnddir = LND_DIR_PATH[OPERATING_SYSTEM] # Previous versions of the launcher set lnddir in the config file, # but it is not a valid key so this helps old users upgrading if self.file['lnddir'] is not None: self.file['lnddir'] = None if self.file['debuglevel'] is None: self.file['debuglevel'] = 'info' self.file['bitcoin.active'] = True self.file['bitcoin.node'] = 'bitcoind' self.file['bitcoind.rpchost'] = f'127.0.0.1:{self.bitcoin.rpc_port}' self.file['bitcoind.rpcuser'] = self.bitcoin.file['rpcuser'] self.file['bitcoind.rpcpass'] = self.bitcoin.file['rpcpassword'] self.file['bitcoind.zmqpubrawblock'] = self.bitcoin.file[ 'zmqpubrawblock'] self.file['bitcoind.zmqpubrawtx'] = self.bitcoin.file['zmqpubrawtx'] if self.file['restlisten'] is None: if self.bitcoin.file['testnet']: self.rest_port = get_port(LND_DEFAULT_REST_PORT + 1) else: self.rest_port = get_port(LND_DEFAULT_REST_PORT) self.file['restlisten'] = f'127.0.0.1:{self.rest_port}' else: self.rest_port = self.file['restlisten'].split(':')[-1] if not self.file['rpclisten']: if self.bitcoin.file['testnet']: self.grpc_port = get_port(LND_DEFAULT_GRPC_PORT + 1) else: self.grpc_port = get_port(LND_DEFAULT_GRPC_PORT) self.file['rpclisten'] = f'127.0.0.1:{self.grpc_port}' else: self.grpc_port = int(self.file['rpclisten'].split(':')[-1]) if not self.file['tlsextraip']: self.file['tlsextraip'] = '127.0.0.1' if self.file['color'] is None: self.file['color'] = '#000000' self.macaroon_path = os.path.join( self.lnddir, 'data', 'chain', 'bitcoin', str(self.bitcoin.network) ) self.config_snapshot = self.file.snapshot.copy() self.file.file_watcher.fileChanged.connect(self.config_file_changed) self.bitcoin.file.file_watcher.fileChanged.connect( self.bitcoin_config_file_changed) self.process = QProcess() self.process.setProgram(self.software.lnd) self.process.setCurrentReadChannel(0) self.process.setArguments(self.args) self.client = LndClient(self) @property def args(self): if IS_WINDOWS: arg_list = [ f'--configfile={self.file.path}', ] else: arg_list = [ f'--configfile="{self.file.path}"', ] if self.bitcoin.file['testnet']: arg_list += [ '--bitcoin.testnet' ] else: arg_list += [ '--bitcoin.mainnet' ] return arg_list @property def node_port(self) -> str: if self.file['listen'] is None: if self.bitcoin.file['testnet']: port = get_port(LND_DEFAULT_PEER_PORT + 1) else: port = get_port(LND_DEFAULT_PEER_PORT) self.file['listen'] = f'127.0.0.1:{port}' else: if not isinstance(self.file['listen'], list): port = self.file['listen'].split(':')[-1] else: port = self.file['listen'][0].split(':')[-1] return port def test_tls_cert(self): context = ssl.create_default_context() context.load_verify_locations(cafile=self.tls_cert_path) conn = context.wrap_socket(socket.socket(socket.AF_INET), server_hostname='127.0.0.1') conn.connect(('127.0.0.1', int(self.rest_port))) cert = conn.getpeercert() return cert @property def admin_macaroon_path(self) -> str: path = os.path.join(self.macaroon_path, 'admin.macaroon') return path @property def wallet_path(self) -> str: wallet_path = os.path.join(self.macaroon_path, 'wallet.db') return wallet_path @property def has_wallet(self) -> bool: return os.path.isfile(self.wallet_path) @property def tls_cert_path(self) -> str: tls_cert_path = os.path.join(self.lnddir, 'tls.cert') return tls_cert_path def lncli_arguments(self) -> List[str]: args = [] if self.grpc_port != LND_DEFAULT_GRPC_PORT: args.append(f'--rpcserver=127.0.0.1:{self.grpc_port}') if self.bitcoin.file['testnet']: args.append(f'--network={self.bitcoin.network}') if self.lnddir != LND_DIR_PATH[OPERATING_SYSTEM]: args.append(f'''--lnddir="{self.lnddir}"''') args.append(f'--macaroonpath="{self.macaroon_path}"') args.append(f'--tlscertpath="{self.tls_cert_path}"') return args @property def lncli(self) -> str: base_command = [ f'"{self.software.lncli}"', ] base_command += self.lncli_arguments() return ' '.join(base_command) @property def rest_url(self) -> str: return f'https://127.0.0.1:{self.rest_port}' @property def grpc_url(self) -> str: return f'127.0.0.1:{self.grpc_port}' def config_file_changed(self): # Refresh config file self.file.file_watcher.blockSignals(True) self.file.populate_cache() self.file.file_watcher.blockSignals(False) if self.file['restlisten']: self.rest_port = int(self.file['restlisten'].split(':')[-1]) if self.file['rpclisten']: self.grpc_port = int(self.file['rpclisten'].split(':')[-1]) # Some text editors do not modify the file, they delete and replace the file # Check if file is still in file_watcher list of files, if not add back files_watched = self.file.file_watcher.files() if len(files_watched) == 0: self.file.file_watcher.addPath(self.file.path) def bitcoin_config_file_changed(self): # Refresh config file self.file.file_watcher.blockSignals(True) self.file.populate_cache() self.file.file_watcher.blockSignals(False) self.file['bitcoind.rpchost'] = f'127.0.0.1:{self.bitcoin.rpc_port}' self.file['bitcoind.rpcuser'] = self.bitcoin.file['rpcuser'] self.file['bitcoind.rpcpass'] = self.bitcoin.file['rpcpassword'] self.file['bitcoind.zmqpubrawblock'] = self.bitcoin.file[ 'zmqpubrawblock'] self.file['bitcoind.zmqpubrawtx'] = self.bitcoin.file['zmqpubrawtx'] @property def restart_required(self): if self.running: # Did bitcoin details change if self.bitcoin.restart_required: return True and self.running old_config = self.config_snapshot.copy() new_config = self.file.snapshot fields = [ 'restlisten', 'listen', 'rpclisten' ] for field in fields: # First check if field is found in both configs found_in_old_config = field in old_config.keys() found_in_new_config = field in new_config.keys() if found_in_old_config != found_in_new_config: return True # Now check that values are the same if found_in_old_config: if old_config[field] != new_config[field]: return True return False @staticmethod def base64URL_from_base64(s): return s.replace('+', '-').replace('/', '_').rstrip('=') @property def lndconnect_url(self): host = self.grpc_url.split(':')[0] port = self.grpc_url.split(':')[1] return f'lndconnect://{host}:{port}' \ f'?cert={self.tls_cert_path}&macaroon={self.admin_macaroon_path}' @property def lndconnect_qrcode(self): img = qrcode.make(self.lndconnect_url) return img def reset_tls(self): os.remove(self.client.tls_cert_path) os.remove(self.client.tls_key_path) self.process.terminate() self.client.reset()