[update] Replace self without launching a subprocess in windows

Closes: #335, https://github.com/ytdl-org/youtube-dl/issues/28488, https://github.com/ytdl-org/youtube-dl/issues/5810, https://github.com/ytdl-org/youtube-dl/issues/5994

In windows, a running executable cannot be replaced. So, the old updater worked by launching a batch script and then exiting, so that the batch script can replace the executable. However, this caused the above-mentioned issues.

The new method takes advantage of the fact that while the executable cannot be replaced or deleted, it can still be renamed. The current update process on windows is as follows:
1. Delete `yt-dlp.exe.old` if it exists
2. Download the new version as `yt-dlp.exe.new`
3. Rename the running exe to `yt-dlp.exe.old`
4. Rename `yt-dlp.exe.new` to `yt-dlp.exe`
5. Open a shell that deletes `yt-dlp.exe.old` and terminate

While we still use a subprocess, the actual update is already done before the app terminates and the batch script does not print anything to stdout/stderr. So this solves all the above issues
This commit is contained in:
pukkandan 2021-05-26 01:13:34 +05:30
parent c19bc311cb
commit b25522ba52
No known key found for this signature in database
GPG key ID: 0F00D95A001F4698

View file

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib import hashlib
import io
import json import json
import os import os
import platform import platform
@ -147,6 +146,11 @@ def get_sha256sum(bin_or_exe, version):
directory = os.path.dirname(exe) directory = os.path.dirname(exe)
if not os.access(directory, os.W_OK): if not os.access(directory, os.W_OK):
return report_error('no write permissions on %s' % directory, expected=True) return report_error('no write permissions on %s' % directory, expected=True)
try:
if os.path.exists(filename + '.old'):
os.remove(filename + '.old')
except (IOError, OSError):
return report_error('unable to remove the old version')
try: try:
arch = platform.architecture()[0][:2] arch = platform.architecture()[0][:2]
@ -176,22 +180,24 @@ def get_sha256sum(bin_or_exe, version):
return report_error('unable to remove corrupt download') return report_error('unable to remove corrupt download')
try: try:
bat = os.path.join(directory, 'yt-dlp-updater.cmd') os.rename(exe, exe + '.old')
with io.open(bat, 'w') as batfile: except (IOError, OSError):
batfile.write(''' return report_error('unable to move current version')
@( try:
echo.Waiting for file handle to be closed ... os.rename(exe + '.new', exe)
ping 127.0.0.1 -n 5 -w 1000 > NUL
move /Y "%s.new" "%s" > NUL
echo.Updated yt-dlp to version %s
)
@start /b "" cmd /c del "%%~f0"&exit /b
''' % (exe, exe, version_id))
subprocess.Popen([bat]) # Continues to run in the background
except (IOError, OSError): except (IOError, OSError):
report_error('unable to overwrite current version') report_error('unable to overwrite current version')
return True # Exit app os.rename(exe + '.old', exe)
return
try:
# Continues to run in the background
subprocess.Popen(
'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % exe,
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ydl.to_screen('Updated yt-dlp to version %s' % version_id)
return True # Exit app
except OSError:
report_error('unable to delete old version')
# Zip unix package # Zip unix package
elif isinstance(globals().get('__loader__'), zipimporter): elif isinstance(globals().get('__loader__'), zipimporter):