Fix bugs & various optimizations:
- Adjust for installations where venv was not configured. - Change update strategy to ensure update goes through on all platforms after compilation. - Make sure that paths work on both Windows & Linux. - Adjust update strategy to make it work on both Windows & Linux.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
import platform
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import site
|
||||
import sys
|
||||
import venv
|
||||
import subprocess
|
||||
import yaml
|
||||
import pygit2
|
||||
@@ -24,18 +24,18 @@ class ModulesManager:
|
||||
|
||||
def __init__(self, mainWindow):
|
||||
self.mainWindow = mainWindow
|
||||
self.system = platform.system()
|
||||
self.baseAppStoragePath = Path(
|
||||
QtCore.QStandardPaths.standardLocations(
|
||||
QtCore.QStandardPaths.StandardLocation.AppDataLocation)[0])
|
||||
self.baseAppStoragePath.mkdir(exist_ok=True, parents=True)
|
||||
self.modulesBaseDirectoryPath = self.baseAppStoragePath / "User Module Packs Storage"
|
||||
self.modulesBaseDirectoryPath.mkdir(exist_ok=True)
|
||||
self.browsersBaseDirectoryPath = self.baseAppStoragePath / "Browsers"
|
||||
self.browsersBaseDirectoryPath.mkdir(exist_ok=True)
|
||||
self.modulesRequirementsPath = self.modulesBaseDirectoryPath / "requirements.txt"
|
||||
self.modulesRequirementsPath.touch(mode=0o700, exist_ok=True)
|
||||
self.modulesRequirementsTempPath = self.modulesBaseDirectoryPath / "requirements.txt.tmp"
|
||||
self.modulesPythonPath = self.modulesBaseDirectoryPath / 'bin' / 'python3'
|
||||
self.modulesPythonPath = self.modulesBaseDirectoryPath / 'bin' / 'python3.11' if self.system == "Linux" \
|
||||
else self.modulesBaseDirectoryPath / 'Scripts' / 'python.exe'
|
||||
self.upgradeThread = None
|
||||
self.moduleReqsThread = None
|
||||
self.uninstallThread = None
|
||||
@@ -50,7 +50,8 @@ class ModulesManager:
|
||||
self.venvThread.start()
|
||||
|
||||
def afterUpgrade(self, upgradeStatus: bool):
|
||||
self.mainWindow.MESSAGEHANDLER.info(f"Upgrade status: {'Success' if upgradeStatus else 'Failed'}")
|
||||
self.mainWindow.MESSAGEHANDLER.info(f"Module environment upgrade status: "
|
||||
f"{'Success' if upgradeStatus else 'Failed'}")
|
||||
self.mainWindow.MESSAGEHANDLER.info('Modules Loaded.')
|
||||
self.mainWindow.setStatus('Environment Updated, Modules Loaded.')
|
||||
|
||||
@@ -70,26 +71,42 @@ class ModulesManager:
|
||||
# {} == False
|
||||
return fileContents if fileContents is not None else {}
|
||||
|
||||
def configureVenv(self, venvPath):
|
||||
binDir = os.path.dirname(venvPath / 'bin')
|
||||
base = binDir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
||||
def configureVenvLinux(self):
|
||||
binDir = self.modulesBaseDirectoryPath / 'bin'
|
||||
|
||||
# prepend bin to PATH (this file is inside the bin directory)
|
||||
os.environ["PATH"] = os.pathsep.join([binDir] + os.environ.get("PATH", "").split(os.pathsep))
|
||||
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
||||
# prepend bin to PATH
|
||||
os.environ["PATH"] = f'{binDir}{os.pathsep}{os.environ.get("PATH", "")}'
|
||||
os.environ["VIRTUAL_ENV"] = str(self.modulesBaseDirectoryPath)
|
||||
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(self.browsersBaseDirectoryPath)
|
||||
|
||||
packagesPath = str(list((self.modulesBaseDirectoryPath / 'lib').glob('python*'))[0] / 'site-packages')
|
||||
|
||||
sys.path.append(packagesPath)
|
||||
site.addsitedir(packagesPath)
|
||||
|
||||
sys.prefix = str(self.modulesBaseDirectoryPath)
|
||||
|
||||
def configureVenvWindows(self):
|
||||
binDir = self.modulesBaseDirectoryPath / 'Scripts'
|
||||
|
||||
# prepend Scripts to PATH
|
||||
os.environ["PATH"] = f'{binDir}{os.pathsep}{os.environ.get("PATH", "")}'
|
||||
os.environ["VIRTUAL_ENV"] = str(self.modulesBaseDirectoryPath)
|
||||
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(self.browsersBaseDirectoryPath)
|
||||
|
||||
# add the virtual environments libraries to the host python import mechanism
|
||||
prevLength = len(sys.path)
|
||||
packagesPath = str(list((venvPath / 'lib').glob('python*'))[0] / 'site-packages')
|
||||
packagesPath = str(self.modulesBaseDirectoryPath / 'Lib' / 'site-packages')
|
||||
|
||||
for lib in packagesPath.split(os.pathsep):
|
||||
path = os.path.realpath(os.path.join(binDir, lib))
|
||||
site.addsitedir(path)
|
||||
sys.path[:] = sys.path[prevLength:] + sys.path[:prevLength]
|
||||
sys.path.append(packagesPath)
|
||||
site.addsitedir(packagesPath)
|
||||
|
||||
sys.real_prefix = sys.prefix
|
||||
sys.prefix = base
|
||||
sys.prefix = str(self.modulesBaseDirectoryPath)
|
||||
|
||||
def configureVenv(self):
|
||||
if self.system == "Linux":
|
||||
self.configureVenvLinux()
|
||||
else:
|
||||
self.configureVenvWindows()
|
||||
|
||||
self.loadAllModules()
|
||||
|
||||
@@ -776,18 +793,30 @@ class AddModulePackDialog(QtWidgets.QDialog):
|
||||
|
||||
|
||||
class InitialiseVenvThread(QtCore.QThread):
|
||||
configureVenvOfMainThreadSignal = QtCore.Signal(Path)
|
||||
configureVenvOfMainThreadSignal = QtCore.Signal()
|
||||
|
||||
def __init__(self, modulesManager: ModulesManager) -> None:
|
||||
super().__init__()
|
||||
self.modulesManager = modulesManager
|
||||
|
||||
def run(self) -> None:
|
||||
venvPath = self.modulesManager.modulesBaseDirectoryPath
|
||||
if not (venvPath / 'bin').exists():
|
||||
venv.create(venvPath, symlinks=True, with_pip=True, upgrade_deps=True)
|
||||
if self.modulesManager.system == "Linux":
|
||||
localBinPath = str(Path.home() / '.local' / 'bin')
|
||||
currentPath = os.environ.get("PATH", "")
|
||||
if localBinPath not in currentPath.split(os.pathsep):
|
||||
os.environ["PATH"] = currentPath + os.pathsep + localBinPath
|
||||
|
||||
self.configureVenvOfMainThreadSignal.emit(venvPath)
|
||||
binPath = self.modulesManager.modulesBaseDirectoryPath / 'bin' \
|
||||
if self.modulesManager.system == "Linux" \
|
||||
else self.modulesManager.modulesBaseDirectoryPath / 'Scripts'
|
||||
if not binPath.exists():
|
||||
pythonExecutable = "python3.11" if self.modulesManager.system == "Linux" else "python.exe"
|
||||
cmdStr = (f'{pythonExecutable} -m venv --symlinks --clear --upgrade-deps '
|
||||
f'"{self.modulesManager.modulesBaseDirectoryPath}"')
|
||||
subprocess.run(cmdStr, shell=True)
|
||||
self.modulesManager.modulesRequirementsPath.touch(mode=0o700, exist_ok=True)
|
||||
|
||||
self.configureVenvOfMainThreadSignal.emit()
|
||||
|
||||
|
||||
class UpgradeVenvThread(QtCore.QThread):
|
||||
@@ -808,14 +837,14 @@ class UpgradeVenvThread(QtCore.QThread):
|
||||
else:
|
||||
reqsFile = self.modulesManager.modulesRequirementsPath
|
||||
try:
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' --version"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" --version'
|
||||
subprocess.check_output(cmdStr, shell=True)
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' -m pip install --upgrade -r '{reqsFile}'"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" -m pip install --upgrade -r "{reqsFile}"'
|
||||
subprocess.check_output(cmdStr, shell=True)
|
||||
# Install / upgrade playwright & misc if not already installed, since they need special treatment.
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' -m pip install --upgrade pip wheel setuptools playwright"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" -m pip install --upgrade pip wheel setuptools playwright'
|
||||
subprocess.run(cmdStr, shell=True)
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' -m playwright install"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" -m playwright install'
|
||||
subprocess.run(cmdStr, shell=True)
|
||||
collapse_browser_folders(self.modulesManager.browsersBaseDirectoryPath)
|
||||
self.upgradeVenvThreadSignal.emit(True)
|
||||
@@ -859,8 +888,8 @@ class InstallRequirementsThread(QtCore.QThread):
|
||||
self.progressSignal.emit(1)
|
||||
|
||||
success = False
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' -m pip install --upgrade -r " \
|
||||
f"'{self.modulesManager.modulesRequirementsTempPath}'"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" -m pip install --upgrade -r ' \
|
||||
f'"{self.modulesManager.modulesRequirementsTempPath}"'
|
||||
try:
|
||||
subprocess.check_output(cmdStr, shell=True)
|
||||
|
||||
@@ -920,7 +949,7 @@ class UninstallModuleThread(QtCore.QThread):
|
||||
[file.write(line + '\n') for line in newRequirementsSet]
|
||||
|
||||
if reqsDiff := allRequirementsSet.difference(newRequirementsSet):
|
||||
cmdStr = f"'{self.modulesManager.modulesPythonPath}' -m pip uninstall {' '.join(reqsDiff)} -y"
|
||||
cmdStr = f'"{self.modulesManager.modulesPythonPath}" -m pip uninstall {" ".join(reqsDiff)} -y'
|
||||
try:
|
||||
subprocess.check_output(cmdStr, shell=True)
|
||||
shutil.move(self.modulesManager.modulesRequirementsTempPath,
|
||||
|
||||
@@ -4,7 +4,10 @@ import contextlib
|
||||
import platform
|
||||
import subprocess
|
||||
import requests
|
||||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
import ctypes
|
||||
|
||||
from pathlib import Path
|
||||
from semver import compare
|
||||
@@ -40,8 +43,9 @@ class UpdateManager:
|
||||
def isUpdateAvailable(self):
|
||||
latest_version = self.getLatestVersion()
|
||||
return (
|
||||
latest_version is not None
|
||||
and compare(self.mainWindow.SETTINGS.value("Program/Version").lstrip('v'), latest_version.lstrip('v')) < 0
|
||||
latest_version is not None
|
||||
and compare(self.mainWindow.SETTINGS.value("Program/Version").lstrip('v'),
|
||||
latest_version.lstrip('v')) < 0
|
||||
)
|
||||
|
||||
def doUpdate(self) -> None:
|
||||
@@ -54,11 +58,22 @@ class UpdateManager:
|
||||
self.updateThread.start()
|
||||
self.mainWindow.MESSAGEHANDLER.info('Update in progress')
|
||||
|
||||
def finalizeUpdate(self, success: bool) -> None:
|
||||
if success:
|
||||
def finalizeUpdate(self, updateTempPath: str) -> None:
|
||||
if updateTempPath != '':
|
||||
latest_version = self.getLatestVersion()
|
||||
self.mainWindow.SETTINGS.setValue("Program/Version", latest_version)
|
||||
self.mainWindow.MESSAGEHANDLER.info('Updating done, please restart for the changes to take effect.')
|
||||
self.mainWindow.MESSAGEHANDLER.info("The application will now save and close to apply the updates. "
|
||||
"Please wait for a few minutes before reopening LinkScope.",
|
||||
popUp=True)
|
||||
|
||||
uncompressNewVersion(
|
||||
self.system,
|
||||
self.mainWindow.SETTINGS.value("Program/BaseDir"),
|
||||
str(self.baseSoftwarePath),
|
||||
updateTempPath)
|
||||
self.mainWindow.close()
|
||||
|
||||
else:
|
||||
self.mainWindow.MESSAGEHANDLER.info('Updating failed.')
|
||||
|
||||
@@ -99,7 +114,7 @@ class UpdaterWindow(QtWidgets.QDialog):
|
||||
|
||||
|
||||
class UpdaterThread(QtCore.QThread):
|
||||
updateDoneSignal = QtCore.Signal(bool)
|
||||
updateDoneSignal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, updateManager, mainWindow):
|
||||
super().__init__()
|
||||
@@ -107,19 +122,72 @@ class UpdaterThread(QtCore.QThread):
|
||||
self.updateManager = updateManager
|
||||
|
||||
def run(self) -> None:
|
||||
downloadUrl = self.updateManager.getDownloadURL()
|
||||
if self.updateManager.system == 'Windows':
|
||||
subprocess.run(
|
||||
['runas',
|
||||
'/user:Administrator',
|
||||
Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "UpdaterUtil.exe",
|
||||
downloadUrl,
|
||||
self.updateManager.baseSoftwarePath.parent]
|
||||
)
|
||||
elif self.updateManager.system == 'Linux':
|
||||
subprocess.run(
|
||||
['pkexec',
|
||||
Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "UpdaterUtil",
|
||||
downloadUrl,
|
||||
self.updateManager.baseSoftwarePath.parent]
|
||||
)
|
||||
try:
|
||||
downloadUrl = self.updateManager.getDownloadURL()
|
||||
clientTempCompressedArchive = tempfile.mkstemp(suffix='.7z')
|
||||
tempPath = clientTempCompressedArchive[1]
|
||||
|
||||
with os.fdopen(clientTempCompressedArchive[0], 'wb') as tempArchive:
|
||||
with requests.get(downloadUrl, stream=True) as fileStream:
|
||||
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
|
||||
tempArchive.write(chunk)
|
||||
self.updateDoneSignal.emit(tempPath)
|
||||
except Exception:
|
||||
self.updateDoneSignal.emit('')
|
||||
|
||||
|
||||
def uncompressNewVersion(system: str, baseDir: str, baseSoftwarePath: str, updateTempPath: str):
|
||||
tempDir = tempfile.mkdtemp(prefix='LinkScope_Updater_TMP_')
|
||||
|
||||
if system == 'Windows':
|
||||
updaterPath = Path(baseDir) / "UpdaterUtil.exe"
|
||||
|
||||
tempUpdaterPath = Path(tempDir) / updaterPath.name
|
||||
shutil.copy(updaterPath, tempUpdaterPath)
|
||||
|
||||
# This is done so that Windows spawns the updater as a detached process.
|
||||
ShellExecuteEx = ctypes.windll.shell32.ShellExecuteEx
|
||||
SEE_MASK_NO_CONSOLE = 0x00008000
|
||||
|
||||
class SHELLEXECUTEINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", ctypes.c_ulong),
|
||||
("fMask", ctypes.c_ulong),
|
||||
("hwnd", ctypes.c_void_p),
|
||||
("lpVerb", ctypes.c_char_p),
|
||||
("lpFile", ctypes.c_char_p),
|
||||
("lpParameters", ctypes.c_char_p),
|
||||
("lpDirectory", ctypes.c_char_p),
|
||||
("nShow", ctypes.c_int),
|
||||
("hInstApp", ctypes.c_void_p),
|
||||
("lpIDList", ctypes.c_void_p),
|
||||
("lpClass", ctypes.c_char_p),
|
||||
("hkeyClass", ctypes.c_void_p),
|
||||
("dwHotKey", ctypes.c_ulong),
|
||||
("hIconOrMonitor", ctypes.c_void_p),
|
||||
("hProcess", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
sei = SHELLEXECUTEINFO()
|
||||
sei.cbSize = ctypes.sizeof(sei)
|
||||
sei.fMask = SEE_MASK_NO_CONSOLE
|
||||
sei.lpVerb = b"runas"
|
||||
sei.lpFile = bytes(tempUpdaterPath)
|
||||
sei.lpParameters = f'"{updateTempPath}" "{baseSoftwarePath}"'.encode('utf-8')
|
||||
sei.nShow = 1
|
||||
|
||||
if not ShellExecuteEx(ctypes.byref(sei)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
elif system == 'Linux':
|
||||
updaterPath = Path(baseDir) / "UpdaterUtil"
|
||||
|
||||
tempUpdaterPath = Path(tempDir) / updaterPath.name
|
||||
shutil.copy(updaterPath, tempUpdaterPath)
|
||||
|
||||
subprocess.Popen(
|
||||
f'pkexec "{tempUpdaterPath}" "{updateTempPath}" "{baseSoftwarePath}"',
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
shell=True,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Load modules
|
||||
import contextlib
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -1085,9 +1086,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if browserDir is not None:
|
||||
browserPath = [subdirectory for subdirectory in browserDir.iterdir() if subdirectory.is_dir()][0]
|
||||
if browser == 'firefox':
|
||||
browserPath /= 'firefox'
|
||||
browserPath /= 'firefox.exe' if platform.system() == 'Windows' else 'firefox'
|
||||
elif browser == 'chromium':
|
||||
browserPath /= 'chrome'
|
||||
browserPath /= 'chrome.exe' if platform.system() == 'Windows' else 'chrome'
|
||||
else:
|
||||
self.MESSAGEHANDLER.critical('Cannot find installed playwright browsers.', exc_info=False)
|
||||
return browserPath
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
import requests
|
||||
import py7zr
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit(3)
|
||||
import py7zr
|
||||
|
||||
downloadUrl = sys.argv[1]
|
||||
softwarePath = sys.argv[2]
|
||||
|
||||
clientTempCompressedArchive = tempfile.mkstemp(suffix='.7z')
|
||||
tempPath = Path(clientTempCompressedArchive[1])
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit(3)
|
||||
|
||||
with os.fdopen(clientTempCompressedArchive[0], 'wb') as tempArchive:
|
||||
with requests.get(downloadUrl, stream=True) as fileStream:
|
||||
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
|
||||
tempArchive.write(chunk)
|
||||
tempPath = Path(sys.argv[1])
|
||||
softwarePath = Path(sys.argv[2])
|
||||
|
||||
with py7zr.SevenZipFile(tempPath, 'r') as archive:
|
||||
archive.extractall(path=softwarePath)
|
||||
time.sleep(10) # Wait for a few seconds for the main application to close.
|
||||
|
||||
tempPath.unlink(missing_ok=True)
|
||||
input("\nPlease make sure that all LinkScope Client processes are closed, "
|
||||
"and then press Enter to begin the update.\n")
|
||||
print("Updating...\n")
|
||||
|
||||
with py7zr.SevenZipFile(tempPath, 'r') as archive:
|
||||
archive.extractall(path=softwarePath.parent)
|
||||
|
||||
tempPath.unlink(missing_ok=True)
|
||||
|
||||
input("Update complete. Press Enter to exit.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
input(f"Update failed. Press Enter to exit. Reason: {repr(e)}")
|
||||
|
||||
Reference in New Issue
Block a user