diff --git a/Furious/Action/Connect.py b/Furious/Action/Connect.py index 19b0638..7415b12 100644 --- a/Furious/Action/Connect.py +++ b/Furious/Action/Connect.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Furious.Core.Core import XrayCore, Hysteria1, Hysteria2 +from Furious.Core.Core import XrayCore, Hysteria1, Hysteria2, StdoutRedirectHelper from Furious.Core.TorRelay import TorRelay from Furious.Core.Tun2socks import Tun2socks from Furious.Core.Intellisense import Intellisense @@ -46,6 +46,7 @@ from Furious.Utility.Utility import ( isValidIPAddress, isAdministrator, isVPNMode, + isPythonw, ) from Furious.Utility.Translator import gettext as _ from Furious.Utility.Proxy import Proxy @@ -60,9 +61,11 @@ from PySide6.QtNetwork import ( QNetworkProxy, ) +import uuid import ujson import random import logging +import pathlib logger = logging.getLogger(__name__) @@ -592,34 +595,68 @@ class ConnectAction(Action): self.errorHttpsProxyConf() else: if validateHttpsProxyServer(httpsProxyServer): - - def fixLoggingRelativePath(attr): - # Relative path fails if booting on start up - # on Windows, when packed using nuitka... - - # Fix relative path if needed. User cannot feel this operation. - - try: - path = self.coreJSON['log'][attr] - except KeyError: - pass - else: - if path and ( - isinstance(path, str) or isinstance(path, bytes) - ): - fix = getAbsolutePath(path) - - logger.info( - f'{XrayCore.name()}: {attr} log is specified as \'{path}\'. ' - f'Fixed to \'{fix}\'' - ) - - self.coreJSON['log'][attr] = fix - logger.info(f'core {XrayCore.name()} configured') - fixLoggingRelativePath('access') - fixLoggingRelativePath('error') + if self.coreJSON.get('log') is None or not isinstance( + self.coreJSON['log'], dict + ): + self.coreJSON['log'] = { + 'access': '', + 'error': '', + 'loglevel': 'warning', + } + + redirect = str(uuid.uuid4()) + + def fixLogObjectPath(logAttr): + try: + path = self.coreJSON['log'][logAttr] + except Exception: + # Any non-exit exceptions + + self.coreJSON['log'][logAttr] = path = '' + + if not isinstance(path, str) and not isinstance(path, bytes): + self.coreJSON['log'][logAttr] = path = '' + + if path == '': + if ( + isPythonw() + and StdoutRedirectHelper.TemporaryDir.isValid() + ): + # Redirect implementation + self.coreJSON['log'][ + logAttr + ] = StdoutRedirectHelper.TemporaryDir.filePath(redirect) + else: + # Relative path fails if booting on start up + # on Windows, when packed using nuitka... + + # Fix relative path if needed. User cannot feel this operation. + self.coreJSON['log'][logAttr] = getAbsolutePath(path) + + result = self.coreJSON['log'][logAttr] + + if result: + try: + # Create a new file + with open(result, 'x'): + pass + except FileExistsError: + pass + except Exception: + # Any non-exit exceptions + + pass + + logger.info( + f'{XrayCore.name()}: {logAttr} log is specified as \'{path}\'. ' + f'Fixed to \'{result}\'' + ) + + # Fix logObject + for attr in ['access', 'error']: + fixLogObjectPath(attr) routing = APP().Routing diff --git a/Furious/Core/Core.py b/Furious/Core/Core.py index 1954c01..a60bda4 100644 --- a/Furious/Core/Core.py +++ b/Furious/Core/Core.py @@ -16,14 +16,16 @@ # along with this program. If not, see . from Furious.Utility.Constants import APP, PLATFORM, LogType -from Furious.Utility.Utility import getAbsolutePath +from Furious.Utility.Utility import getAbsolutePath, isPythonw from PySide6 import QtCore from PySide6.QtTest import QTest +import io import os import sys import uuid +import ujson import logging import functools import threading @@ -33,6 +35,13 @@ logger = logging.getLogger(__name__) class Core: + class ExitCode: + ConfigurationError = 23 + # Windows: 4294967295. Darwin, Linux: 255 (-1) + ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255 + # Windows shutting down + SystemShuttingDown = 0x40010004 + def __init__(self, *args, exitCallback=None, **kwargs): self._process = None self._exitCallback = exitCallback @@ -66,6 +75,12 @@ class Core: def registerExitCallback(self, exitCallback): self._exitCallback = exitCallback + def isAlive(self): + if isinstance(self._process, multiprocessing.Process): + return self._process.is_alive() + else: + return False + def checkAlive(self): assert isinstance(self._process, multiprocessing.Process) @@ -103,10 +118,7 @@ class Core: self._daemonTimer.start() def stop(self): - if ( - isinstance(self._process, multiprocessing.Process) - and self._process.is_alive() - ): + if self.isAlive(): self._stdoutTimer.stop() self._daemonTimer.stop() @@ -129,22 +141,6 @@ class Core: class StdoutRedirectHelper: TemporaryDir = QtCore.QTemporaryDir() - @staticmethod - def isRealFile(file): - if not hasattr(file, 'fileno'): - return False - - try: - tmp = os.dup(file.fileno()) - except Exception: - # Any non-exit exceptions - - return False - else: - os.close(tmp) - - return True - @staticmethod def launch(msgQueue, entrypoint, redirect): if not callable(entrypoint): @@ -154,8 +150,7 @@ class StdoutRedirectHelper: not StdoutRedirectHelper.TemporaryDir.isValid() or not redirect # pythonw.exe - or not StdoutRedirectHelper.isRealFile(sys.__stdout__) - or not StdoutRedirectHelper.isRealFile(sys.__stderr__) + or isPythonw() ): # Call entrypoint directly entrypoint() @@ -204,14 +199,82 @@ def startXrayCore(json, msgQueue): while True: pass else: - redirect = True - if xray.__version__ <= '1.8.4': redirect = False + else: + redirect = True - StdoutRedirectHelper.launch( - msgQueue, lambda: xray.startFromJSON(json), redirect - ) + # Can be redirected + + if not isPythonw(): + StdoutRedirectHelper.launch( + msgQueue, lambda: xray.startFromJSON(json), redirect + ) + + return + + if not redirect: + xray.startFromJSON(json) + + return + + def xrayPythonwProduceMsg(): + try: + jsonObject = ujson.loads(json) + except Exception: + # Any non-exit exceptions + + xray.startFromJSON(json) + + return + + loggerPath = [] + fileStream = [] + + for attr in ['access', 'error']: + try: + path = jsonObject['log'][attr] + except Exception: + # Any non-exit exceptions + + continue + + if path not in loggerPath: + loggerPath.append(path) + + try: + stream = open(path, 'rb') + except Exception: + # Any non-exit exceptions + + pass + else: + stream.seek(0, io.SEEK_END) + + fileStream.append(stream) + + def produceMsg(): + while True: + for file in fileStream: + 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 + + try: + msgThread = threading.Thread(target=produceMsg, daemon=True) + msgThread.start() + + xray.startFromJSON(json) + finally: + for stream in fileStream: + stream.close() + + xrayPythonwProduceMsg() class XrayCore(Core): @@ -253,10 +316,10 @@ def startHysteria1(json, rule, mmdb, msgQueue): while True: pass else: - redirect = True - if hysteria.__version__ <= '1.3.5': redirect = False + else: + redirect = True StdoutRedirectHelper.launch( msgQueue, lambda: hysteria.startFromJSON(json, rule, mmdb), redirect @@ -339,10 +402,10 @@ def startHysteria2(json, msgQueue): while True: pass else: - redirect = True - if hysteria2.__version__ <= '2.0.0.1': redirect = False + else: + redirect = True StdoutRedirectHelper.launch( msgQueue, lambda: hysteria2.startFromJSON(json), redirect