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:
AccentuSoft
2023-09-30 16:51:39 +03:00
parent 9c6fee0fa6
commit 9c5dd03473
4 changed files with 177 additions and 72 deletions

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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)}")