mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-28 18:08:11 +03:00
208 lines
5.8 KiB
Python
208 lines
5.8 KiB
Python
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
|
#
|
|
# This file is part of Furious.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import annotations
|
|
|
|
from Furious.Interface import *
|
|
from Furious.Utility import *
|
|
|
|
from PySide6 import QtCore
|
|
|
|
from abc import ABC
|
|
from typing import Callable
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import uuid
|
|
import logging
|
|
import threading
|
|
import multiprocessing
|
|
|
|
__all__ = ['CoreProcess', 'StdoutRedirectHelper']
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CoreProcess(CoreFactory, ABC):
|
|
MESG_PRODUCE_THRESHOLD = 250
|
|
|
|
def __init__(self, **kwargs):
|
|
exitCallback = kwargs.pop('exitCallback', None)
|
|
|
|
super().__init__(exitCallback)
|
|
|
|
self._process = None
|
|
self._msgQueue = multiprocessing.Queue()
|
|
self._msgCallback = kwargs.pop('msgCallback', None)
|
|
|
|
@QtCore.Slot()
|
|
def handleMsgTimemout():
|
|
msg = self.getMsgNoWait()
|
|
|
|
if msg and not msg.isspace():
|
|
if callable(self._msgCallback):
|
|
self._msgCallback(msg)
|
|
|
|
self._msgTimer = QtCore.QTimer()
|
|
self._msgTimer.timeout.connect(handleMsgTimemout)
|
|
|
|
@QtCore.Slot()
|
|
def handleDaemonTimemout():
|
|
self.checkIsRunning()
|
|
|
|
self._daemonTimer = QtCore.QTimer()
|
|
self._daemonTimer.timeout.connect(handleDaemonTimemout)
|
|
|
|
@property
|
|
def msgQueue(self) -> multiprocessing.Queue:
|
|
return self._msgQueue
|
|
|
|
def registerMsgCallback(self, msgCallback) -> CoreProcess:
|
|
self._msgCallback = msgCallback
|
|
|
|
return self
|
|
|
|
def isRunning(self) -> bool:
|
|
if isinstance(self._process, multiprocessing.Process):
|
|
return self._process.is_alive()
|
|
else:
|
|
return False
|
|
|
|
def checkIsRunning(self) -> bool:
|
|
if isinstance(self._process, multiprocessing.Process):
|
|
if self._process.is_alive():
|
|
return True
|
|
else:
|
|
logger.error(
|
|
f'{self.name()} stopped unexpectedly with exitcode {self._process.exitcode}'
|
|
)
|
|
|
|
self._msgTimer.stop()
|
|
self._daemonTimer.stop()
|
|
|
|
if callable(self._exitCallback):
|
|
self._exitCallback(self, self._process.exitcode)
|
|
|
|
# Reset internal process
|
|
self._process = None
|
|
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
def start(self, **kwargs) -> bool:
|
|
daemon = kwargs.pop('daemon', True)
|
|
waitCore = kwargs.pop('waitCore', True)
|
|
waitTime = kwargs.pop('waitTime', 2500)
|
|
|
|
self._process = multiprocessing.Process(**kwargs, daemon=daemon)
|
|
self._process.start()
|
|
|
|
logger.info(f'{self.name()} {self.version()} started')
|
|
|
|
self._msgTimer.start(self.MESG_PRODUCE_THRESHOLD)
|
|
|
|
if waitCore:
|
|
# Wait for the core to start up completely
|
|
PySide6LegacyEventLoopWait(waitTime)
|
|
|
|
if self.checkIsRunning():
|
|
# Start core daemon
|
|
self._daemonTimer.start(CORE_CHECK_ALIVE_INTERVAL)
|
|
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def stop(self):
|
|
if self.isRunning():
|
|
self._msgTimer.stop()
|
|
self._daemonTimer.stop()
|
|
|
|
self._process.terminate()
|
|
self._process.join()
|
|
|
|
logger.info(
|
|
f'{self.name()} terminated with exitcode {self._process.exitcode}'
|
|
)
|
|
|
|
def getMsgNoWait(self) -> str:
|
|
try:
|
|
return self._msgQueue.get_nowait()
|
|
except Exception:
|
|
# Any non-exit exceptions
|
|
|
|
return ''
|
|
|
|
|
|
class StdoutRedirectHelper:
|
|
TemporaryDir = QtCore.QTemporaryDir()
|
|
|
|
@staticmethod
|
|
def launch(
|
|
msgQueue: multiprocessing.Queue, entrypoint: Callable[[], None], redirect: bool
|
|
):
|
|
if not callable(entrypoint):
|
|
return
|
|
|
|
if (
|
|
not StdoutRedirectHelper.TemporaryDir.isValid()
|
|
or not redirect
|
|
# pythonw.exe
|
|
or isPythonw()
|
|
):
|
|
# Call entrypoint directly
|
|
entrypoint()
|
|
|
|
return
|
|
|
|
temporaryFile = StdoutRedirectHelper.TemporaryDir.filePath(str(uuid.uuid4()))
|
|
tmpFileStream = open(temporaryFile, 'w+b')
|
|
stdoutFileno_ = sys.stdout.fileno()
|
|
stderrFileno_ = sys.stderr.fileno()
|
|
|
|
sys.stdout.close()
|
|
sys.stderr.close()
|
|
|
|
# Redirect
|
|
os.dup2(tmpFileStream.fileno(), stdoutFileno_)
|
|
os.dup2(tmpFileStream.fileno(), stderrFileno_)
|
|
|
|
sys.stdout = tmpFileStream
|
|
sys.stderr = tmpFileStream
|
|
|
|
def produceMsg():
|
|
with open(temporaryFile, 'rb') as file:
|
|
while True:
|
|
for line in iter(file.readline, b''):
|
|
if line and not line.isspace():
|
|
try:
|
|
msgQueue.put_nowait(line.decode('utf-8', 'replace'))
|
|
except Exception:
|
|
# Any non-exit exceptions
|
|
|
|
pass
|
|
|
|
time.sleep(CoreProcess.MESG_PRODUCE_THRESHOLD / 1000)
|
|
|
|
msgThread = threading.Thread(target=produceMsg, daemon=True)
|
|
msgThread.start()
|
|
|
|
with tmpFileStream:
|
|
entrypoint()
|