Skip to content

Commit

Permalink
Fix bugs & various optimizations:
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
AccentuSoft committed Oct 24, 2023
1 parent 9c6fee0 commit 9c5dd03
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 72 deletions.
91 changes: 60 additions & 31 deletions Core/ModuleManager.py
@@ -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
Expand All @@ -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
Expand All @@ -50,7 +50,8 @@ def __init__(self, mainWindow):
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.')

Expand All @@ -70,26 +71,42 @@ def loadYamlFile(self, filePath: Path):
# {} == 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')

sys.path.append(packagesPath)
site.addsitedir(packagesPath)

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.prefix = str(self.modulesBaseDirectoryPath)

sys.real_prefix = sys.prefix
sys.prefix = base
def configureVenv(self):
if self.system == "Linux":
self.configureVenvLinux()
else:
self.configureVenvWindows()

self.loadAllModules()

Expand Down Expand Up @@ -776,18 +793,30 @@ def accept(self) -> None:


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

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(venvPath)
self.configureVenvOfMainThreadSignal.emit()


class UpgradeVenvThread(QtCore.QThread):
Expand All @@ -808,14 +837,14 @@ def run(self) -> None:
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)
Expand Down Expand Up @@ -859,8 +888,8 @@ def run(self) -> None:
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)

Expand Down Expand Up @@ -920,7 +949,7 @@ def run(self) -> None:
[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,
Expand Down
110 changes: 89 additions & 21 deletions Core/UpdateManager.py
Expand Up @@ -4,7 +4,10 @@
import platform
import subprocess
import requests
import tempfile
import shutil
import os
import ctypes

from pathlib import Path
from semver import compare
Expand Down Expand Up @@ -40,8 +43,9 @@ def getLatestVersion(self):
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:
Expand All @@ -54,11 +58,22 @@ def doUpdate(self) -> None:
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.')

Expand Down Expand Up @@ -99,27 +114,80 @@ def initiateUpdate(self) -> None:


class UpdaterThread(QtCore.QThread):
updateDoneSignal = QtCore.Signal(bool)
updateDoneSignal = QtCore.Signal(str)

def __init__(self, updateManager, mainWindow):
super().__init__()
self.mainWindow = mainWindow
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,
)
5 changes: 3 additions & 2 deletions LinkScope.py
Expand Up @@ -2,6 +2,7 @@

# Load modules
import contextlib
import platform
import re
import sys
import time
Expand Down Expand Up @@ -1085,9 +1086,9 @@ def getPlaywrightBrowserPath(self, browser: str = None) -> Path:
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
Expand Down
43 changes: 25 additions & 18 deletions UpdaterUtil.py
@@ -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


def main():
if len(sys.argv) != 3:
sys.exit(3)

tempPath = Path(sys.argv[1])
softwarePath = Path(sys.argv[2])

time.sleep(10) # Wait for a few seconds for the main application to close.

input("\nPlease make sure that all LinkScope Client processes are closed, "
"and then press Enter to begin the update.\n")
print("Updating...\n")

downloadUrl = sys.argv[1]
softwarePath = sys.argv[2]
with py7zr.SevenZipFile(tempPath, 'r') as archive:
archive.extractall(path=softwarePath.parent)

clientTempCompressedArchive = tempfile.mkstemp(suffix='.7z')
tempPath = Path(clientTempCompressedArchive[1])
tempPath.unlink(missing_ok=True)

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)
input("Update complete. Press Enter to exit.")

with py7zr.SevenZipFile(tempPath, 'r') as archive:
archive.extractall(path=softwarePath)

tempPath.unlink(missing_ok=True)
if __name__ == '__main__':
try:
main()
except Exception as e:
input(f"Update failed. Press Enter to exit. Reason: {repr(e)}")

0 comments on commit 9c5dd03

Please sign in to comment.