Skip to content

Commit 6f5984c

Browse files
authored
fix(scheduler ): scheduler path and check (NewFuture#550)
1 parent 1e0f066 commit 6f5984c

File tree

20 files changed

+397
-134
lines changed

20 files changed

+397
-134
lines changed

.github/patch.py

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,110 @@ def add_nuitka_include_modules(pyfile):
8686
return True
8787

8888

89+
def add_nuitka_windows_unbuffered(pyfile):
90+
"""
91+
为Windows平台在现有的 --python-flag 配置中添加 unbuffered 标志
92+
"""
93+
import platform
94+
95+
# 只在Windows平台执行
96+
if platform.system().lower() != "windows":
97+
print(f"Skipping unbuffered flag addition: not on Windows (current: {platform.system()})")
98+
return False
99+
100+
with open(pyfile, "r", encoding="utf-8") as f:
101+
content = f.read()
102+
103+
# 查找现有的 --python-flag 配置行
104+
python_flag_pattern = r"(# nuitka-project: --python-flag=)([^\n]*)"
105+
match = re.search(python_flag_pattern, content)
106+
107+
if not match:
108+
print(f"No existing --python-flag found in {pyfile}")
109+
return False
110+
111+
existing_flags = match.group(2)
112+
113+
# 检查是否已经包含 unbuffered
114+
if "unbuffered" in existing_flags:
115+
print(f"unbuffered flag already exists in {pyfile}")
116+
return False
117+
118+
# 添加 unbuffered 到现有标志
119+
new_flags = "unbuffered" if not existing_flags.strip() else existing_flags + ",unbuffered"
120+
new_content = re.sub(python_flag_pattern, r"\g<1>" + new_flags, content)
121+
122+
with open(pyfile, "w", encoding="utf-8") as f:
123+
f.write(new_content)
124+
125+
print(f"Added unbuffered to python-flag in {pyfile}: {new_flags}")
126+
return True
127+
128+
129+
def remove_scheduler_for_docker():
130+
"""
131+
为Docker构建移除scheduler相关代码和task子命令
132+
通过注释方式保持行号不变,便于调试
133+
"""
134+
import shutil
135+
136+
# 1. 移除scheduler文件夹
137+
scheduler_dir = os.path.join(ROOT, "ddns", "scheduler")
138+
if os.path.exists(scheduler_dir):
139+
shutil.rmtree(scheduler_dir)
140+
print(f"Removed scheduler directory: {scheduler_dir}")
141+
142+
# 2. 修改ddns/config/cli.py,注释掉scheduler相关代码(保持行号不变)
143+
cli_path = os.path.join(ROOT, "ddns", "config", "cli.py")
144+
if not os.path.exists(cli_path):
145+
return False
146+
147+
with open(cli_path, "r", encoding="utf-8") as f:
148+
content = f.read()
149+
150+
# 注释掉scheduler导入
151+
content = re.sub(
152+
r"^(from \.\.scheduler import get_scheduler)$",
153+
r"# \1",
154+
content,
155+
flags=re.MULTILINE
156+
)
157+
158+
# 注释掉函数调用
159+
content = re.sub(
160+
r"^(\s*)(_add_task_subcommand_if_needed\(parser\))$",
161+
r"\1# \2",
162+
content,
163+
flags=re.MULTILINE
164+
)
165+
166+
# 注释掉整个函数块,保持行号
167+
target_functions = ["_add_task_subcommand_if_needed", "_handle_task_command", "_print_status"]
168+
for func_name in target_functions:
169+
# 匹配函数定义到下一个函数或文件结尾
170+
pattern = rf"([ \t]*def {func_name}\s*\(.*?\):(?:.*?\n)*?)(?=^[ \t]*def |\Z)"
171+
172+
def comment_block(match):
173+
block = match.group(1)
174+
lines = block.split("\n")
175+
commented_lines = []
176+
for line in lines:
177+
if line.strip(): # 非空行
178+
# 在每行前加注释
179+
commented_lines.append("# " + line)
180+
else: # 空行保持原样
181+
commented_lines.append(line)
182+
return "\n".join(commented_lines)
183+
184+
content = re.sub(pattern, comment_block, content, flags=re.DOTALL | re.MULTILINE)
185+
186+
with open(cli_path, "w", encoding="utf-8") as f:
187+
f.write(content)
188+
189+
print(f"Commented out scheduler-related code in {cli_path} (preserving line numbers)")
190+
return True
191+
192+
89193
def remove_python2_compatibility(pyfile): # noqa: C901
90194
"""
91195
自动将所有 try-except python2/3 兼容导入替换为 python3 only 导入,并显示处理日志
@@ -351,9 +455,9 @@ def main():
351455
mode = sys.argv[1].lower() if len(sys.argv) > 1 else "default"
352456
version = resolve_version(mode)
353457

354-
if mode not in ["version", "release", "default"]:
458+
if mode not in ["version", "release", "default", "docker"]:
355459
print(f"unknown mode: {mode}")
356-
print("Usage: python patch.py [version|release]")
460+
print("Usage: python patch.py [version|release|docker]")
357461
exit(1)
358462
elif mode == "release":
359463
# 同步修改 doc/release.md 的版本与链接
@@ -379,8 +483,14 @@ def main():
379483
run_py_path = os.path.join(ROOT, "run.py")
380484
update_nuitka_version(run_py_path, version)
381485
add_nuitka_file_description(run_py_path)
486+
add_nuitka_windows_unbuffered(run_py_path)
382487
# add_nuitka_include_modules(run_py_path)
383488

489+
# 检测Docker环境并移除scheduler
490+
if mode == "docker":
491+
print("Detected Docker environment, removing scheduler components...")
492+
remove_scheduler_for_docker()
493+
384494
changed_files = 0
385495
for dirpath, _, filenames in os.walk(ROOT):
386496
for fname in filenames:

.github/workflows/build.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ jobs:
148148
runs-on: ${{ matrix.os }}
149149
env:
150150
OS_NAME: ${{ contains(matrix.os,'ubuntu') && 'ubuntu' || contains(matrix.os, 'mac') && 'mac' || 'windows' }}
151-
timeout-minutes: 10
151+
timeout-minutes: ${{ matrix.arch == 'x86' && 20 || contains(matrix.os, 'windows') && 12 || 8 }}
152152
steps:
153153
- uses: actions/checkout@v4
154154
- name: Set up Python 3.12
@@ -327,6 +327,7 @@ jobs:
327327
}}
328328
steps:
329329
- uses: actions/checkout@v4
330+
- run: python3 .github/patch.py docker
330331
- uses: docker/setup-qemu-action@v3 # 仅仅在需要时启用 QEMU 支持
331332
if: matrix.host == 'qemu'
332333
with:
@@ -388,9 +389,6 @@ jobs:
388389
docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag -h
389390
docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag || test -e "config.json"
390391
sudo rm -f config.json
391-
echo "Testing task functionality..."
392-
docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag ddns task -h
393-
docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag /ddns/tests/scripts/test-task-cron.sh ddns
394392
echo "Testing with config files..."
395393
docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/callback.json
396394
docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/multi-provider.json

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x
2424
steps:
2525
- uses: actions/checkout@v4
26+
- run: python3 .github/patch.py docker
2627
- uses: docker/setup-qemu-action@v3
2728
with:
2829
platforms: ${{ env.platforms }}

.vscode/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,26 @@
4141
120
4242
],
4343
"chat.agent.maxRequests": 64,
44+
"chat.tools.terminal.autoApprove": {
45+
"python": true,
46+
"python -c": true,
47+
"python -m": true,
48+
"python -m unittest": true,
49+
"python3": true,
50+
"python3 -c": true,
51+
"python3 -m": true,
52+
"python3 -m unittest": true,
53+
"echo": true,
54+
"cd": true,
55+
"ls": true,
56+
"dir": true,
57+
"git": true,
58+
"git status": true,
59+
"rm": true,
60+
"del": true,
61+
"delete": true,
62+
"mv": true,
63+
"move": true,
64+
},
65+
"github.copilot.chat.agent.currentEditorContext.enabled": true
4466
}

ddns/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ def main():
107107
# 兼容windows 和部分ASCII编码的老旧系统
108108
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
109109
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
110+
111+
# Windows 下输出一个空行
112+
if stdout and sys.platform.startswith("win"):
113+
stdout.write("\r\n")
114+
110115
logger.name = "ddns"
111116

112117
# 使用多配置加载器,它会自动处理单个和多个配置

ddns/config/cli.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
@author: NewFuture
55
"""
66

7-
import sys
87
import platform
9-
from argparse import Action, ArgumentParser, RawTextHelpFormatter, SUPPRESS
10-
from logging import DEBUG, getLevelName, basicConfig
8+
import sys
9+
from argparse import SUPPRESS, Action, ArgumentParser, RawTextHelpFormatter
10+
from logging import DEBUG, basicConfig, getLevelName
1111
from os import path as os_path
12-
from .file import save_config
12+
1313
from ..scheduler import get_scheduler
14+
from .file import save_config
1415

1516
__all__ = ["load_config", "str_bool"]
1617

@@ -246,7 +247,8 @@ def load_config(description, doc, version, date):
246247
parser = ArgumentParser(description=description, epilog=doc, formatter_class=RawTextHelpFormatter)
247248
sysinfo = _get_system_info_str()
248249
pyinfo = _get_python_info_str()
249-
version_str = "v{} ({})\n{}\n{}".format(version, date, pyinfo, sysinfo)
250+
compiled = getattr(sys.modules["__main__"], "__compiled__", "")
251+
version_str = "v{} ({})\n{}\n{}\n{}".format(version, date, pyinfo, sysinfo, compiled)
250252

251253
_add_ddns_args(parser) # Add common DDNS arguments to main parser
252254
# Default behavior (no subcommand) - add all the regular DDNS options
@@ -323,23 +325,18 @@ def _handle_task_command(args): # type: (dict) -> None
323325
status = scheduler.get_status()
324326

325327
if args.get("status") or status["installed"]:
326-
_print_status(status)
328+
print("DDNS Task Status:")
329+
print(" Installed: {}".format("Yes" if status["installed"] else "No"))
330+
print(" Scheduler: {}".format(status["scheduler"]))
331+
if status["installed"]:
332+
print(" Enabled: {}".format(status.get("enabled", "unknown")))
333+
print(" Interval: {} minutes".format(status.get("interval", "unknown")))
334+
print(" Command: {}".format(status.get("command", "unknown")))
335+
print(" Description: {}".format(status.get("description", "")))
327336
else:
328337
print("DDNS task is not installed. Installing with default settings...")
329338
if scheduler.install(interval, ddns_args):
330339
print("DDNS task installed successfully with {} minute interval".format(interval))
331340
else:
332341
print("Failed to install DDNS task")
333342
sys.exit(1)
334-
335-
336-
def _print_status(status):
337-
"""Print task status information"""
338-
print("DDNS Task Status:")
339-
print(" Installed: {}".format("Yes" if status["installed"] else "No"))
340-
print(" Scheduler: {}".format(status["scheduler"]))
341-
if status["installed"]:
342-
print(" Enabled: {}".format(status.get("enabled", "unknown")))
343-
print(" Interval: {} minutes".format(status.get("interval", "unknown")))
344-
print(" Command: {}".format(status.get("command", "unknown")))
345-
print(" Description: {}".format(status.get("description", "")))

ddns/scheduler/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
@author: NewFuture
66
"""
77

8-
import platform
98
import os
9+
import platform
1010

1111
from ddns.util.fileio import read_file_safely
1212

13+
from ..util.try_run import try_run
14+
1315
# Import all scheduler classes
1416
from ._base import BaseScheduler
15-
from .systemd import SystemdScheduler
1617
from .cron import CronScheduler
1718
from .launchd import LaunchdScheduler
1819
from .schtasks import SchtasksScheduler
20+
from .systemd import SystemdScheduler
1921

2022

2123
def get_scheduler(scheduler=None):
@@ -48,7 +50,10 @@ def get_scheduler(scheduler=None):
4850
launchd_dirs = ["/Library/LaunchDaemons", "/System/Library/LaunchDaemons"]
4951
if any(os.path.isdir(d) for d in launchd_dirs):
5052
return LaunchdScheduler()
51-
elif system == "linux" and read_file_safely("/proc/1/comm", default="").strip() == "systemd": # type: ignore
53+
elif system == "linux" and (
54+
(read_file_safely("/proc/1/comm", default="").strip().lower() == "systemd")
55+
or (try_run(["systemctl", "--version"]) is not None)
56+
): # Linux with systemd available
5257
return SystemdScheduler()
5358
return CronScheduler() # Other Unix-like systems, use cron
5459
elif scheduler == "systemd":

ddns/scheduler/_base.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
@author: NewFuture
55
"""
66

7-
import subprocess
87
import sys
98
from datetime import datetime
109
from logging import Logger, getLogger # noqa: F401
@@ -18,20 +17,10 @@ class BaseScheduler(object):
1817
def __init__(self, logger=None): # type: (Logger | None) -> None
1918
self.logger = (logger or getLogger()).getChild("task")
2019

21-
def _run_command(self, command, **kwargs): # type: (list[str], **Any) -> str | None
22-
"""Safely run subprocess command, return decoded string or None if failed"""
23-
try:
24-
if sys.version_info[0] >= 3:
25-
kwargs.setdefault("timeout", 60) # 60 second timeout to prevent hanging
26-
return subprocess.check_output(command, universal_newlines=True, **kwargs)
27-
except Exception as e:
28-
self.logger.debug("Command failed: %s", e)
29-
return None
30-
3120
def _get_ddns_cmd(self): # type: () -> list[str]
3221
"""Get DDNS command for scheduled execution as array"""
33-
if hasattr(sys, "frozen"):
34-
return [sys.argv[0] or sys.executable]
22+
if hasattr(sys.modules["__main__"], "__compiled__"):
23+
return [sys.argv[0]]
3524
else:
3625
return [sys.executable, "-m", "ddns"]
3726

ddns/scheduler/cron.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tempfile
1010

1111
from ..util.fileio import write_file
12+
from ..util.try_run import try_run
1213
from ._base import BaseScheduler
1314

1415

@@ -32,13 +33,13 @@ def _update_crontab(self, lines): # type: (list[str]) -> bool
3233
return False
3334

3435
def is_installed(self, crontab_content=None): # type: (str | None) -> bool
35-
result = crontab_content or self._run_command(["crontab", "-l"]) or ""
36+
result = crontab_content or try_run(["crontab", "-l"], logger=self.logger) or ""
3637
return self.KEY in result
3738

3839
def get_status(self):
3940
status = {"scheduler": "cron", "installed": False} # type: dict[str, str | bool | int | None]
4041
# Get crontab content once and reuse it for all checks
41-
crontab_content = self._run_command(["crontab", "-l"]) or ""
42+
crontab_content = try_run(["crontab", "-l"], logger=self.logger) or ""
4243
lines = crontab_content.splitlines()
4344
line = next((i for i in lines if self.KEY in i), "").strip()
4445

@@ -63,7 +64,7 @@ def install(self, interval, ddns_args=None):
6364
description = self._get_description()
6465
cron_entry = '*/{} * * * * cd "{}" && {} # DDNS: {}'.format(interval, os.getcwd(), ddns_command, description)
6566

66-
crontext = self._run_command(["crontab", "-l"]) or ""
67+
crontext = try_run(["crontab", "-l"], logger=self.logger) or ""
6768
lines = [line for line in crontext.splitlines() if self.KEY not in line]
6869
lines.append(cron_entry)
6970

@@ -84,7 +85,7 @@ def disable(self):
8485

8586
def _modify_cron_lines(self, action): # type: (str) -> bool
8687
"""Helper to enable, disable, or uninstall cron lines"""
87-
crontext = self._run_command(["crontab", "-l"])
88+
crontext = try_run(["crontab", "-l"], logger=self.logger)
8889
if not crontext or self.KEY not in crontext:
8990
self.logger.info("No crontab found")
9091
return False

0 commit comments

Comments
 (0)