-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When I confirm the update installation after restarting the application, I get a console window with the error The batch file cannot be found. #5
Comments
@its-monotype Could you provide some more information regarding your system, and could you describe steps to reproduce the issue? |
@dennisvang I have Windows 11 21H2 (OS Build 22000.1042), VSCode, PowerShell
|
Instead of moving the Furthermore, as mentioned in the readme, the example app must be extracted to (and run from) the directory specified in If you want to use a different directory, you should modify |
Okay, thanks, I'll try that |
@its-monotype After the update attempt, there should be a file called I guess this error is probably caused by the installation batch file trying to delete itself at the very end, after the installation is done. Also see this discussion. The batch file is a temporary file (with We haven't encountered this issue on any of our test systems, but they all run windows 10. |
Strangely, when I started using pyinstaller with the --onefile attribute, this problem disappeared. I also want to know if it is possible to somehow make the program restart automatically after a successful update, so you don't have to do it manually? |
@its-monotype That's interesting. I'll have a look at that.
Restarting automatically should be possible, but it is not supported out-of-the-box. This, too, is a matter of limiting the maintenance burden. Although I haven't tried this, you could probably do it by adding a windows start call to the end of the installation batch file (defined in the WIN_MOVE_FILES_BAT variable). For cases like this, For example: def install_update_restart(src_dir, dst_dir, purge_dst_dir=False, exclude_from_purge=None, **kwargs):
# Custom install function with restart functionality
... and then ...
client.download_and_apply_update(..., install=install_update_restart, ...)
... A quicker alternative would be to monkey patch the WIN_MOVE_FILES_BAT variable and append the start command that way. See e.g. unittest.mock.patch. |
@dennisvang Сan you please check out my implementation of the automatic restart after an update? It works but I want to know your opinion. + # tufup-example\src\myapp\__init__.py
import logging
+ import pathlib
import shutil
+ import subprocess
+ import sys
+ from tempfile import NamedTemporaryFile
import time
+ from typing import List, Optional, Union
from tufup.client import Client
+ from tufup.utils.platform_specific import (
+ ON_WINDOWS,
+ ON_MAC,
+ WIN_ROBOCOPY_OVERWRITE,
+ WIN_LOG_LINES,
+ WIN_ROBOCOPY_PURGE,
+ WIN_ROBOCOPY_EXCLUDE_FROM_PURGE,
+ run_bat_as_admin,
+ _install_update_mac,
+ )
from myapp import settings
logger = logging.getLogger(__name__)
__version__ = settings.APP_VERSION
def progress_hook(bytes_downloaded: int, bytes_expected: int):
progress_percent = bytes_downloaded / bytes_expected * 100
print(f"\r{progress_percent:.1f}%", end="")
time.sleep(0.5) # quick and dirty: simulate slow or large download
if progress_percent >= 100:
print("")
+ # https://stackoverflow.com/a/20333575
+ WIN_MOVE_FILES_BAT = """@echo off
+ {log_lines}
+ echo Moving app files...
+ robocopy "{src}" "{dst}" {options}
+ echo Done.
+ echo Starting app...
+ start {exe_path}
+ rem Delete self
+ (goto) 2>nul & del "%~f0"
+ """
+ def _install_update_win(
+ src_dir: Union[pathlib.Path, str],
+ dst_dir: Union[pathlib.Path, str],
+ purge_dst_dir: bool,
+ exclude_from_purge: List[Union[pathlib.Path, str]],
+ as_admin: bool = False,
+ log_file_name: Optional[str] = None,
+ robocopy_options_override: Optional[List[str]] = None,
+ ):
+ """
+ Create a batch script that moves files from src to dst, then run the
+ script in a new console, and exit the current process.
+ The script is created in a default temporary directory, and deletes
+ itself when done.
+ The `as_admin` options allows installation as admin (opens UAC dialog).
+ The `debug` option will log the output of the install script to a file in
+ the dst_dir.
+ Options for [robocopy][1] can be overridden completely by passing a list
+ of option strings to `robocopy_options_override`. This will cause the
+ purge arguments to be ignored as well.
+ [1]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy
+ """
+ if robocopy_options_override is None:
+ options = list(WIN_ROBOCOPY_OVERWRITE)
+ if purge_dst_dir:
+ options.append(WIN_ROBOCOPY_PURGE)
+ if exclude_from_purge:
+ options.append(WIN_ROBOCOPY_EXCLUDE_FROM_PURGE)
+ options.extend(exclude_from_purge)
+ else:
+ # empty list [] simply clears all options
+ options = robocopy_options_override
+ options_str = " ".join(options)
+ log_lines = ""
+ if log_file_name:
+ log_file_path = pathlib.Path(dst_dir) / log_file_name
+ log_lines = WIN_LOG_LINES.format(log_file_path=log_file_path)
+ logger.info(f"logging install script output to {log_file_path}")
+ script_content = WIN_MOVE_FILES_BAT.format(
+ src=src_dir,
+ dst=dst_dir,
+ options=options_str,
+ log_lines=log_lines,
+ exe_path=dst_dir.joinpath(
+ settings.EXE_PATH.name
+ ), # settings.EXE_PATH=pathlib.Path(sys.executable).resolve()
+ )
+ logger.debug(f"writing windows batch script:\n{script_content}")
+ with NamedTemporaryFile(
+ mode="w", prefix="tufup", suffix=".bat", delete=False
+ ) as temp_file:
+ temp_file.write(script_content)
+ logger.debug(f"temporary batch script created: {temp_file.name}")
+ script_path = pathlib.Path(temp_file.name).resolve()
+ logger.debug(f"starting script in new console: {script_path}")
+ # start the script in a separate process, non-blocking
+ if as_admin:
+ run_bat_as_admin(file_path=script_path)
+ else:
+ # we use Popen() instead of run(), because the latter blocks execution
+ subprocess.Popen([script_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
+ logger.debug("exiting")
+ sys.exit(0)
+ def install_update_restart(
+ src_dir, dst_dir, purge_dst_dir=False, exclude_from_purge=None, **kwargs
+ ):
+ # Custom install function with restart functionality
+ """
+ Installs update files using platform specific installation script. The
+ actual installation script copies the files and folders from `src_dir` to
+ `dst_dir`.
+ If `purge_dst_dir` is `True`, *ALL* files and folders are deleted from
+ `dst_dir` before copying.
+ **DANGER**:
+ ONLY use `purge_dst_dir=True` if your app is properly installed in its
+ own *separate* directory, such as %PROGRAMFILES%\MyApp.
+ DO NOT use `purge_dst_dir=True` if your app executable is running
+ directly from a folder that also contains unrelated files or folders,
+ such as the Desktop folder or the Downloads folder, because this
+ unrelated content would be then also be deleted.
+ Individual files and folders can be excluded from purge using e.g.
+ exclude_from_purge=['path\\to\\file1', r'"path to\file2"', ...]
+ If `purge_dst_dir` is `False`, the `exclude_from_purge` argument is
+ ignored.
+ """
+ if ON_WINDOWS:
+ _install_update = _install_update_win
+ elif ON_MAC:
+ _install_update = _install_update_mac
+ else:
+ raise RuntimeError("This platform is not supported.")
+ return _install_update(
+ src_dir=src_dir,
+ dst_dir=dst_dir,
+ purge_dst_dir=purge_dst_dir,
+ exclude_from_purge=exclude_from_purge,
+ **kwargs,
+ )
def update(pre: str):
# Create update client
client = Client(
app_name=settings.APP_NAME,
app_install_dir=settings.INSTALL_DIR,
current_version=settings.APP_VERSION,
metadata_dir=settings.METADATA_DIR,
metadata_base_url=settings.METADATA_BASE_URL,
target_dir=settings.TARGET_DIR,
target_base_url=settings.TARGET_BASE_URL,
refresh_required=False,
)
# Perform update
if client.check_for_updates(pre=pre):
client.download_and_apply_update(
# WARNING: Be very careful with purge_dst_dir=True, because this
# will delete *EVERYTHING* inside the app_install_dir, except
# paths specified in exclude_from_purge. So, only use
# purge_dst_dir=True if you are certain that your app_install_dir
# does not contain any unrelated content.
progress_hook=progress_hook,
purge_dst_dir=False,
exclude_from_purge=None,
log_file_name="install.log",
+ install=install_update_restart,
)
def main(cmd_args):
# extract options from command line args
pre_release_channel = cmd_args[0] if cmd_args else None # 'a', 'b', or 'rc'
# The app must ensure dirs exist
for dir_path in [settings.INSTALL_DIR, settings.METADATA_DIR, settings.TARGET_DIR]:
dir_path.mkdir(exist_ok=True, parents=True)
# The app must be shipped with a trusted "root.json" metadata file,
# which is created using the tufup.repo tools. The app must ensure
# this file can be found in the specified metadata_dir. The root metadata
# file lists all trusted keys and TUF roles.
if not settings.TRUSTED_ROOT_DST.exists():
shutil.copy(src=settings.TRUSTED_ROOT_SRC, dst=settings.TRUSTED_ROOT_DST)
logger.info("Trusted root metadata copied to cache.")
# Download and apply any available updates
update(pre=pre_release_channel)
# Do what the app is supposed to do
print(f"Starting {settings.APP_NAME} {settings.APP_VERSION}...")
...
print("Doing what the app is supposed to do...")
+ time.sleep(10)
...
print("Done.") |
Or maybe even better call exe_path=dst_dir.joinpath(settings.EXE_PATH.name) # settings.EXE_PATH=pathlib.Path(sys.executable).resolve() |
@its-monotype could you edit your code above, to show only the lines that were changed (plus a little context)? It's a bit difficult for me to distinguish without copying and doing a diff. |
@dennisvang I changed the code above so you can see what has been added. In short, I just created a custom |
@its-monotype Thanks, now I see what you're trying to do. You're on the right track, but your code can be simplified considerably: If you're only running on windows, you can remove your current Now, as you probably know exactly what you're going to need, you can strip unnecessary stuff from this function. To give you an idea:
|
@dennisvang Thank you very much for your advice 😊 |
@its-monotype You're welcome. I realize this workaround is a bit cumbersome, so I'm going to try to make this easier, see issue linked above. |
@its-monotype By the way, I see you use To prevent issues with whitespace in paths, I would enclose the variable in double quotes like this:
EDIT: Sorry, that probably won't work because |
@its-monotype Sorry, see edit above. I guess that should be something like:
otherwise it will simply open a command window with the path as title, instead of actually running the executable. Or you could do something like this, but I'm not sure if that's overly complicated:
Also see this for some examples. |
@its-monotype A new release is now available: 0.4.4 This makes it possible to specify a custom batch script or batch template. Your example code above would now reduce to this: ...
CUSTOM_BATCH_TEMPLATE = """@echo off
{log_lines}
echo Moving app files...
robocopy "{src_dir}" "{dst_dir}" {robocopy_options}
echo Done.
echo Starting app...
start "" "{exe_path}"
{delete_self}
"""
NEW_EXE_PATH = settings.INSTALL_DIR / settings.EXE_PATH.name
...
client.download_and_apply_update(
progress_hook=progress_hook,
purge_dst_dir=False,
exclude_from_purge=None,
log_file_name='install.log',
batch_template=CUSTOM_BATCH_TEMPLATE,
batch_template_extra_kwargs=dict(exe_path=NEW_EXE_PATH),
)
... |
@dennisvang This is great, thank you so much for adding this feature so quickly, I'm very glad that you are actively developing and improving |
@its-monotype Thanks for the kind words, and thanks for helping us test-drive |
The text was updated successfully, but these errors were encountered: