跳到主要内容

教程:OpenBayes 容器延时关闭工具

这是一个用于设置 OpenBayes 容器延时关闭的工具。当你需要在特定时间后自动停止容器时特别有用,比如在长时间训练任务完成后自动关闭容器。

主要功能

  • 设置容器在指定时间后自动关闭
  • 支持小时、分钟和秒的时间设置
  • 实时显示当前时间和倒计时(每秒更新)
  • 支持随时取消预定的关闭
  • 支持非容器环境下的模拟运行模式

使用方法

推荐使用 tmux 运行此程序,这样可以:

  • 防止 SSH 连接断开导致程序退出
  • 方便随时重新连接查看状态
  • 需要时可以取消预定的关闭

创建或连接到 tmux 会话

要创建一个新的 tmux 会话,使用以下命令:

tmux new -s delayed-shutdown  # 统一会话名称与后续示例一致

执行命令

# 代码实际使用 delayed_stop.py 而非 delayed_close.py
python delayed_stop.py 2h # 30m示例保持单位一致性
python delayed_stop.py 30m # 原文档使用30min不符合代码要求
python delayed_stop.py 45s

输出示例

运行脚本后,你将看到如下输出:

Scheduled shutdown details:
Shutdown scheduled at: 2024-03-14 17:30:00 (UTC+8)

Press Ctrl+C to cancel.

Current time: 2024-03-14 15:30:00 (UTC+8)
Time until shutdown: 2 hours
Press Ctrl+C to cancel.

显示会实时更新:

  • 当前时间每秒更新一次
  • 剩余时间每秒更新一次
  • 取消提示始终显示
  • 使用动态刷新方式,保持屏幕整洁

注意事项

  • 无需安装额外依赖
  • 时间值必须是整数(例如:2h 有效,但 2.5h 无效)
  • 每次只能指定一个时间单位
  • 在容器环境外运行时会进入模拟模式,不会真正执行关闭操作
  • 重要:直接在 SSH 中运行程序时,如果连接断开,程序会退出,预定的关闭任务将不会执行
  • 建议:使用 tmux 运行程序,这样即使 SSH 连接断开,程序也会继续在后台运行

tmux 常用操作

  • tmux new -s delayed-shutdown: 创建新的会话
  • tmux attach -t delayed-shutdown: 连接到已有会话
  • Ctrl+B 然后 D: 分离当前会话(程序继续在后台运行)
  • Ctrl+B 然后 [: 进入滚动模式查看历史输出
  • tmux ls: 列出所有会话
  • tmux kill-session -t delayed-shutdown: 结束会话(会终止正在运行的程序)

附件

delayed_stop.py 代码

import time
import os
import sys
import datetime
import signal
import subprocess
from datetime import datetime, timedelta
import re

def upgrade_bayes_cli():
"""Upgrade bayes CLI to the latest version"""
try:
print("Upgrading bayes CLI...")
subprocess.run(["bayes", "upgrade"], check=True)
print("Upgrade completed.")
except subprocess.CalledProcessError:
print("Warning: Failed to upgrade bayes CLI. Continuing with current version.")
except FileNotFoundError:
print("Warning: bayes CLI not found. Please make sure it's installed.")

def parse_time(delay):
"""Parse time format, e.g., 2h, 30m, 45s"""
# Define valid time units
units = {
"h": 3600, # hours
"m": 60, # minutes
"s": 1 # seconds
}

# Validate format using regex
pattern = r'^\d+[hms]$'
if not re.match(pattern, delay):
print(f"Error: Invalid time format '{delay}'")
print("Valid formats: <number>[h|m|s] (e.g., 2h, 30m, 45s)")
sys.exit(1)

# Extract number and unit
number = int(delay[:-1])
unit = delay[-1]

if unit not in units:
print(f"Error: Invalid time unit '{unit}'")
print("Valid units: h (hours), m (minutes), s (seconds)")
sys.exit(1)

return number * units[unit]

def format_time(timestamp):
"""Format timestamp to local time string (UTC+8)"""
utc_time = datetime.utcfromtimestamp(timestamp)
local_time = utc_time + timedelta(hours=8)
return local_time.strftime('%Y-%m-%d %H:%M:%S (UTC+8)')

def shutdown():
"""Execute shutdown command"""
print("\nStopping Service...")
if OPENBAYES_JOB_ID:
subprocess.run(["bayes", "gear", "stop", OPENBAYES_JOB_ID], check=True)
else:
print("This is a dummy run - no actual shutdown will be performed")

def signal_handler(sig, frame):
"""Capture Ctrl+C to cancel the task"""
print("\nShutdown canceled.")
sys.exit(0)

def format_time_remaining(seconds):
"""Format remaining time in a human-readable way"""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
secs = seconds % 60
parts = []
if hours > 0:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
if secs > 0:
parts.append(f"{secs} second{'s' if secs != 1 else ''}")
return " ".join(parts)

if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: delayed_stop.py <time_delay> (e.g., 2h, 1h, 30min, 45s)")
sys.exit(1)

delay = sys.argv[1]
OPENBAYES_JOB_ID = os.getenv("OPENBAYES_JOB_ID", "")

# Upgrade bayes CLI first
if OPENBAYES_JOB_ID:
upgrade_bayes_cli()

# Get current and shutdown timestamps
current_timestamp = time.time()
wait_seconds = parse_time(delay)
shutdown_timestamp = current_timestamp + wait_seconds

if not OPENBAYES_JOB_ID:
print("Notice: Running in dummy mode - no actual shutdown will be performed")

# Initial display
print(f"\nScheduled shutdown details:")
print(f"Shutdown scheduled at: {format_time(shutdown_timestamp)}")
print("\nPress Ctrl+C to cancel.\n")
print("\033[?25l", end="") # Hide cursor

# Listen for Ctrl+C
signal.signal(signal.SIGINT, signal_handler)

try:
while wait_seconds > 0:
# Clear previous lines and move cursor up
print("\033[2K\033[A" * 3, end="")

# Update current time and remaining time
current_time = time.time()
print(f"Current time: {format_time(current_time)}")
print(f"Time until shutdown: {format_time_remaining(wait_seconds)}")
print("Press Ctrl+C to cancel.")

time.sleep(1)
wait_seconds -= 1

print("\033[?25h", end="") # Show cursor
shutdown()
except KeyboardInterrupt:
print("\033[?25h", end="") # Show cursor
signal_handler(None, None)
except Exception as e:
print("\033[?25h", end="") # Show cursor
raise e