feat(update): Implement external script-based update system

- Created update_script.py for reliable external updates
- Modified update command to use external script approach
- Bot now shuts down, launches update script, and restarts automatically
- Added proper repository handling and dependency updates
- Improved update UI with better status messages and buttons
- Fixed issues with in-process updates that could cause corruption

This approach is much more reliable as it:
- Avoids updating files while they're in use
- Properly handles git operations without conflicts
- Ensures clean restart after updates
- Provides better error handling and user feedback
This commit is contained in:
Cursor User
2025-06-18 17:06:30 +02:00
parent 156d79ff32
commit 7b1d54a866
4 changed files with 269 additions and 77 deletions

View File

@@ -5,30 +5,21 @@
# PLease read the GNU Affero General Public License in
# <https://www.github.com/TeamUltroid/Ultroid/blob/main/LICENSE/>.
from . import get_help
__doc__ = get_help("help_afk")
import asyncio
from telethon import events
from pyUltroid.dB.afk_db import add_afk, del_afk, is_afk
from pyUltroid.dB.base import KeyManager
from pyUltroid import asst, ultroid_bot
from pyUltroid.config import LOG_CHANNEL, NOSPAM_CHAT
from pyUltroid.db.afk_db import add_afk, del_afk, is_afk
from pyUltroid.db.base import KeyManager
from pyUltroid.fns.decorators import ultroid_cmd
from pyUltroid.fns.helper import get_help, get_string
from pyUltroid.fns.tools import mediainfo, upload_file
from pyUltroid.udB import udB
__doc__ = get_help("help_afk")
from . import (
LOG_CHANNEL,
NOSPAM_CHAT,
Redis,
asst,
get_string,
mediainfo,
udB,
ultroid_bot,
ultroid_cmd,
upload_file
)
old_afk_msg = []
@@ -93,7 +84,11 @@ async def set_afk(event):
async def remove_afk(event):
if event.is_private and udB.get_key("PMSETTING") and not is_approved(event.chat_id):
if (
event.is_private
and udB.get_key("PMSETTING")
and not is_approved(event.chat_id)
):
return
elif "afk" in event.text.lower():
return
@@ -114,7 +109,11 @@ async def remove_afk(event):
async def on_afk(event):
if event.is_private and Redis("PMSETTING") and not is_approved(event.chat_id):
if (
event.is_private
and udB.get_key("PMSETTING")
and not is_approved(event.chat_id)
):
return
elif "afk" in event.text.lower():
return

View File

