diff --git a/py_installer/Installer.py b/py_installer/Installer.py index 9288af4..c6f405b 100644 --- a/py_installer/Installer.py +++ b/py_installer/Installer.py @@ -114,6 +114,9 @@ def _RunInternal(self): uninstall.DoUninstall(context) return + # Since this runs an async thread, kick it off now so it can start working. + ZStandard.TryToInstallZStandardAsync(context) + # Next step is to discover and fill out the moonraker config file path and service file name. # If we are doing an companion or bambu setup, we need the user to help us input the details to the external moonraker IP or bambu printer. # This is the hardest part of the setup, because it's highly dependent on the system and different moonraker setups. @@ -145,9 +148,6 @@ def _RunInternal(self): # Installing ffmpeg is best effort and not required for the plugin to work. Ffmpeg.TryToInstallFfmpeg(context) - # We also want to try to install the optional zstandard lib for compression. - ZStandard.TryToInstallZStandard(context) - # Before we start the service, check if the secrets config file already exists and if a printer id already exists. # This will indicate if this is a fresh install or not. context.ExistingPrinterId = Linker.GetPrinterIdFromServiceSecretsConfigFile(context) @@ -158,6 +158,10 @@ def _RunInternal(self): # Just before we start (or restart) the service, ensure all of the permission are set correctly permissions.EnsureFinalPermissions(context) + # If there was an install running, wait for it to finish now, before the service starts. + # For most installs, the user will take longer to add the info than it takes to install zstandard. + ZStandard.WaitForInstallToComplete() + # We are fully configured, create the service file and it's dependent files. service = Service() service.Install(context) diff --git a/py_installer/Updater.py b/py_installer/Updater.py index c7aa27b..1626e3e 100644 --- a/py_installer/Updater.py +++ b/py_installer/Updater.py @@ -31,6 +31,9 @@ class Updater: def DoUpdate(self, context:Context): Logger.Header("Starting Update Logic") + # Since this takes a while, kick it off now. The pip install can take upwards of 30 seconds. + ZStandard.TryToInstallZStandardAsync(context) + # Enumerate all service file to find any local plugins, Sonic Pad plugins, companion service files, and bambu service files, since all service files contain this name. # Note GetServiceFileFolderPath will return dynamically based on the OsType detected. # Use sorted, so the results are in a nice user presentable order. @@ -49,8 +52,9 @@ def DoUpdate(self, context:Context): # On any system, try to install or update ffmpeg. Ffmpeg.TryToInstallFfmpeg(context) - # We also want to try to install or update the optional zstandard lib for compression. - ZStandard.TryToInstallZStandard(context) + # Before we restart the plugins, wait for the zstandard install to be done. + # Give the updater extra time to work, since it's much shorter + ZStandard.WaitForInstallToComplete(timeoutSec==20.0) Logger.Info("We found the following plugins to update:") for s in foundOeServices: diff --git a/py_installer/ZStandard.py b/py_installer/ZStandard.py index 092f5de..f7d4451 100644 --- a/py_installer/ZStandard.py +++ b/py_installer/ZStandard.py @@ -1,6 +1,7 @@ import sys import time import subprocess +import threading import multiprocessing from octoeverywhere.compression import Compression @@ -12,10 +13,13 @@ # A helper class to make sure the optional zstandard lib and deps are installed. class ZStandard: + # If there's an installer thread, it will be stored here. + _InstallThread = None + # Tries to install zstandard, but this won't fail if the install fails. + # The PIP install can take quite a long time (20-30 seconds) so we run in async. @staticmethod - def TryToInstallZStandard(context:Context): - + def TryToInstallZStandardAsync(context:Context) -> None: # We don't even try installing on K1 or SonicPad, we know it fail. if context.OsType == OsTypes.K1 or context.OsType == OsTypes.SonicPad: return @@ -25,21 +29,50 @@ def TryToInstallZStandard(context:Context): if multiprocessing.cpu_count() < Compression.ZStandardMinCoreCountForInstall: return - # Try to install or upgrade. - Logger.Info("Installing zstandard, this might take a moment...") - startSec = time.time() - (returnCode, stdOut, stdError) = Util.RunShellCommand("sudo apt-get install zstd -y", False) - Logger.Debug(f"Zstandard apt install result. Code: {returnCode}, StdOut: {stdOut}, StdErr: {stdError}") + # Since the pip install can take a long time, do the install process async. + ZStandard._InstallThread = threading.Thread(target=ZStandard._InstallThread, daemon=True) + ZStandard._InstallThread.start() - # Use the same logic as we do in the Compression class. - result = subprocess.run([sys.executable, '-m', 'pip', 'install', Compression.ZStandardPipPackageString], timeout=60.0, check=False, capture_output=True) - Logger.Debug(f"Zstandard PIP install result. Code: {result.returncode}, StdOut: {result.stdout}, StdErr: {result.stderr}") - # Report the status to the installer log. - if result.returncode == 0: - Logger.Info(f"zStandard successfully installed/updated. It took {str(round(time.time()-startSec, 2))} seconds.") + @staticmethod + def WaitForInstallToComplete(timeoutSec:float=10.0) -> None: + # See if we started a thread. + t = ZStandard._InstallThread + if t is None: return - # Tell the user, but this is a best effort, so if it fails we don't care. - # Any user who wants to use RTSP and doesn't have ffmpeg installed can use our help docs to install it. - Logger.Info(f"We didn't install zstandard. It took {str(round(time.time()-startSec, 2))} seconds. Output: {result.stderr}") + # If we did, report and try to join it. + # If this fails, it's no big deal, because the plugin runtime will also try to install zstandard. + Logger.Info("Finishing install... this might take a moment...") + try: + t.join(timeout=timeoutSec) + except Exception as e: + Logger.Debug(f"Failed to join ztd installer thread. {str(e)}") + + + @staticmethod + def _InstallThread() -> None: + try: + # Try to install the system package, if possible. This might bring in a binary. + # If this fails, the PY package might be able to still bring in a pre-built binary. + Logger.Debug("Installing zstandard, this might take a moment...") + startSec = time.time() + (returnCode, stdOut, stdError) = Util.RunShellCommand("sudo apt-get install zstd -y", False) + Logger.Debug(f"Zstandard apt install result. Code: {returnCode}, StdOut: {stdOut}, StdErr: {stdError}") + + # Now try to install the PY package. + # NOTE: Use the same logic as we do in the Compression class. + # Only allow blocking up to 20 seconds, so we don't hang the installer too long. + result = subprocess.run([sys.executable, '-m', 'pip', 'install', Compression.ZStandardPipPackageString], timeout=30.0, check=False, capture_output=True) + Logger.Debug(f"Zstandard PIP install result. Code: {result.returncode}, StdOut: {result.stdout}, StdErr: {result.stderr}") + + # Report the status to the installer log. + if result.returncode == 0: + Logger.Debug(f"zStandard successfully installed/updated. It took {str(round(time.time()-startSec, 2))} seconds.") + return + + # Tell the user, but this is a best effort, so if it fails we don't care. + # Any user who wants to use RTSP and doesn't have ffmpeg installed can use our help docs to install it. + Logger.Debug(f"We didn't install zstandard. It took {str(round(time.time()-startSec, 2))} seconds. Output: {result.stderr}") + except Exception as e: + Logger.Debug(f"Error installing zstandard. {str(e)}")