Skip to content

Commit

Permalink
Adding logic to allow the zstandard install to be async.
Browse files Browse the repository at this point in the history
  • Loading branch information
QuinnDamerell committed Jun 19, 2024
1 parent afa0166 commit 2dfe596
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 21 deletions.
10 changes: 7 additions & 3 deletions py_installer/Installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions py_installer/Updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
65 changes: 49 additions & 16 deletions py_installer/ZStandard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import time
import subprocess
import threading
import multiprocessing

from octoeverywhere.compression import Compression
Expand All @@ -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
Expand All @@ -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)}")

0 comments on commit 2dfe596

Please sign in to comment.