Add support for Xray-core logging in Pythonw mode

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2023-10-15 13:43:12 +08:00
parent 46ffcaa14c
commit f9d8f24fc9
2 changed files with 159 additions and 59 deletions
+64 -27
View File
@@ -15,7 +15,7 @@
# 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 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
+95 -32
View File
@@ -16,14 +16,16 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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