@@ -13,6 +13,7 @@ import os
import re
import sys
import time
import asyncio
from platform import python_version as pyver
from random import choice
@@ -378,6 +379,29 @@ async def get_updates(ulttext, repo_url):
return repo, True, changelog
def launch_update_script(repo_url=None):
"""Launch the external update script and shutdown the bot."""
import subprocess
import sys
script_path = "update_script.py"
# Prepare command arguments
cmd = [sys.executable, script_path]
if repo_url:
cmd.append(repo_url)
# Add original start arguments so the script knows how to restart
if len(sys.argv) > 1:
cmd.extend(sys.argv[1:])
# Launch the update script
subprocess.Popen(cmd, cwd=os.getcwd())
# Shutdown the bot
os._exit(0)
@ultroid_cmd(
pattern="update(.*)",
command="update",
@@ -389,12 +413,12 @@ async def updater(event):
Description: Checks for updates for your userbot.
• `{tr}update`: Checks for updates from your forked repo (if set), otherwise from original.
• `{tr}update now`: Forces an update from the configured repo.
• `{tr}update now`: Forces an immediate update using external script from the configured repo.
• `{tr}update original`: Checks for updates from the official Ultroid repo.
• `{tr}update now original`: Forces an update from the official Ultroid repo.
• `{tr}update now original`: Forces an immediate update from the official Ultroid repo.
Note: Use `{tr}setrepo <your_fork_url>` to update from your own fork."""
if Var.HEROKU_API and Var.HEROKU_APP_NAME:
if Var.HEROKU_API:
return await event.eor(
"Heroku user! Please update from Heroku dashboard.",
)
@@ -411,10 +435,24 @@ Note: Use `{tr}setrepo <your_fork_url>` to update from your own fork."""
or "https://github.com/ThePrateekBhatia/Ultroid"
)
off_repo, is_new, changelog = await get_updates(
ulttext,
repo_url=repo_url,
)
if is_now:
# Use external script for immediate update
await ulttext.edit(
"🔄 **Starting update process...**\n\n"
f"📦 Repository: `{repo_url}`\n"
"⚡ Using external script for reliable update\n\n"
"🤖 Bot will shutdown and restart automatically after update completes."
)
# Wait a moment for the message to be sent
await asyncio.sleep(2)
# Launch external update script and shutdown
launch_update_script(repo_url)
return
# Regular update check (non-destructive)
off_repo, is_new, changelog = await get_updates(ulttext, repo_url=repo_url)
if not off_repo:
return
@@ -422,41 +460,38 @@ Note: Use `{tr}setrepo <your_fork_url>` to update from your own fork."""
branch = off_repo.active_branch.name
if is_new:
if is_now:
await ulttext.edit("`Force updating...`")
try:
await bash(f"git config remote.upstream.url {repo_url} && git pull -f upstream {branch}")
await bash("pip3 install -r requirements.txt --break-system-packages")
call_back()
await ulttext.edit("`Update successful! Restarting...`")
os.execl(sys.executable, sys.executable, "-m", "pyUltroid")
except Exception as e:
await ulttext.edit(f"**Update failed!**\n\n**Error:**\n`{e}`")
finally:
try:
off_repo.delete_remote("upstream")
except Exception:
pass
return
# Show update available with options
buttons = [
[
Button.inline("🔄 Update Now", data=f"update_now|{repo_url}"),
Button.inline("📋 View Changes", data=f"update_changelog|{repo_url}"),
],
[Button.inline("❌ Dismiss", data="close_update")],
]
m = await asst.send_message(
udB.get_key("LOG_CHANNEL"),
changelog,
buttons=[
Button.inline("Update Now", data=f"update_now|{repo_url}"),
Button.inline("Dismiss", data="close_update"),
],
f"🆕 **Update Available!**\n\n"
f"📦 Repository: `{repo_url}`\n"
f"🌿 Branch: `{branch}`\n\n"
f"Use the buttons below to update or view changes.",
buttons=buttons,
)
Link = m.message_link
await ulttext.edit(
f'**Update available!**\n\nView changelog and update from your log channel.\n\n[View Changelog]({Link})',
f'**🆕 Update available!**\n\n'
f'📦 Repository: `{repo_url.replace(".git", "")}`\n'
f'🌿 Branch: `{branch}`\n\n'
f'[📋 View Options & Update]({Link})',
parse_mode="md",
link_preview=False,
)
else:
await ulttext.edit(
f'<code>Your BOT is </code><strong>up-to-date</strong><code> with </code><strong><a href="{repo_url.replace(".git", "")}/tree/{branch}">[{branch}]</a></strong>.',
parse_mode="html",
f'✅ **Your bot is up-to-date!**\n\n'
f'📦 Repository: `{repo_url.replace(".git", "")}`\n'
f'🌿 Branch: `{branch}`',
parse_mode="md",
link_preview=False,
)
@@ -469,23 +504,56 @@ Note: Use `{tr}setrepo <your_fork_url>` to update from your own fork."""
@callback(re.compile(b"update_now\\|(.*)"))
async def update_now_callback(event):
repo_url = event.data_match.group(1).decode("utf-8")
await event.edit("`Updating now...`")
await event.edit(
"🔄 **Starting update process...**\n\n"
f"📦 Repository: `{repo_url}`\n"
"⚡ Using external script for reliable update\n\n"
"🤖 Bot will shutdown and restart automatically after update completes."
)
# Wait a moment for the message to be sent
await asyncio.sleep(2)
# Launch external update script and shutdown
launch_update_script(repo_url)
@callback(re.compile(b"update_changelog\\|(.*)"))
async def update_changelog_callback(event):
repo_url = event.data_match.group(1).decode("utf-8")
# Get changelog
try:
repo = Repo()
branch = repo.active_branch.name
await bash(f"git config remote.upstream.url {repo_url} || git remote add upstream {repo_url}")
await bash(f"git pull -f upstream {branch}")
await bash("pip3 install -r requirements.txt --break-system-packages")
call_back()
await event.edit("`Update successful! Restarting...`")
os.execl(sys.executable, sys.executable, "-m", "pyUltroid")
except Exception as e:
await event.edit(f"**Update failed!**\n\n**Error:**\n`{e}`")
finally:
# Set up upstream remote
try:
repo.delete_remote("upstream")
except Exception:
pass
upstream_remote = repo.remote("upstream")
upstream_remote.set_url(repo_url)
except ValueError:
upstream_remote = repo.create_remote("upstream", repo_url)
# Fetch updates
upstream_remote.fetch(branch)
# Generate changelog
changelog = f"**📋 Changelog for [{branch}]({repo_url.replace('.git', '')}/tree/{branch})**\n\n"
for commit in repo.iter_commits(f'{branch}..upstream/{branch}'):
changelog += f"• `{commit.summary}` by __{commit.author.name}__\n"
# Cleanup
repo.delete_remote("upstream")
await event.edit(
changelog,
buttons=[
[Button.inline("🔄 Update Now", data=f"update_now|{repo_url}")],
[Button.inline("❌ Close", data="close_update")],
],
)
except Exception as e:
await event.edit(f"**Error getting changelog:**\n`{e}`")
@callback("close_update")

View File

