|
|
@ -34,245 +34,358 @@ import sabnzbd.cfg as cfg |
|
|
|
from sabnzbd.constants import LOW_PRIORITY, NORMAL_PRIORITY, HIGH_PRIORITY |
|
|
|
|
|
|
|
|
|
|
|
__SCHED: Optional[kronos.ThreadedScheduler] = None # Global pointer to Scheduler instance |
|
|
|
|
|
|
|
SCHEDULE_GUARD_FLAG = False |
|
|
|
PP_PAUSE_EVENT = False |
|
|
|
|
|
|
|
|
|
|
|
def schedule_guard(): |
|
|
|
""" Set flag for scheduler restart """ |
|
|
|
global SCHEDULE_GUARD_FLAG |
|
|
|
SCHEDULE_GUARD_FLAG = True |
|
|
|
|
|
|
|
|
|
|
|
def pp_pause(): |
|
|
|
sabnzbd.PostProcessor.paused = True |
|
|
|
|
|
|
|
|
|
|
|
def pp_resume(): |
|
|
|
sabnzbd.PostProcessor.paused = False |
|
|
|
|
|
|
|
class Scheduler: |
|
|
|
def __init__(self): |
|
|
|
self.scheduler = kronos.ThreadedScheduler() |
|
|
|
self.pause_end: Optional[float] = None # Moment when pause will end |
|
|
|
self.restart_scheduler = False |
|
|
|
self.pp_pause_event = False |
|
|
|
self.load_schedules() |
|
|
|
|
|
|
|
def start(self): |
|
|
|
""" Start the scheduler """ |
|
|
|
self.scheduler.start() |
|
|
|
|
|
|
|
def stop(self): |
|
|
|
""" Stop the scheduler, destroy instance """ |
|
|
|
logging.debug("Stopping scheduler") |
|
|
|
self.scheduler.stop() |
|
|
|
|
|
|
|
def restart(self, plan_restart=True): |
|
|
|
""" Stop and start scheduler """ |
|
|
|
if plan_restart: |
|
|
|
self.restart_scheduler = True |
|
|
|
elif self.restart_scheduler: |
|
|
|
logging.debug("Restarting scheduler") |
|
|
|
self.restart_scheduler = False |
|
|
|
self.scheduler.stop() |
|
|
|
self.scheduler.start() |
|
|
|
self.analyse(sabnzbd.Downloader.paused) |
|
|
|
self.load_schedules() |
|
|
|
|
|
|
|
def abort(self): |
|
|
|
"""Emergency stop, just set the running attribute false so we don't |
|
|
|
have to wait the full scheduler-check cycle before it really stops""" |
|
|
|
self.scheduler.running = False |
|
|
|
|
|
|
|
def is_alive(self): |
|
|
|
""" Thread-like check if we are doing fine """ |
|
|
|
if self.scheduler.thread: |
|
|
|
return self.scheduler.thread.is_alive() |
|
|
|
return False |
|
|
|
|
|
|
|
def pp_pause_event(): |
|
|
|
return PP_PAUSE_EVENT |
|
|
|
def load_schedules(self): |
|
|
|
rss_planned = False |
|
|
|
|
|
|
|
for schedule in cfg.schedules(): |
|
|
|
arguments = [] |
|
|
|
argument_list = None |
|
|
|
|
|
|
|
def init(): |
|
|
|
""" Create the scheduler and set all required events """ |
|
|
|
global __SCHED |
|
|
|
try: |
|
|
|
enabled, m, h, d, action_name = schedule.split() |
|
|
|
except: |
|
|
|
try: |
|
|
|
enabled, m, h, d, action_name, argument_list = schedule.split(None, 5) |
|
|
|
except: |
|
|
|
continue # Bad schedule, ignore |
|
|
|
|
|
|
|
reset_guardian() |
|
|
|
__SCHED = kronos.ThreadedScheduler() |
|
|
|
rss_planned = False |
|
|
|
if argument_list: |
|
|
|
arguments = argument_list.split() |
|
|
|
|
|
|
|
for schedule in cfg.schedules(): |
|
|
|
arguments = [] |
|
|
|
argument_list = None |
|
|
|
|
|
|
|
try: |
|
|
|
enabled, m, h, d, action_name = schedule.split() |
|
|
|
except: |
|
|
|
action_name = action_name.lower() |
|
|
|
try: |
|
|
|
enabled, m, h, d, action_name, argument_list = schedule.split(None, 5) |
|
|
|
m = int(m) |
|
|
|
h = int(h) |
|
|
|
except: |
|
|
|
continue # Bad schedule, ignore |
|
|
|
logging.warning(T("Bad schedule %s at %s:%s"), action_name, m, h) |
|
|
|
continue |
|
|
|
|
|
|
|
if d.isdigit(): |
|
|
|
d = [int(i) for i in d] |
|
|
|
else: |
|
|
|
d = list(range(1, 8)) |
|
|
|
|
|
|
|
if action_name == "resume": |
|
|
|
action = self.scheduled_resume |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause": |
|
|
|
action = sabnzbd.Downloader.pause |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause_all": |
|
|
|
action = sabnzbd.pause_all |
|
|
|
arguments = [] |
|
|
|
elif action_name == "shutdown": |
|
|
|
action = sabnzbd.shutdown_program |
|
|
|
arguments = [] |
|
|
|
elif action_name == "restart": |
|
|
|
action = sabnzbd.restart_program |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause_post": |
|
|
|
action = pp_pause |
|
|
|
elif action_name == "resume_post": |
|
|
|
action = pp_resume |
|
|
|
elif action_name == "speedlimit" and arguments != []: |
|
|
|
action = sabnzbd.Downloader.limit_speed |
|
|
|
elif action_name == "enable_server" and arguments != []: |
|
|
|
action = sabnzbd.enable_server |
|
|
|
elif action_name == "disable_server" and arguments != []: |
|
|
|
action = sabnzbd.disable_server |
|
|
|
elif action_name == "scan_folder": |
|
|
|
action = sabnzbd.DirScanner.scan |
|
|
|
elif action_name == "rss_scan": |
|
|
|
action = sabnzbd.RSSReader.run |
|
|
|
rss_planned = True |
|
|
|
elif action_name == "remove_failed": |
|
|
|
action = sabnzbd.api.history_remove_failed |
|
|
|
elif action_name == "remove_completed": |
|
|
|
action = sabnzbd.api.history_remove_completed |
|
|
|
elif action_name == "enable_quota": |
|
|
|
action = sabnzbd.BPSMeter.set_status |
|
|
|
arguments = [True] |
|
|
|
elif action_name == "disable_quota": |
|
|
|
action = sabnzbd.BPSMeter.set_status |
|
|
|
arguments = [False] |
|
|
|
elif action_name == "pause_all_low": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [LOW_PRIORITY] |
|
|
|
elif action_name == "pause_all_normal": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [NORMAL_PRIORITY] |
|
|
|
elif action_name == "pause_all_high": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [HIGH_PRIORITY] |
|
|
|
elif action_name == "resume_all_low": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [LOW_PRIORITY] |
|
|
|
elif action_name == "resume_all_normal": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [NORMAL_PRIORITY] |
|
|
|
elif action_name == "resume_all_high": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [HIGH_PRIORITY] |
|
|
|
elif action_name == "pause_cat": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_cat |
|
|
|
arguments = [argument_list] |
|
|
|
elif action_name == "resume_cat": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_cat |
|
|
|
arguments = [argument_list] |
|
|
|
else: |
|
|
|
logging.warning(T("Unknown action: %s"), action_name) |
|
|
|
continue |
|
|
|
|
|
|
|
if enabled == "1": |
|
|
|
logging.info("Scheduling %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) |
|
|
|
self.scheduler.add_daytime_task(action, action_name, d, None, (h, m), args=arguments) |
|
|
|
else: |
|
|
|
logging.debug("Skipping %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) |
|
|
|
|
|
|
|
# Set RSS check interval |
|
|
|
if not rss_planned: |
|
|
|
interval = cfg.rss_rate() |
|
|
|
delay = random.randint(0, interval - 1) |
|
|
|
logging.info("Scheduling RSS interval task every %s min (delay=%s)", interval, delay) |
|
|
|
sabnzbd.RSSReader.next_run = time.time() + delay * 60 |
|
|
|
self.scheduler.add_interval_task(sabnzbd.RSSReader.run, "RSS", delay * 60, interval * 60) |
|
|
|
self.scheduler.add_single_task(sabnzbd.RSSReader.run, "RSS", 15) |
|
|
|
|
|
|
|
if cfg.version_check(): |
|
|
|
# Check for new release, once per week on random time |
|
|
|
m = random.randint(0, 59) |
|
|
|
h = random.randint(0, 23) |
|
|
|
d = (random.randint(1, 7),) |
|
|
|
|
|
|
|
logging.info("Scheduling VersionCheck on day %s at %s:%s", d[0], h, m) |
|
|
|
self.scheduler.add_daytime_task(sabnzbd.misc.check_latest_version, "VerCheck", d, None, (h, m)) |
|
|
|
|
|
|
|
action, hour, minute = sabnzbd.BPSMeter.get_quota() |
|
|
|
if action: |
|
|
|
logging.info("Setting schedule for quota check daily at %s:%s", hour, minute) |
|
|
|
self.scheduler.add_daytime_task(action, "quota_reset", list(range(1, 8)), None, (hour, minute)) |
|
|
|
|
|
|
|
if sabnzbd.misc.int_conv(cfg.history_retention()) > 0: |
|
|
|
logging.info("Setting schedule for midnight auto history-purge") |
|
|
|
self.scheduler.add_daytime_task( |
|
|
|
sabnzbd.database.midnight_history_purge, "midnight_history_purge", list(range(1, 8)), None, (0, 0) |
|
|
|
) |
|
|
|
|
|
|
|
logging.info("Setting schedule for midnight BPS reset") |
|
|
|
self.scheduler.add_daytime_task(sabnzbd.BPSMeter.midnight, "midnight_bps", list(range(1, 8)), None, (0, 0)) |
|
|
|
|
|
|
|
# Subscribe to special schedule changes |
|
|
|
cfg.rss_rate.callback(self.scheduler_restart_guard) |
|
|
|
|
|
|
|
def analyse(self, was_paused=False, priority=None): |
|
|
|
"""Determine what pause/resume state we would have now. |
|
|
|
'priority': evaluate only effect for given priority, return True for paused |
|
|
|
""" |
|
|
|
self.pp_pause_event = False |
|
|
|
paused = None |
|
|
|
paused_all = False |
|
|
|
pause_post = False |
|
|
|
pause_low = pause_normal = pause_high = False |
|
|
|
speedlimit = None |
|
|
|
quota = True |
|
|
|
servers = {} |
|
|
|
|
|
|
|
for ev in sort_schedules(all_events=True): |
|
|
|
if priority is None: |
|
|
|
logging.debug("Schedule check result = %s", ev) |
|
|
|
|
|
|
|
# Skip if disabled |
|
|
|
if ev[4] == "0": |
|
|
|
continue |
|
|
|
|
|
|
|
action = ev[1] |
|
|
|
try: |
|
|
|
value = ev[2] |
|
|
|
except: |
|
|
|
value = None |
|
|
|
if action == "pause": |
|
|
|
paused = True |
|
|
|
elif action == "pause_all": |
|
|
|
paused_all = True |
|
|
|
self.pp_pause_event = True |
|
|
|
elif action == "resume": |
|
|
|
paused = False |
|
|
|
paused_all = False |
|
|
|
elif action == "pause_post": |
|
|
|
pause_post = True |
|
|
|
self.pp_pause_event = True |
|
|
|
elif action == "resume_post": |
|
|
|
pause_post = False |
|
|
|
self.pp_pause_event = True |
|
|
|
elif action == "speedlimit" and value is not None: |
|
|
|
speedlimit = ev[2] |
|
|
|
elif action == "pause_all_low": |
|
|
|
pause_low = True |
|
|
|
elif action == "pause_all_normal": |
|
|
|
pause_normal = True |
|
|
|
elif action == "pause_all_high": |
|
|
|
pause_high = True |
|
|
|
elif action == "resume_all_low": |
|
|
|
pause_low = False |
|
|
|
elif action == "resume_all_normal": |
|
|
|
pause_normal = False |
|
|
|
elif action == "resume_all_high": |
|
|
|
pause_high = False |
|
|
|
elif action == "enable_quota": |
|
|
|
quota = True |
|
|
|
elif action == "disable_quota": |
|
|
|
quota = False |
|
|
|
elif action == "enable_server": |
|
|
|
try: |
|
|
|
servers[value] = 1 |
|
|
|
except: |
|
|
|
logging.warning(T("Schedule for non-existing server %s"), value) |
|
|
|
elif action == "disable_server": |
|
|
|
try: |
|
|
|
servers[value] = 0 |
|
|
|
except: |
|
|
|
logging.warning(T("Schedule for non-existing server %s"), value) |
|
|
|
|
|
|
|
# Special case, a priority was passed, so evaluate only that and return state |
|
|
|
if priority == LOW_PRIORITY: |
|
|
|
return pause_low |
|
|
|
if priority == NORMAL_PRIORITY: |
|
|
|
return pause_normal |
|
|
|
if priority == HIGH_PRIORITY: |
|
|
|
return pause_high |
|
|
|
if priority is not None: |
|
|
|
return False |
|
|
|
|
|
|
|
# Normal analysis |
|
|
|
if not was_paused: |
|
|
|
if paused_all: |
|
|
|
sabnzbd.pause_all() |
|
|
|
else: |
|
|
|
sabnzbd.unpause_all() |
|
|
|
sabnzbd.Downloader.set_paused_state(paused or paused_all) |
|
|
|
|
|
|
|
sabnzbd.PostProcessor.paused = pause_post |
|
|
|
if speedlimit is not None: |
|
|
|
sabnzbd.Downloader.limit_speed(speedlimit) |
|
|
|
|
|
|
|
sabnzbd.BPSMeter.set_status(quota, action=False) |
|
|
|
|
|
|
|
for serv in servers: |
|
|
|
try: |
|
|
|
item = config.get_config("servers", serv) |
|
|
|
value = servers[serv] |
|
|
|
if bool(item.enable()) != bool(value): |
|
|
|
item.enable.set(value) |
|
|
|
sabnzbd.Downloader.init_server(serv, serv) |
|
|
|
except: |
|
|
|
pass |
|
|
|
config.save_config() |
|
|
|
|
|
|
|
if argument_list: |
|
|
|
arguments = argument_list.split() |
|
|
|
def scheduler_restart_guard(self): |
|
|
|
""" Set flag for scheduler restart """ |
|
|
|
self.restart_scheduler = True |
|
|
|
|
|
|
|
action_name = action_name.lower() |
|
|
|
try: |
|
|
|
m = int(m) |
|
|
|
h = int(h) |
|
|
|
except: |
|
|
|
logging.warning(T("Bad schedule %s at %s:%s"), action_name, m, h) |
|
|
|
continue |
|
|
|
def scheduled_resume(self): |
|
|
|
""" Scheduled resume, only when no oneshot resume is active """ |
|
|
|
if self.pause_end is None: |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
if d.isdigit(): |
|
|
|
d = [int(i) for i in d] |
|
|
|
def __oneshot_resume(self, when): |
|
|
|
"""Called by delayed resume schedule |
|
|
|
Only resumes if call comes at the planned time |
|
|
|
""" |
|
|
|
if self.pause_end is not None and (when > self.pause_end - 5) and (when < self.pause_end + 55): |
|
|
|
self.pause_end = None |
|
|
|
logging.debug("Resume after pause-interval") |
|
|
|
sabnzbd.unpause_all() |
|
|
|
else: |
|
|
|
d = list(range(1, 8)) |
|
|
|
|
|
|
|
if action_name == "resume": |
|
|
|
action = scheduled_resume |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause": |
|
|
|
action = sabnzbd.Downloader.pause |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause_all": |
|
|
|
action = sabnzbd.pause_all |
|
|
|
arguments = [] |
|
|
|
elif action_name == "shutdown": |
|
|
|
action = sabnzbd.shutdown_program |
|
|
|
arguments = [] |
|
|
|
elif action_name == "restart": |
|
|
|
action = sabnzbd.restart_program |
|
|
|
arguments = [] |
|
|
|
elif action_name == "pause_post": |
|
|
|
action = pp_pause |
|
|
|
elif action_name == "resume_post": |
|
|
|
action = pp_resume |
|
|
|
elif action_name == "speedlimit" and arguments != []: |
|
|
|
action = sabnzbd.Downloader.limit_speed |
|
|
|
elif action_name == "enable_server" and arguments != []: |
|
|
|
action = sabnzbd.enable_server |
|
|
|
elif action_name == "disable_server" and arguments != []: |
|
|
|
action = sabnzbd.disable_server |
|
|
|
elif action_name == "scan_folder": |
|
|
|
action = sabnzbd.DirScanner.scan |
|
|
|
elif action_name == "rss_scan": |
|
|
|
action = sabnzbd.RSSReader.run |
|
|
|
rss_planned = True |
|
|
|
elif action_name == "remove_failed": |
|
|
|
action = sabnzbd.api.history_remove_failed |
|
|
|
elif action_name == "remove_completed": |
|
|
|
action = sabnzbd.api.history_remove_completed |
|
|
|
elif action_name == "enable_quota": |
|
|
|
action = sabnzbd.BPSMeter.set_status |
|
|
|
arguments = [True] |
|
|
|
elif action_name == "disable_quota": |
|
|
|
action = sabnzbd.BPSMeter.set_status |
|
|
|
arguments = [False] |
|
|
|
elif action_name == "pause_all_low": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [LOW_PRIORITY] |
|
|
|
elif action_name == "pause_all_normal": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [NORMAL_PRIORITY] |
|
|
|
elif action_name == "pause_all_high": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_prio |
|
|
|
arguments = [HIGH_PRIORITY] |
|
|
|
elif action_name == "resume_all_low": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [LOW_PRIORITY] |
|
|
|
elif action_name == "resume_all_normal": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [NORMAL_PRIORITY] |
|
|
|
elif action_name == "resume_all_high": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_prio |
|
|
|
arguments = [HIGH_PRIORITY] |
|
|
|
elif action_name == "pause_cat": |
|
|
|
action = sabnzbd.NzbQueue.pause_on_cat |
|
|
|
arguments = [argument_list] |
|
|
|
elif action_name == "resume_cat": |
|
|
|
action = sabnzbd.NzbQueue.resume_on_cat |
|
|
|
arguments = [argument_list] |
|
|
|
logging.debug("Ignoring cancelled resume") |
|
|
|
|
|
|
|
def plan_resume(self, interval): |
|
|
|
""" Set a scheduled resume after the interval """ |
|
|
|
if interval > 0: |
|
|
|
self.pause_end = time.time() + (interval * 60) |
|
|
|
logging.debug("Schedule resume at %s", self.pause_end) |
|
|
|
self.scheduler.add_single_task(self.__oneshot_resume, "", interval * 60, args=[self.pause_end]) |
|
|
|
sabnzbd.Downloader.pause() |
|
|
|
else: |
|
|
|
logging.warning(T("Unknown action: %s"), action_name) |
|
|
|
continue |
|
|
|
self.pause_end = None |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
if enabled == "1": |
|
|
|
logging.debug("Scheduling %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) |
|
|
|
__SCHED.add_daytime_task(action, action_name, d, None, (h, m), kronos.method.sequential, arguments, None) |
|
|
|
def pause_int(self) -> str: |
|
|
|
""" Return minutes:seconds until pause ends """ |
|
|
|
if self.pause_end is None: |
|
|
|
return "0" |
|
|
|
else: |
|
|
|
logging.debug("Skipping %s(%s) on days %s at %02d:%02d", action_name, arguments, d, h, m) |
|
|
|
|
|
|
|
# Set Guardian interval to 30 seconds |
|
|
|
__SCHED.add_interval_task(sched_guardian, "Guardian", 15, 30, kronos.method.sequential, None, None) |
|
|
|
|
|
|
|
# Set RSS check interval |
|
|
|
if not rss_planned: |
|
|
|
interval = cfg.rss_rate() |
|
|
|
delay = random.randint(0, interval - 1) |
|
|
|
logging.debug("Scheduling RSS interval task every %s min (delay=%s)", interval, delay) |
|
|
|
sabnzbd.RSSReader.next_run = time.time() + delay * 60 |
|
|
|
__SCHED.add_interval_task( |
|
|
|
sabnzbd.RSSReader.run, "RSS", delay * 60, interval * 60, kronos.method.sequential, None, None |
|
|
|
) |
|
|
|
__SCHED.add_single_task(sabnzbd.RSSReader.run, "RSS", 15, kronos.method.sequential, None, None) |
|
|
|
|
|
|
|
if cfg.version_check(): |
|
|
|
# Check for new release, once per week on random time |
|
|
|
m = random.randint(0, 59) |
|
|
|
h = random.randint(0, 23) |
|
|
|
d = (random.randint(1, 7),) |
|
|
|
|
|
|
|
logging.debug("Scheduling VersionCheck on day %s at %s:%s", d[0], h, m) |
|
|
|
__SCHED.add_daytime_task( |
|
|
|
sabnzbd.misc.check_latest_version, "VerCheck", d, None, (h, m), kronos.method.sequential, [], None |
|
|
|
) |
|
|
|
|
|
|
|
action, hour, minute = sabnzbd.BPSMeter.get_quota() |
|
|
|
if action: |
|
|
|
logging.info("Setting schedule for quota check daily at %s:%s", hour, minute) |
|
|
|
__SCHED.add_daytime_task( |
|
|
|
action, "quota_reset", list(range(1, 8)), None, (hour, minute), kronos.method.sequential, [], None |
|
|
|
) |
|
|
|
|
|
|
|
if sabnzbd.misc.int_conv(cfg.history_retention()) > 0: |
|
|
|
logging.info("Setting schedule for midnight auto history-purge") |
|
|
|
__SCHED.add_daytime_task( |
|
|
|
sabnzbd.database.midnight_history_purge, |
|
|
|
"midnight_history_purge", |
|
|
|
list(range(1, 8)), |
|
|
|
None, |
|
|
|
(0, 0), |
|
|
|
kronos.method.sequential, |
|
|
|
[], |
|
|
|
None, |
|
|
|
) |
|
|
|
|
|
|
|
logging.info("Setting schedule for midnight BPS reset") |
|
|
|
__SCHED.add_daytime_task( |
|
|
|
sabnzbd.BPSMeter.midnight, |
|
|
|
"midnight_bps", |
|
|
|
list(range(1, 8)), |
|
|
|
None, |
|
|
|
(0, 0), |
|
|
|
kronos.method.sequential, |
|
|
|
[], |
|
|
|
None, |
|
|
|
) |
|
|
|
|
|
|
|
# Subscribe to special schedule changes |
|
|
|
cfg.rss_rate.callback(schedule_guard) |
|
|
|
|
|
|
|
|
|
|
|
def start(): |
|
|
|
""" Start the scheduler """ |
|
|
|
global __SCHED |
|
|
|
if __SCHED: |
|
|
|
logging.debug("Starting scheduler") |
|
|
|
__SCHED.start() |
|
|
|
|
|
|
|
|
|
|
|
def restart(force=False): |
|
|
|
""" Stop and start scheduler """ |
|
|
|
global SCHEDULE_GUARD_FLAG |
|
|
|
|
|
|
|
if force: |
|
|
|
SCHEDULE_GUARD_FLAG = True |
|
|
|
else: |
|
|
|
if SCHEDULE_GUARD_FLAG: |
|
|
|
SCHEDULE_GUARD_FLAG = False |
|
|
|
stop() |
|
|
|
|
|
|
|
analyse(sabnzbd.Downloader.paused) |
|
|
|
|
|
|
|
init() |
|
|
|
start() |
|
|
|
|
|
|
|
|
|
|
|
def stop(): |
|
|
|
""" Stop the scheduler, destroy instance """ |
|
|
|
global __SCHED |
|
|
|
if __SCHED: |
|
|
|
logging.debug("Stopping scheduler") |
|
|
|
try: |
|
|
|
__SCHED.stop() |
|
|
|
except IndexError: |
|
|
|
pass |
|
|
|
del __SCHED |
|
|
|
__SCHED = None |
|
|
|
val = self.pause_end - time.time() |
|
|
|
if val < 0: |
|
|
|
sign = "-" |
|
|
|
val = abs(val) |
|
|
|
else: |
|
|
|
sign = "" |
|
|
|
mins = int(val / 60) |
|
|
|
sec = int(val - mins * 60) |
|
|
|
return "%s%d:%02d" % (sign, mins, sec) |
|
|
|
|
|
|
|
def pause_check(self): |
|
|
|
""" Unpause when time left is negative, compensate for missed schedule """ |
|
|
|
if self.pause_end is not None and (self.pause_end - time.time()) < 0: |
|
|
|
self.pause_end = None |
|
|
|
logging.debug("Force resume, negative timer") |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
def plan_server(self, action, parms, interval): |
|
|
|
""" Plan to re-activate server after 'interval' minutes """ |
|
|
|
self.scheduler.add_single_task(action, "", interval * 60, args=parms) |
|
|
|
|
|
|
|
def abort(): |
|
|
|
""" Emergency stop, just set the running attribute false """ |
|
|
|
global __SCHED |
|
|
|
if __SCHED: |
|
|
|
logging.debug("Terminating scheduler") |
|
|
|
__SCHED.running = False |
|
|
|
def force_rss(self): |
|
|
|
""" Add a one-time RSS scan, one second from now """ |
|
|
|
self.scheduler.add_single_task(sabnzbd.RSSReader.run, "RSS", 1) |
|
|
|
|
|
|
|
|
|
|
|
def pp_pause(): |
|
|
|
sabnzbd.PostProcessor.paused = True |
|
|
|
|
|
|
|
|
|
|
|
def pp_resume(): |
|
|
|
sabnzbd.PostProcessor.paused = False |
|
|
|
|
|
|
|
|
|
|
|
def sort_schedules(all_events, now=None): |
|
|
@ -318,211 +431,3 @@ def sort_schedules(all_events, now=None): |
|
|
|
|
|
|
|
events.sort(key=lambda x: x[0]) |
|
|
|
return events |
|
|
|
|
|
|
|
|
|
|
|
def analyse(was_paused=False, priority=None): |
|
|
|
"""Determine what pause/resume state we would have now. |
|
|
|
'priority': evaluate only effect for given priority, return True for paused |
|
|
|
""" |
|
|
|
global PP_PAUSE_EVENT |
|
|
|
PP_PAUSE_EVENT = False |
|
|
|
paused = None |
|
|
|
paused_all = False |
|
|
|
pause_post = False |
|
|
|
pause_low = pause_normal = pause_high = False |
|
|
|
speedlimit = None |
|
|
|
quota = True |
|
|
|
servers = {} |
|
|
|
|
|
|
|
for ev in sort_schedules(all_events=True): |
|
|
|
if priority is None: |
|
|
|
logging.debug("Schedule check result = %s", ev) |
|
|
|
|
|
|
|
# Skip if disabled |
|
|
|
if ev[4] == "0": |
|
|
|
continue |
|
|
|
|
|
|
|
action = ev[1] |
|
|
|
try: |
|
|
|
value = ev[2] |
|
|
|
except: |
|
|
|
value = None |
|
|
|
if action == "pause": |
|
|
|
paused = True |
|
|
|
elif action == "pause_all": |
|
|
|
paused_all = True |
|
|
|
PP_PAUSE_EVENT = True |
|
|
|
elif action == "resume": |
|
|
|
paused = False |
|
|
|
paused_all = False |
|
|
|
elif action == "pause_post": |
|
|
|
pause_post = True |
|
|
|
PP_PAUSE_EVENT = True |
|
|
|
elif action == "resume_post": |
|
|
|
pause_post = False |
|
|
|
PP_PAUSE_EVENT = True |
|
|
|
elif action == "speedlimit" and value is not None: |
|
|
|
speedlimit = ev[2] |
|
|
|
elif action == "pause_all_low": |
|
|
|
pause_low = True |
|
|
|
elif action == "pause_all_normal": |
|
|
|
pause_normal = True |
|
|
|
elif action == "pause_all_high": |
|
|
|
pause_high = True |
|
|
|
elif action == "resume_all_low": |
|
|
|
pause_low = False |
|
|
|
elif action == "resume_all_normal": |
|
|
|
pause_normal = False |
|
|
|
elif action == "resume_all_high": |
|
|
|
pause_high = False |
|
|
|
elif action == "enable_quota": |
|
|
|
quota = True |
|
|
|
elif action == "disable_quota": |
|
|
|
quota = False |
|
|
|
elif action == "enable_server": |
|
|
|
try: |
|
|
|
servers[value] = 1 |
|
|
|
except: |
|
|
|
logging.warning(T("Schedule for non-existing server %s"), value) |
|
|
|
elif action == "disable_server": |
|
|
|
try: |
|
|
|
servers[value] = 0 |
|
|
|
except: |
|
|
|
logging.warning(T("Schedule for non-existing server %s"), value) |
|
|
|
|
|
|
|
# Special case, a priority was passed, so evaluate only that and return state |
|
|
|
if priority == LOW_PRIORITY: |
|
|
|
return pause_low |
|
|
|
if priority == NORMAL_PRIORITY: |
|
|
|
return pause_normal |
|
|
|
if priority == HIGH_PRIORITY: |
|
|
|
return pause_high |
|
|
|
if priority is not None: |
|
|
|
return False |
|
|
|
|
|
|
|
# Normal analysis |
|
|
|
if not was_paused: |
|
|
|
if paused_all: |
|
|
|
sabnzbd.pause_all() |
|
|
|
else: |
|
|
|
sabnzbd.unpause_all() |
|
|
|
sabnzbd.Downloader.set_paused_state(paused or paused_all) |
|
|
|
|
|
|
|
sabnzbd.PostProcessor.paused = pause_post |
|
|
|
if speedlimit is not None: |
|
|
|
sabnzbd.Downloader.limit_speed(speedlimit) |
|
|
|
|
|
|
|
sabnzbd.BPSMeter.set_status(quota, action=False) |
|
|
|
|
|
|
|
for serv in servers: |
|
|
|
try: |
|
|
|
item = config.get_config("servers", serv) |
|
|
|
value = servers[serv] |
|
|
|
if bool(item.enable()) != bool(value): |
|
|
|
item.enable.set(value) |
|
|
|
sabnzbd.Downloader.init_server(serv, serv) |
|
|
|
except: |
|
|
|
pass |
|
|
|
config.save_config() |
|
|
|
|
|
|
|
|
|
|
|
# Support for single shot pause (=delayed resume) |
|
|
|
__PAUSE_END = None # Moment when pause will end |
|
|
|
|
|
|
|
|
|
|
|
def scheduled_resume(): |
|
|
|
""" Scheduled resume, only when no oneshot resume is active """ |
|
|
|
global __PAUSE_END |
|
|
|
if __PAUSE_END is None: |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
|
|
|
|
def __oneshot_resume(when): |
|
|
|
"""Called by delayed resume schedule |
|
|
|
Only resumes if call comes at the planned time |
|
|
|
""" |
|
|
|
global __PAUSE_END |
|
|
|
if __PAUSE_END is not None and (when > __PAUSE_END - 5) and (when < __PAUSE_END + 55): |
|
|
|
__PAUSE_END = None |
|
|
|
logging.debug("Resume after pause-interval") |
|
|
|
sabnzbd.unpause_all() |
|
|
|
else: |
|
|
|
logging.debug("Ignoring cancelled resume") |
|
|
|
|
|
|
|
|
|
|
|
def plan_resume(interval): |
|
|
|
""" Set a scheduled resume after the interval """ |
|
|
|
global __SCHED, __PAUSE_END |
|
|
|
if interval > 0: |
|
|
|
__PAUSE_END = time.time() + (interval * 60) |
|
|
|
logging.debug("Schedule resume at %s", __PAUSE_END) |
|
|
|
__SCHED.add_single_task(__oneshot_resume, "", interval * 60, kronos.method.sequential, [__PAUSE_END], None) |
|
|
|
sabnzbd.Downloader.pause() |
|
|
|
else: |
|
|
|
__PAUSE_END = None |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
|
|
|
|
def pause_int(): |
|
|
|
""" Return minutes:seconds until pause ends """ |
|
|
|
global __PAUSE_END |
|
|
|
if __PAUSE_END is None: |
|
|
|
return "0" |
|
|
|
else: |
|
|
|
val = __PAUSE_END - time.time() |
|
|
|
if val < 0: |
|
|
|
sign = "-" |
|
|
|
val = abs(val) |
|
|
|
else: |
|
|
|
sign = "" |
|
|
|
mins = int(val / 60) |
|
|
|
sec = int(val - mins * 60) |
|
|
|
return "%s%d:%02d" % (sign, mins, sec) |
|
|
|
|
|
|
|
|
|
|
|
def pause_check(): |
|
|
|
""" Unpause when time left is negative, compensate for missed schedule """ |
|
|
|
global __PAUSE_END |
|
|
|
if __PAUSE_END is not None and (__PAUSE_END - time.time()) < 0: |
|
|
|
__PAUSE_END = None |
|
|
|
logging.debug("Force resume, negative timer") |
|
|
|
sabnzbd.unpause_all() |
|
|
|
|
|
|
|
|
|
|
|
def plan_server(action, parms, interval): |
|
|
|
""" Plan to re-activate server after 'interval' minutes """ |
|
|
|
__SCHED.add_single_task(action, "", interval * 60, kronos.method.sequential, parms, None) |
|
|
|
|
|
|
|
|
|
|
|
def force_rss(): |
|
|
|
""" Add a one-time RSS scan, one second from now """ |
|
|
|
__SCHED.add_single_task(sabnzbd.RSSReader.run, "RSS", 1, kronos.method.sequential, None, None) |
|
|
|
|
|
|
|
|
|
|
|
# Scheduler Guarding system |
|
|
|
# Each check sets the guardian flag False |
|
|
|
# Each successful scheduled check sets the flag |
|
|
|
# If 4 consecutive checks fail, the scheduler is assumed to have crashed |
|
|
|
|
|
|
|
__SCHED_GUARDIAN = False |
|
|
|
__SCHED_GUARDIAN_CNT = 0 |
|
|
|
|
|
|
|
|
|
|
|
def reset_guardian(): |
|
|
|
global __SCHED_GUARDIAN, __SCHED_GUARDIAN_CNT |
|
|
|
__SCHED_GUARDIAN = False |
|
|
|
__SCHED_GUARDIAN_CNT = 0 |
|
|
|
|
|
|
|
|
|
|
|
def sched_guardian(): |
|
|
|
global __SCHED_GUARDIAN, __SCHED_GUARDIAN_CNT |
|
|
|
__SCHED_GUARDIAN = True |
|
|
|
|
|
|
|
|
|
|
|
def sched_check(): |
|
|
|
global __SCHED_GUARDIAN, __SCHED_GUARDIAN_CNT |
|
|
|
if not __SCHED_GUARDIAN: |
|
|
|
__SCHED_GUARDIAN_CNT += 1 |
|
|
|
return __SCHED_GUARDIAN_CNT < 4 |
|
|
|
reset_guardian() |
|
|
|
return True |
|
|
|