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:
@@ -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
|
||||
|
||||
158
plugins/bot.py
158
plugins/bot.py
@@ -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")
|
||||
|
||||
@@ -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
121
update_script.py
Normal 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)
|
||||
Reference in New Issue
Block a user