@@ -227,13 +227,17 @@ if run_as_module:
f"{sys.executable} -m pip install --no-cache-dir -r requirements.txt"
)
@run_async
def gen_chlog(repo, diff):
def gen_chlog(repo, diff, repo_url=None):
"""Generate Changelogs..."""
from .. import udB
UPSTREAM_REPO_URL = (
udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
).replace(".git", "")
if not repo_url:
UPSTREAM_REPO_URL = (
udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
).replace(".git", "")
else:
UPSTREAM_REPO_URL = repo_url.replace(".git", "")
ac_br = repo.active_branch.name
ch_log = tldr_log = ""
ch = f"<b>Ultroid {ultroid_version} updates for <a href={UPSTREAM_REPO_URL}/tree/{ac_br}>[{ac_br}]</a>:</b>"
@@ -271,7 +275,7 @@ async def bash(cmd, run_code=0):
# Will add in class
async def updater():
async def updater(repo_url=None):
from .. import LOGS, udB
if not Repo:
@@ -285,8 +289,7 @@ async def updater():
if isinstance(e, InvalidGitRepositoryError):
repo = Repo.init()
off_repo = (
udB.get_key("UPSTREAM_REPO")
or "https://github.com/ThePrateekBhatia/Ultroid"
repo_url or udB.get_key("UPSTREAM_REPO") or "https://github.com/ThePrateekBhatia/Ultroid"
)
if "upstream" not in repo.remotes:
origin = repo.create_remote("upstream", off_repo)
@@ -299,12 +302,13 @@ async def updater():
ac_br = repo.active_branch.name
off_repo = udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
if not repo_url:
repo_url = udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
if "upstream" not in repo.remotes:
repo.create_remote("upstream", off_repo)
repo.create_remote("upstream", repo_url)
else:
repo.remote("upstream").set_url(off_repo)
repo.remote("upstream").set_url(repo_url)
ups_rem = repo.remote("upstream")
@@ -314,7 +318,7 @@ async def updater():
LOGS.info(f"Failed to fetch from upstream remote: {e}")
return False
changelog, tl_chnglog = await gen_chlog(repo, f"HEAD..upstream/{ac_br}")
changelog, tl_chnglog = await gen_chlog(repo, f"HEAD..upstream/{ac_br}", repo_url)
return bool(changelog)

121
update_script.py Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Ultroid Update Script
This script handles updating the bot while it's not running.
"""
import os
import sys
import subprocess
import time
from pathlib import Path
def run_command(cmd, shell=True):
"""Run a command and return success status."""
try:
result = subprocess.run(cmd, shell=shell, capture_output=True, text=True)
print(f"Command: {cmd}")
print(f"Output: {result.stdout}")
if result.stderr:
print(f"Error: {result.stderr}")
return result.returncode == 0
except Exception as e:
print(f"Error running command '{cmd}': {e}")
return False
def main():
"""Main update function."""
print("🔄 Starting Ultroid update process...")
# Get script directory
script_dir = Path(__file__).parent.absolute()
os.chdir(script_dir)
print(f"📁 Working directory: {script_dir}")
# Check if we're in a git repository
if not (script_dir / ".git").exists():
print("❌ Not a git repository. Cannot update.")
return False
# Get the repository URL from command line args or database
repo_url = sys.argv[1] if len(sys.argv) > 1 else None
# Fetch and pull updates
print("📥 Fetching updates from repository...")
if repo_url:
print(f"🔗 Using repository: {repo_url}")
# Set up remote if needed
if not run_command("git remote get-url origin"):
run_command(f"git remote add origin {repo_url}")
else:
run_command(f"git remote set-url origin {repo_url}")
# Fetch latest changes
if not run_command("git fetch origin"):
print("❌ Failed to fetch updates")
return False
# Get current branch
result = subprocess.run("git branch --show-current", shell=True, capture_output=True, text=True)
current_branch = result.stdout.strip() or "main"
print(f"🌿 Current branch: {current_branch}")
# Pull updates
print("⬇️ Pulling updates...")
if not run_command(f"git pull origin {current_branch}"):
print("❌ Failed to pull updates")
return False
# Update dependencies
print("📦 Installing/updating dependencies...")
if not run_command("pip3 install -r requirements.txt --upgrade"):
print("⚠️ Warning: Failed to update some dependencies")
# Try alternative pip command
run_command("pip3 install -r requirements.txt --break-system-packages --upgrade")
print("✅ Update completed successfully!")
return True
def restart_bot():
"""Restart the bot after update."""
print("🔄 Restarting Ultroid...")
# Check if we have a virtual environment
venv_python = None
if os.path.exists("venv/bin/python"):
venv_python = "venv/bin/python"
elif os.path.exists("venv/Scripts/python.exe"):
venv_python = "venv/Scripts/python.exe"
# Determine how to start the bot
if len(sys.argv) > 1 and sys.argv[-1] == "main.py":
# Started with main.py
if venv_python:
os.execv(venv_python, [venv_python, "main.py"])
else:
os.execv(sys.executable, [sys.executable, "main.py"])
else:
# Started as module
if venv_python:
os.execv(venv_python, [venv_python, "-m", "pyUltroid"])
else:
os.execv(sys.executable, [sys.executable, "-m", "pyUltroid"])
if __name__ == "__main__":
print("🚀 Ultroid Update Script")
print("=" * 40)
# Wait a moment for the bot to fully shutdown
time.sleep(2)
# Perform update
if main():
print("=" * 40)
restart_bot()
else:
print("❌ Update failed. Please check the errors above.")
sys.exit(1)