File size: 5,946 Bytes
316aa0c
58c4d77
316aa0c
 
 
58c4d77
9f05494
316aa0c
 
e64e6b4
316aa0c
e64e6b4
 
 
316aa0c
 
 
 
 
 
58c4d77
316aa0c
e64e6b4
 
 
316aa0c
cb618f2
fe46788
e64e6b4
316aa0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e64e6b4
316aa0c
 
 
e64e6b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316aa0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e64e6b4
316aa0c
 
e64e6b4
 
 
316aa0c
 
 
 
 
 
e64e6b4
316aa0c
 
e64e6b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316aa0c
 
58c4d77
e64e6b4
58c4d77
 
 
 
 
 
 
 
e64e6b4
 
 
 
 
 
 
 
58c4d77
 
316aa0c
 
 
 
 
 
 
 
4819687
316aa0c
e64e6b4
 
 
 
 
 
 
 
316aa0c
9f05494
316aa0c
 
4819687
316aa0c
 
 
58c4d77
316aa0c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import os
import time
from time import sleep
from huggingface_hub import HfApi
from datetime import datetime, timedelta
import threading
import gradio as gr

#
# Expects HF_TOKEN, REPOS, and RESTART_TIMES env vars
#
# API endpoints:
# /call/status - Returns the current countdown and target time.
# /call/force_reboot - Immediately triggers the restart of all defined repositories.
#
#
#

HF_TOKEN = os.getenv("HF_TOKEN")
repos_env = os.getenv("REPOS")
times_env = os.getenv("RESTART_TIMES")

# Fallback for local testing to avoid crashes if envs are missing
REPOS = [t.strip() for t in repos_env.split(",")] if repos_env else []
RESTART_TIMES = [t.strip() for t in times_env.split(",")] if times_env else ["00:00"]

HELPER = "lainlives/starter"
SELF_TIME = "00:30"

# Global variable to track the specific target timestamp
next_reboot_datetime = None


def get_next_scheduled_time():
    """Finds the next scheduled time from the list relative to now."""
    now = datetime.now()
    candidates = []

    for t_str in RESTART_TIMES:
        try:
            h, m = map(int, t_str.split(':'))
            dt = now.replace(hour=h, minute=m, second=0, microsecond=0)
            candidates.append(dt)
        except ValueError:
            print(f"Invalid time format: {t_str}")

    candidates.sort()

    for dt in candidates:
        if dt > now:
            return dt

    return candidates[0] + timedelta(days=1)


# Initialize first target
next_reboot_datetime = get_next_scheduled_time()


def perform_restarts(source="Manual"):
    """Core logic to restart the repos."""
    log_messages = []
    if HF_TOKEN:
        api = HfApi()
        for repo in REPOS:
            try:
                print(f"[{source}] Restarting space {repo}...")
                api.pause_space(repo_id=repo)
                sleep(3)
                api.restart_space(repo_id=repo, token=HF_TOKEN, factory_reboot=True)
                log_messages.append(f"Successfully restarted {repo}")
            except Exception as e:
                err = f"Failed to restart {repo}: {e}"
                print(err)
                log_messages.append(err)
    else:
        log_messages.append("Error: HF_TOKEN not found.")

    return "\n".join(log_messages)


def time_until_next_reboot():
    """Calculates and formats the time until the next planned reboot."""
    global next_reboot_datetime
    if next_reboot_datetime is None:
        next_reboot_datetime = get_next_scheduled_time()

    remaining_time = next_reboot_datetime - datetime.now()

    if remaining_time.total_seconds() <= 0:
        return "Rebooting..."

    mins, secs = divmod(remaining_time.seconds, 60)
    hours, mins = divmod(mins, 60)
    return f"{hours:02}:{mins:02}:{secs:02}"


def get_last_reboot_status():
    global next_reboot_datetime
    return f"Next target: {next_reboot_datetime.strftime('%Y-%m-%d %H:%M:%S')}"


def check_and_reboot():
    """Checks if current time has passed the scheduled time."""
    global next_reboot_datetime
    now = datetime.now()

    if now >= next_reboot_datetime:
        print(f"Reboot triggered at {now.strftime('%H:%M:%S')}")

        # 1. Execute Reboots
        perform_restarts(source="Schedule")

        # 2. Advance schedule
        next_reboot_datetime = get_next_scheduled_time()

        return f"Just rebooted. Next scheduled: {next_reboot_datetime.strftime('%H:%M:%S')}"

    return "Space running normally."

# --- API Functions ---


def api_get_info():
    """Returns JSON status for the API."""
    return {
        "status": "active",
        "next_reboot": str(next_reboot_datetime),
        "time_remaining": time_until_next_reboot(),
        "monitored_repos": REPOS
    }


def api_trigger_now():
    """Triggers immediate reboot via API."""
    logs = perform_restarts(source="API Force")
    return {
        "status": "executed",
        "timestamp": str(datetime.now()),
        "logs": logs
    }


def trigger_helper():
    if HF_TOKEN:
        api = HfApi()
        try:
            print(f"Restarting space {HELPER}...")
            api.pause_space(repo_id=HELPER)
            sleep(3)
            api.restart_space(repo_id=HELPER, token=HF_TOKEN)
        except Exception as e:
            print(f"Failed to restart {HELPER}: {e}")


def sanitize():
    # Sanitizer logic remains the same
    now = datetime.now()
    target_time = datetime.strptime(SELF_TIME, "%H:%M").time()
    next_run = datetime.combine(now.date(), target_time)
    if next_run <= now:
        next_run += timedelta(days=1)
    delay = (next_run - now).total_seconds()
    timer = threading.Timer(delay, trigger_helper)
    timer.start()
    print(f"Scheduled helper restart for {next_run}")


def pipeline():
    status = check_and_reboot()
    countdown = time_until_next_reboot()
    target_info = get_last_reboot_status()
    return countdown, status, target_info


# --- Gradio Interface ---
with gr.Blocks() as demo:
    gr.Markdown("## Space Rebooter")

    with gr.Row():
        next_reboot_display = gr.Textbox(label="Scheduled Target", value=get_last_reboot_status())
        next_reboot_countdown = gr.Textbox(label="Time Until Next Reboot", value=time_until_next_reboot())

    status_output = gr.Textbox(label="Log", value="Space running normally.")

    # --- Hidden API Elements ---
    # These buttons are hidden but define the API endpoints via 'api_name'
    api_info_btn = gr.Button("Get Info", visible=False)
    api_info_btn.click(fn=api_get_info, outputs=gr.JSON(), api_name="status")

    api_force_btn = gr.Button("Force Reboot", visible=False)
    api_force_btn.click(fn=api_trigger_now, outputs=gr.JSON(), api_name="force_reboot")

    # Timer ticks every 1 second
    timer = gr.Timer(1).tick(
        fn=pipeline,
        inputs=None,
        outputs=[next_reboot_countdown, status_output, next_reboot_display]
    )

if __name__ == "__main__":
    sanitize()
    demo.launch(server_name="0.0.0.0", server_port=7860)