-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathplugin_updater.py
More file actions
153 lines (131 loc) Β· 7.5 KB
/
Copy pathplugin_updater.py
File metadata and controls
153 lines (131 loc) Β· 7.5 KB
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
# ==========================================================
# FILE: plugin_updater.py
# ==========================================================
# π¨ MODIFIED: [λ°°ν¬ λ½λ€μ΄ νμλΌμΈ μ΅μ ν (Case 09 μ λ μ¬μ)] κΈ°μ‘΄ 04:00~16:10 ESTμ κ³Όλν λ½λ€μ΄μ νκΈ°νκ³ , μ€μ V-REV μ¬λΌμ΄μ± ν격 λ° μ μ°μ΄ μ΄λ£¨μ΄μ§λ 'μ₯ λ§κ° μ§μ ν΅μ¬ κ΅μ μλμ°(15:26 ~ 16:10 EST)'λ‘λ§ μ λ° λ½μ¨ μλ£. μ΄λ₯Ό ν΅ν΄ λΉ-κ΅μ μκ°λμλ μμ λ‘μ΄ μ
λ°μ΄νΈλ₯Ό 보μ₯νλ, μ€μ μ£Όλ¬Έμ΄ λκ°λ λμ°Έμ¬ μν ꡬκ°λ§ 100% μ§κ³΅ 격리ν¨.
# π¨ MODIFIED: [V44.53 μ 1νλ² λ° 16κ³λͺ
μ λ λ½μ¨] λ¬λ ₯ API(mcal) μ€μΊμ λΉλκΈ°(to_thread) λν
# π¨ MODIFIED: [Case 14 μ λ νλ² μ€μ] λ¬λ ₯ API νμμμ 5.0μ΄λ₯Ό 10.0μ΄λ‘ ν©νΈ κ΅μ νμ¬ νμμμ νλ² μΌμν.
# π¨ MODIFIED: [μ 1νλ² μ² μ μ€μ] λ‘μ time.sleep(0.06) λμ§ μ½λλ₯Ό μꡬ μκ°νκ³ GlobalThrottle.wait_api_sync() μ€μ ν΅μ μ λ½μ¨ κ²°μ.
# π¨ MODIFIED: [μ 1νλ² κ΅μ ] μλΈνλ‘μΈμ€ κ΅μ°© λ°©μ΄λ₯Ό μν 30μ΄ νμμμ μ‘±μ λ° os.makedirs λΉλκΈ° λν μ λ©΄ κ²°μ
# π¨ NEW: [μ’λΉ νλ‘μΈμ€ λ°©μ΄] μλΈνλ‘μΈμ€ TimeoutError λ°μ μ .kill() μ§ν await .wait()λ₯Ό κ°μ νΈμΆνμ¬ OS μμ λμ(Zombie) μμ² μ°¨λ¨
# ==========================================================
import logging
import asyncio
import subprocess
import os
import time
import datetime
from zoneinfo import ZoneInfo
from dotenv import load_dotenv
from global_throttle import GlobalThrottle # π¨ μ€μ ν΅μ μ κ²°μ
class SystemUpdater:
def __init__(self):
self.remote_branch = "origin/main"
load_dotenv()
self.daemon_name = os.getenv("daemon_name") or os.getenv("DAEMON_NAME", "mybot")
async def is_update_allowed(self):
est = ZoneInfo('America/New_York')
now_est = datetime.datetime.now(est)
if now_est.weekday() >= 5:
return True, ""
def _check_holiday():
GlobalThrottle.wait_api_sync()
import pandas_market_calendars as mcal
nyse = mcal.get_calendar('NYSE')
schedule = nyse.schedule(start_date=now_est.date(), end_date=now_est.date())
return schedule.empty
is_holiday = False
for attempt in range(3):
try:
is_holiday = await asyncio.wait_for(asyncio.to_thread(_check_holiday), timeout=10.0)
break
except asyncio.TimeoutError:
if attempt == 2:
logging.error("β οΈ [Updater] λ¬λ ₯ API νμμμ. Fail-Open νμΌ κ°μ κ²μ¬ μ§ν.")
else:
await asyncio.sleep(1.0 * (2 ** attempt))
except Exception as e:
if attempt == 2:
logging.debug(f"μ
λ°μ΄νΈ λ½λ€μ΄ λ¬λ ₯ μ€μΊ μλ¬ (무μνκ³ μκ° κ²μ¬ μ§ν): {e}")
else:
await asyncio.sleep(1.0 * (2 ** attempt))
if is_holiday:
return True, ""
curr_time = now_est.time()
# π¨ MODIFIED: [λ°°ν¬ λ½λ€μ΄ μ λ° μΊ‘ν] μ€μ λ§€λ§€κ° λ°μνλ 15:26 ~ 16:10 EST ꡬκ°λ§ μ
λ°μ΄νΈ μ λ©΄ ν΅μ
start_lock = datetime.time(15, 26)
end_lock = datetime.time(16, 10)
if start_lock <= curr_time <= end_lock:
return False, "β οΈ <b>[λ°°ν¬ κΈμ§]</b> μ€μ μ£Όλ¬Έμ΄ μ§νλλ ν΅μ¬ κ΅μ μλμ°(15:26~16:10 EST) μ€μλ ν«ν½μ€ λ‘€λ°± λ°©μ§λ₯Ό μν΄ μλ μ
λ°μ΄νΈλ₯Ό κ°μ μ°¨λ¨ν©λλ€."
return True, ""
async def _create_safety_backup(self):
try:
backup_dir = "stable_backup"
await asyncio.to_thread(os.makedirs, backup_dir, exist_ok=True)
proc = await asyncio.create_subprocess_shell(
f"cp -p *.py {backup_dir}/ 2>/dev/null || true",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
try:
await asyncio.wait_for(proc.communicate(), timeout=30.0)
logging.info("π‘οΈ [Updater] λ‘€λ°± λ΄μ μν μμ λ (stable_backup) κ²°μ μλ£")
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
logging.error("π¨ [Updater] μμ λ κ²°μ μλΈνλ‘μΈμ€ ν΅μ νμμμ (30μ΄ μ΄κ³Ό). λ°±μ
μ 건λλλλ€.")
except Exception as e:
logging.error(f"π¨ [Updater] μμ λ κ²°μ μ€ μλ¬ λ°μ (μ
λ°μ΄νΈλ κ³μ μ§ν): {e}")
async def pull_latest_code(self):
allowed, msg = await self.is_update_allowed()
if not allowed:
logging.warning(f"π [Updater] κΉνλΈ κ°μ λκΈ°ν μ°¨λ¨ (λ λμ‘΄): {msg}")
return False, msg
await self._create_safety_backup()
try:
fetch_proc = await asyncio.create_subprocess_shell(
"git fetch --all",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
try:
_, fetch_err = await asyncio.wait_for(fetch_proc.communicate(), timeout=30.0)
except asyncio.TimeoutError:
fetch_proc.kill()
await fetch_proc.wait()
return False, "Git Fetch ν΅μ μ§μ° νμμμ (30μ΄ μ΄κ³Ό)"
if fetch_proc.returncode != 0:
error_msg = fetch_err.decode('utf-8').strip()
logging.error(f"π¨ [Updater] Git Fetch μ€ν¨: {error_msg}")
return False, f"Git Fetch μ€ν¨: {error_msg} (μλ²μμ git init λ° remote add λͺ
λ Ήμ μ ννμμμ€)"
reset_proc = await asyncio.create_subprocess_shell(
f"git reset --hard {self.remote_branch}",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
try:
_, reset_err = await asyncio.wait_for(reset_proc.communicate(), timeout=30.0)
except asyncio.TimeoutError:
reset_proc.kill()
await reset_proc.wait()
return False, "Git Reset ν΅μ μ§μ° νμμμ (30μ΄ μ΄κ³Ό)"
if reset_proc.returncode != 0:
error_msg = reset_err.decode('utf-8').strip()
logging.error(f"π¨ [Updater] Git Reset μ€ν¨: {error_msg}")
return False, f"Git Reset μ€ν¨: {error_msg}"
logging.info("β
[Updater] κΉνλΈ μ΅μ μ½λ κ°μ λκΈ°ν μλ£")
return True, "κΉνλΈ μ΅μ μ½λκ° λ‘컬μ μλ²½ν λκΈ°νλμμ΅λλ€."
except Exception as e:
logging.error(f"π¨ [Updater] λκΈ°ν μ€ μΉλͺ
μ μμΈ λ°μ: {e}")
return False, f"μ
λ°μ΄νΈ νλ‘μΈμ€ μμΈ λ°μ: {e}"
async def restart_daemon(self):
allowed, _ = await self.is_update_allowed()
if not allowed:
logging.error("β λ λμ‘΄ μκ°λ λ°λͺ¬ μ¬κ°λ μλκ° κ°μ§λμ΄ OS κ°μ μ°¨λ¨νμ΅λλ€.")
return False
try:
logging.info(f"π [Updater] μ’λΉ μ
§λ€μ΄ λ°©μ΄λ₯Ό μν΄ νμ΄μ¬ νλ‘μΈμ€λ₯Ό μ¦μ μν(Hard Kill)μν΅λλ€. (systemdκ° λΆνμν΄)")
os._exit(0)
return True
except Exception as e:
logging.error(f"π¨ [Updater] λ°λͺ¬ μν λͺ
λ Ή νλ¬ μ€ν¨: {e}")
return False