ddns 4.1.0b3__tar.gz → 4.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {ddns-4.1.0b3 → ddns-4.1.1}/PKG-INFO +2 -2
  2. {ddns-4.1.0b3 → ddns-4.1.1}/README.md +1 -1
  3. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/__init__.py +2 -2
  4. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/__main__.py +19 -9
  5. ddns-4.1.1/ddns/config/__init__.py +171 -0
  6. ddns-4.1.1/ddns/config/cli.py +366 -0
  7. ddns-4.1.1/ddns/config/config.py +214 -0
  8. ddns-4.1.1/ddns/config/env.py +77 -0
  9. ddns-4.1.1/ddns/config/file.py +169 -0
  10. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/ip.py +50 -4
  11. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/cloudflare.py +25 -6
  12. ddns-4.1.1/ddns/scheduler/__init__.py +72 -0
  13. ddns-4.1.1/ddns/scheduler/_base.py +80 -0
  14. ddns-4.1.1/ddns/scheduler/cron.py +111 -0
  15. ddns-4.1.1/ddns/scheduler/launchd.py +130 -0
  16. ddns-4.1.1/ddns/scheduler/schtasks.py +120 -0
  17. ddns-4.1.1/ddns/scheduler/systemd.py +139 -0
  18. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/util/fileio.py +2 -2
  19. ddns-4.1.1/ddns/util/try_run.py +37 -0
  20. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/PKG-INFO +2 -2
  21. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/SOURCES.txt +16 -0
  22. {ddns-4.1.0b3 → ddns-4.1.1}/pyproject.toml +1 -2
  23. ddns-4.1.1/tests/test_config_cli_extra.py +132 -0
  24. ddns-4.1.1/tests/test_config_env_extra.py +145 -0
  25. ddns-4.1.1/tests/test_config_extra.py +244 -0
  26. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_schema_v4_1.py +78 -0
  27. ddns-4.1.1/tests/test_ip.py +239 -0
  28. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_alidns.py +26 -0
  29. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_aliesa.py +27 -0
  30. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_cloudflare.py +125 -10
  31. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_dnscom.py +17 -0
  32. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_dnspod.py +18 -0
  33. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_huaweidns.py +24 -0
  34. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_namesilo.py +23 -0
  35. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_tencentcloud.py +21 -0
  36. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_base.py +37 -9
  37. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_cron.py +119 -35
  38. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_init.py +16 -11
  39. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_launchd.py +32 -26
  40. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_schtasks.py +33 -34
  41. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_scheduler_systemd.py +34 -21
  42. {ddns-4.1.0b3 → ddns-4.1.1}/LICENSE +0 -0
  43. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/__builtins__.pyi +0 -0
  44. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/cache.py +0 -0
  45. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/__init__.py +0 -0
  46. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/_base.py +0 -0
  47. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/_signature.py +0 -0
  48. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/alidns.py +0 -0
  49. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/aliesa.py +0 -0
  50. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/callback.py +0 -0
  51. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/debug.py +0 -0
  52. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/dnscom.py +0 -0
  53. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/dnspod.py +0 -0
  54. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/dnspod_com.py +0 -0
  55. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/edgeone.py +0 -0
  56. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/he.py +0 -0
  57. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/huaweidns.py +0 -0
  58. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/namesilo.py +0 -0
  59. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/noip.py +0 -0
  60. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/provider/tencentcloud.py +0 -0
  61. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/util/__init__.py +0 -0
  62. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/util/comment.py +0 -0
  63. {ddns-4.1.0b3 → ddns-4.1.1}/ddns/util/http.py +0 -0
  64. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/dependency_links.txt +0 -0
  65. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/entry_points.txt +0 -0
  66. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/requires.txt +0 -0
  67. {ddns-4.1.0b3 → ddns-4.1.1}/ddns.egg-info/top_level.txt +0 -0
  68. {ddns-4.1.0b3 → ddns-4.1.1}/setup.cfg +0 -0
  69. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_cache.py +0 -0
  70. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_cli.py +0 -0
  71. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_cli_task.py +0 -0
  72. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_config.py +0 -0
  73. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_env.py +0 -0
  74. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_file.py +0 -0
  75. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_file_remote.py +0 -0
  76. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_init.py +0 -0
  77. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_config_init_multi.py +0 -0
  78. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider__signature.py +0 -0
  79. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_base.py +0 -0
  80. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_base_simple.py +0 -0
  81. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_callback.py +0 -0
  82. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_debug.py +0 -0
  83. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_dnspod_com.py +0 -0
  84. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_edgeone.py +0 -0
  85. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_he.py +0 -0
  86. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_noip.py +0 -0
  87. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_provider_proxy_list.py +0 -0
  88. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_util_comment.py +0 -0
  89. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_util_fileio.py +0 -0
  90. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_util_http.py +0 -0
  91. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_util_http_proxy_list.py +0 -0
  92. {ddns-4.1.0b3 → ddns-4.1.1}/tests/test_util_http_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddns
3
- Version: 4.1.0b3
3
+ Version: 4.1.1
4
4
  Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
5
5
  Author-email: NewFuture <python@newfuture.cc>
6
6
  License-Expression: MIT
@@ -138,7 +138,7 @@ Dynamic: license-file
138
138
  也可使用一键安装脚本自动下载并安装对应平台的二进制:
139
139
 
140
140
  ```bash
141
- curl -fSL https://ddns.newfuture.cc/install.sh | sh
141
+ curl -#fSL https://ddns.newfuture.cc/install.sh | sh
142
142
  ```
143
143
  提示:安装到系统目录(如 /usr/local/bin)可能需要 root 或 sudo 权限;若权限不足,可改为 `sudo sh` 运行。
144
144
 
@@ -94,7 +94,7 @@
94
94
  也可使用一键安装脚本自动下载并安装对应平台的二进制:
95
95
 
96
96
  ```bash
97
- curl -fSL https://ddns.newfuture.cc/install.sh | sh
97
+ curl -#fSL https://ddns.newfuture.cc/install.sh | sh
98
98
  ```
99
99
  提示:安装到系统目录(如 /usr/local/bin)可能需要 root 或 sudo 权限;若权限不足,可改为 `sudo sh` 运行。
100
100
 
@@ -6,7 +6,7 @@ ddns Package
6
6
  __description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
7
7
 
8
8
  # 编译时,版本会被替换
9
- __version__ = "4.1.0b3"
9
+ __version__ = "4.1.1"
10
10
 
11
11
  # 时间也会被替换掉
12
- build_date = "2025-08-11T15:00:49Z"
12
+ build_date = "2025-10-31T07:36:47Z"
@@ -4,16 +4,16 @@ DDNS
4
4
  @author: NewFuture, rufengsuixing
5
5
  """
6
6
 
7
+ import sys
7
8
  from io import TextIOWrapper
8
- from subprocess import check_output
9
9
  from logging import getLogger
10
- import sys
10
+ from subprocess import check_output
11
11
 
12
- from .__init__ import __version__, __description__, build_date
13
- from .config import load_configs, Config # noqa: F401
14
- from .provider import get_provider_class, SimpleProvider # noqa: F401
15
12
  from . import ip
13
+ from .__init__ import __description__, __version__, build_date
16
14
  from .cache import Cache
15
+ from .config import Config, load_configs # noqa: F401
16
+ from .provider import SimpleProvider, get_provider_class # noqa: F401
17
17
 
18
18
  logger = getLogger()
19
19
 
@@ -68,7 +68,7 @@ def update_ip(dns, cache, index_rule, domains, record_type, config):
68
68
  update_success = True
69
69
  else:
70
70
  try:
71
- result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line)
71
+ result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line, **config.extra)
72
72
  if result:
73
73
  logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
74
74
  update_success = True
@@ -92,7 +92,12 @@ def run(config):
92
92
  # dns provider class
93
93
  provider_class = get_provider_class(config.dns)
94
94
  dns = provider_class(
95
- config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
95
+ config.id,
96
+ config.token,
97
+ endpoint=config.endpoint,
98
+ logger=logger,
99
+ proxy=config.proxy,
100
+ ssl=config.ssl,
96
101
  )
97
102
  cache = Cache.new(config.cache, config.md5(), logger)
98
103
  return (
@@ -102,11 +107,16 @@ def run(config):
102
107
 
103
108
 
104
109
  def main():
105
- encode = sys.stdout.encoding
106
- if encode is not None and encode.lower() != "utf-8" and hasattr(sys.stdout, "buffer"):
110
+ stdout = sys.stdout # pythonw 模式无 stdout
111
+ if stdout and stdout.encoding and stdout.encoding.lower() != "utf-8" and hasattr(stdout, "buffer"):
107
112
  # 兼容windows 和部分ASCII编码的老旧系统
108
113
  sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
109
114
  sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
115
+
116
+ # Windows 下输出一个空行
117
+ if stdout and sys.platform.startswith("win"):
118
+ stdout.write("\r\n")
119
+
110
120
  logger.name = "ddns"
111
121
 
112
122
  # 使用多配置加载器,它会自动处理单个和多个配置
@@ -0,0 +1,171 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ Configuration loader for DDNS.
4
+
5
+ This module handles loading configuration from command-line arguments,
6
+ JSON configuration files, and environment variables.
7
+
8
+ @author: NewFuture
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import logging
14
+ from .cli import load_config as load_cli_config
15
+ from .file import load_config as load_file_config, save_config
16
+ from .env import load_config as load_env_config
17
+ from .config import Config, split_array_string
18
+
19
+
20
+ def _get_config_paths(config_paths):
21
+ # type: (list[str] | None) -> list[str]
22
+ """
23
+ 获取配置文件路径列表,支持多个配置文件
24
+ """
25
+ if not config_paths:
26
+ # Find config file in default locations
27
+ for p in [
28
+ "config.json",
29
+ os.path.expanduser("~/.ddns/config.json"),
30
+ os.path.expanduser("~/.ddns.json"),
31
+ "/etc/ddns/config.json",
32
+ "/etc/ddns.json",
33
+ ]:
34
+ if os.path.exists(p):
35
+ return [p]
36
+ return []
37
+
38
+ # 验证所有路径都存在(跳过URL检查)
39
+ for config_path in config_paths:
40
+ # 跳过远程URL的存在性检查
41
+ if "://" not in config_path and not os.path.exists(config_path):
42
+ sys.stderr.write("Config file `%s` does not exist!\n" % config_path)
43
+ sys.stdout.write("Please check the path or use `--new-config` to create new one.\n")
44
+ sys.exit(1)
45
+
46
+ return config_paths
47
+
48
+
49
+ def _setup_logging(cli_config, env_config, all_json_configs):
50
+ # type: (dict, dict, list[dict]) -> logging.Logger
51
+ """Setup logging configuration and return logger."""
52
+ # Use first config for global log settings (log config is inherited from global in v4.1 format)
53
+ json_config = all_json_configs[0] if all_json_configs else {}
54
+ global_conf = Config(cli_config=cli_config, json_config=json_config, env_config=env_config)
55
+ log_format = global_conf.log_format # type: str # type: ignore
56
+ if log_format:
57
+ # A custom log format is already set; no further action is required.
58
+ pass
59
+ elif global_conf.log_level < logging.INFO:
60
+ # Override log format in debug mode to include filename and line number for detailed debugging
61
+ log_format = "%(asctime)s %(levelname)s [%(name)s.%(funcName)s](%(filename)s:%(lineno)d): %(message)s"
62
+ elif global_conf.log_level > logging.INFO:
63
+ log_format = "%(asctime)s %(levelname)s: %(message)s"
64
+ else:
65
+ log_format = "%(asctime)s %(levelname)s [%(name)s]: %(message)s"
66
+ logging.basicConfig(
67
+ level=global_conf.log_level, format=log_format, datefmt=global_conf.log_datefmt, filename=global_conf.log_file
68
+ )
69
+ return logging.getLogger().getChild("config") # type: logging.Logger
70
+
71
+
72
+ def _load_json_configs(config_paths, proxy, ssl):
73
+ # type: (list[str], list[str], str) -> list[dict]
74
+ """Load all JSON configurations from config paths."""
75
+ all_json_configs = []
76
+ for config_path in config_paths:
77
+ json_configs = load_file_config(config_path, proxy=proxy, ssl=ssl)
78
+ if isinstance(json_configs, list):
79
+ all_json_configs.extend(json_configs)
80
+ else:
81
+ all_json_configs.append(json_configs)
82
+
83
+ # 如果没有找到任何配置文件或JSON配置,创建一个空配置
84
+ return all_json_configs or [{}]
85
+
86
+
87
+ def _validate_configs(configs, logger):
88
+ # type: (list[Config], logging.Logger) -> None
89
+ """Validate that all configs have DNS providers."""
90
+ for i, conf in enumerate(configs):
91
+ if not conf.dns:
92
+ logger.critical(
93
+ "No DNS provider specified in config %d! Please set `dns` in config or use `--dns` CLI option.", i + 1
94
+ )
95
+ sys.exit(2)
96
+
97
+
98
+ def load_configs(description, version, date):
99
+ # type: (str, str, str) -> list[Config]
100
+ """
101
+ Load and merge configuration from CLI, JSON, and environment variables.
102
+ Supports multiple config files and array config formats.
103
+
104
+ This function loads configuration from all three sources and returns a
105
+ list of Config objects that provides easy access to merged configuration values.
106
+
107
+ Args:
108
+ description (str): The program description for the CLI parser.
109
+ version (str): The program version for the CLI parser.
110
+ date (str): The program release date for the CLI parser.
111
+
112
+ Returns:
113
+ list[Config]: A list of Config objects with merged configuration from all sources.
114
+ """
115
+ doc = """
116
+ ddns [v{version}@{date}]
117
+ (i) homepage or docs [文档主页]: https://ddns.newfuture.cc/
118
+ (?) issues or bugs [问题和反馈]: https://github.com/NewFuture/DDNS/issues
119
+ Copyright (c) NewFuture (MIT License)
120
+ """.format(version=version, date=date)
121
+ # Load CLI configuration first
122
+ cli_config = load_cli_config(description, doc, version, date)
123
+ env_config = load_env_config()
124
+
125
+ # 获取配置文件路径列表
126
+ config_paths = split_array_string(cli_config.get("config", env_config.get("config", [])))
127
+ config_paths = _get_config_paths(config_paths)
128
+
129
+ # 提取代理和SSL设置用于HTTP请求
130
+ proxy_settings = split_array_string(cli_config.get("proxy", env_config.get("proxy", []))) # type: list[str]
131
+ ssl_settings = cli_config.get("ssl", env_config.get("ssl", "auto"))
132
+
133
+ # 加载所有配置文件
134
+ all_json_configs = _load_json_configs(config_paths, proxy_settings, ssl_settings)
135
+
136
+ # 为每个JSON配置创建Config对象
137
+ configs = [
138
+ Config(cli_config=cli_config, json_config=json_config, env_config=env_config)
139
+ for json_config in all_json_configs
140
+ ]
141
+
142
+ # 设置日志
143
+ logger = _setup_logging(cli_config, env_config, all_json_configs)
144
+
145
+ # 处理无配置情况 - inline _handle_no_config logic
146
+ if len(cli_config) <= 1 and len(all_json_configs) == 1 and not all_json_configs[0] and not env_config:
147
+ # 没有配置时生成默认配置文件
148
+ logger.warning("[deprecated] auto gernerate config file will be deprecated in future versions.")
149
+ logger.warning("usage:\n `ddns --new-config` to generate a new config.\n `ddns -h` for help.")
150
+ default_config_path = config_paths[0] if config_paths else "config.json"
151
+ save_config(default_config_path, cli_config)
152
+ logger.info("No config file found, generated default config at `%s`.", default_config_path)
153
+ sys.exit(1)
154
+
155
+ # 记录配置加载情况
156
+ if config_paths:
157
+ logger.info("load config: %s", config_paths)
158
+ else:
159
+ logger.debug("No config file specified, using CLI and environment variables only.")
160
+
161
+ # 仅在没有配置文件且开启debug时自动设置debug provider
162
+ if not config_paths and cli_config.get("debug") and len(configs) == 1 and not configs[0].dns:
163
+ configs[0].dns = "debug"
164
+
165
+ # 验证每个配置都有DNS provider
166
+ _validate_configs(configs, logger)
167
+
168
+ return configs
169
+
170
+
171
+ __all__ = ["load_configs", "Config"]
@@ -0,0 +1,366 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ Configuration loader for DDNS command-line interface.
4
+ @author: NewFuture
5
+ """
6
+
7
+ import platform
8
+ import sys
9
+ from argparse import SUPPRESS, Action, ArgumentParser, RawTextHelpFormatter
10
+ from logging import DEBUG, basicConfig, getLevelName
11
+ from os import path as os_path
12
+
13
+ from ..scheduler import get_scheduler
14
+ from .file import save_config
15
+
16
+ __all__ = ["load_config", "str_bool"]
17
+
18
+
19
+ def str_bool(v):
20
+ # type: (str | bool | None | int | float | list) -> bool | str
21
+ """
22
+ parse string to boolean
23
+ """
24
+ if isinstance(v, bool):
25
+ return v
26
+ if v is None:
27
+ return False
28
+ if not isinstance(v, str) and not type(v).__name__ == "unicode":
29
+ return bool(v) # For non-string types, convert to string first
30
+ if v.lower() in ("yes", "true", "t", "y", "1"): # type: ignore[attribute-defined]
31
+ return True
32
+ elif v.lower() in ("no", "false", "f", "n", "0"): # type: ignore[attribute-defined]
33
+ return False
34
+ else:
35
+ return v # type: ignore[return-value]
36
+
37
+
38
+ def log_level(value):
39
+ """
40
+ parse string to log level
41
+ or getattr(logging, value.upper())
42
+ """
43
+ return getLevelName(value if isinstance(value, int) else value.upper())
44
+
45
+
46
+ def _get_system_info_str():
47
+ system = platform.system()
48
+ release = platform.release()
49
+ machine = platform.machine()
50
+ arch = platform.architecture()
51
+ return "{}-{} {} {}".format(system, release, machine, arch)
52
+
53
+
54
+ def _get_python_info_str():
55
+ version = platform.python_version()
56
+ branch, py_build_date = platform.python_build()
57
+ return "Python-{} {} ({})".format(version, branch, py_build_date)
58
+
59
+
60
+ class ExtendAction(Action):
61
+ """兼容 Python <3.8 的 extend action"""
62
+
63
+ def __call__(self, parser, namespace, values, option_string=None):
64
+ items = getattr(namespace, self.dest, None)
65
+ if items is None:
66
+ items = []
67
+ # values 可能是单个值或列表
68
+ if isinstance(values, list):
69
+ items.extend(values)
70
+ else:
71
+ items.append(values)
72
+ setattr(namespace, self.dest, items)
73
+
74
+
75
+ class NewConfigAction(Action):
76
+ """生成配置文件并退出程序"""
77
+
78
+ def __call__(self, parser, namespace, values, option_string=None):
79
+ # 获取配置文件路径
80
+ if values and values != "true":
81
+ config_path = str(values) # type: str
82
+ else:
83
+ config_path = getattr(namespace, "config", None) or "config.json" # type: str
84
+ config_path = config_path[0] if isinstance(config_path, list) else config_path
85
+ if os_path.exists(config_path):
86
+ sys.stderr.write("The default %s already exists!\n" % config_path)
87
+ sys.stdout.write("Please use `--new-config=%s` to specify a new config file.\n" % config_path)
88
+ sys.exit(1)
89
+ # 获取当前已解析的参数
90
+ current_config = {k: v for k, v in vars(namespace).items() if v is not None}
91
+ # 保存配置文件
92
+ save_config(config_path, current_config)
93
+ sys.stdout.write("%s is generated.\n" % config_path)
94
+ sys.exit(0)
95
+
96
+
97
+ def _add_ddns_args(arg): # type: (ArgumentParser) -> None
98
+ """Add common DDNS arguments to a parser"""
99
+ log_levels = [
100
+ "CRITICAL", # 50
101
+ "ERROR", # 40
102
+ "WARNING", # 30
103
+ "INFO", # 20
104
+ "DEBUG", # 10
105
+ "NOTSET", # 0
106
+ ]
107
+ arg.add_argument(
108
+ "-c",
109
+ "--config",
110
+ nargs="*",
111
+ action=ExtendAction,
112
+ metavar="FILE",
113
+ help="load config file [配置文件路径, 可多次指定]",
114
+ )
115
+ arg.add_argument("--debug", action="store_true", help="debug mode [开启调试模式]")
116
+
117
+ # DDNS Configuration group
118
+ ddns = arg.add_argument_group("DDNS Configuration [DDNS配置参数]")
119
+ ddns.add_argument(
120
+ "--dns",
121
+ help="DNS provider [DNS服务提供商]",
122
+ choices=[
123
+ "51dns",
124
+ "alidns",
125
+ "aliesa",
126
+ "callback",
127
+ "cloudflare",
128
+ "debug",
129
+ "dnscom",
130
+ "dnspod_com",
131
+ "dnspod",
132
+ "edgeone",
133
+ "he",
134
+ "huaweidns",
135
+ "namesilo",
136
+ "noip",
137
+ "tencentcloud",
138
+ ],
139
+ )
140
+ ddns.add_argument("--id", help="API ID or email [对应账号ID或邮箱]")
141
+ ddns.add_argument("--token", help="API token or key [授权凭证或密钥]")
142
+ ddns.add_argument("--endpoint", help="API endpoint URL [API端点URL]")
143
+ ddns.add_argument(
144
+ "--index4", nargs="*", action=ExtendAction, metavar="RULE", help="IPv4 rules [获取IPv4方式, 多次可配置多规则]"
145
+ )
146
+ ddns.add_argument(
147
+ "--index6", nargs="*", action=ExtendAction, metavar="RULE", help="IPv6 rules [获取IPv6方式, 多次配置多规则]"
148
+ )
149
+ ddns.add_argument(
150
+ "--ipv4", nargs="*", action=ExtendAction, metavar="DOMAIN", help="IPv4 domains [IPv4域名列表, 可配多个域名]"
151
+ )
152
+ ddns.add_argument(
153
+ "--ipv6", nargs="*", action=ExtendAction, metavar="DOMAIN", help="IPv6 domains [IPv6域名列表, 可配多个域名]"
154
+ )
155
+ ddns.add_argument("--ttl", type=int, help="DNS TTL(s) [设置域名解析过期时间]")
156
+ ddns.add_argument("--line", help="DNS line/route [DNS线路设置]")
157
+
158
+ # Advanced Options group
159
+ advanced = arg.add_argument_group("Advanced Options [高级参数]")
160
+ advanced.add_argument("--proxy", nargs="*", action=ExtendAction, help="HTTP proxy [设置http代理,可配多个代理连接]")
161
+ advanced.add_argument(
162
+ "--cache", type=str_bool, nargs="?", const=True, help="set cache [启用缓存开关,或传入保存路径]"
163
+ )
164
+ advanced.add_argument(
165
+ "--no-cache", dest="cache", action="store_const", const=False, help="disable cache [关闭缓存等效 --cache=false]"
166
+ )
167
+ advanced.add_argument(
168
+ "--ssl",
169
+ type=str_bool,
170
+ nargs="?",
171
+ const=True,
172
+ help="SSL certificate verification [SSL证书验证方式]: "
173
+ "true(强制验证), false(禁用验证), auto(自动降级), /path/to/cert.pem(自定义证书)",
174
+ )
175
+ advanced.add_argument(
176
+ "--no-ssl",
177
+ dest="ssl",
178
+ action="store_const",
179
+ const=False,
180
+ help="disable SSL verify [禁用验证, 等效 --ssl=false]",
181
+ )
182
+ advanced.add_argument("--log_file", metavar="FILE", help="log file [日志文件,默认标准输出]")
183
+ advanced.add_argument("--log.file", "--log-file", dest="log_file", help=SUPPRESS) # 隐藏参数
184
+ advanced.add_argument("--log_level", type=log_level, metavar="|".join(log_levels), help=None)
185
+ advanced.add_argument("--log.level", "--log-level", dest="log_level", type=log_level, help=SUPPRESS) # 隐藏参数
186
+ advanced.add_argument("--log_format", metavar="FORMAT", help="set log format [日志格式]")
187
+ advanced.add_argument("--log.format", "--log-format", dest="log_format", help=SUPPRESS) # 隐藏参数
188
+ advanced.add_argument("--log_datefmt", metavar="FORMAT", help="set log date format [日志时间格式]")
189
+ advanced.add_argument("--log.datefmt", "--log-datefmt", dest="log_datefmt", help=SUPPRESS) # 隐藏参数
190
+
191
+
192
+ def _add_task_subcommand_if_needed(parser): # type: (ArgumentParser) -> None
193
+ """
194
+ Conditionally add task subcommand to avoid Python 2 'too few arguments' error.
195
+
196
+ Python 2's argparse requires subcommand when subparsers are defined, but Python 3 doesn't.
197
+ We only add subparsers when the first argument is likely a subcommand (doesn't start with '-').
198
+ """
199
+ # Python2 Only add subparsers when first argument is a subcommand (not an option)
200
+ if len(sys.argv) <= 1 or (sys.argv[1].startswith("-") and sys.argv[1] != "--help"):
201
+ return
202
+
203
+ # Add subparsers for subcommands
204
+ subparsers = parser.add_subparsers(dest="command", help="subcommands [子命令]")
205
+
206
+ # Create task subcommand parser
207
+ task = subparsers.add_parser("task", help="Manage scheduled tasks [管理定时任务]")
208
+ task.set_defaults(func=_handle_task_command)
209
+ _add_ddns_args(task)
210
+
211
+ # Add task-specific arguments
212
+ task.add_argument(
213
+ "-i",
214
+ "--install",
215
+ nargs="?",
216
+ type=int,
217
+ const=5,
218
+ metavar="MINs",
219
+ help="Install task with <mins> [安装定时任务,默认5分钟]",
220
+ )
221
+ task.add_argument("--uninstall", action="store_true", help="Uninstall scheduled task [卸载定时任务]")
222
+ task.add_argument("--status", action="store_true", help="Show task status [显示定时任务状态]")
223
+ task.add_argument("--enable", action="store_true", help="Enable scheduled task [启用定时任务]")
224
+ task.add_argument("--disable", action="store_true", help="Disable scheduled task [禁用定时任务]")
225
+ task.add_argument(
226
+ "--scheduler",
227
+ choices=["auto", "systemd", "cron", "launchd", "schtasks"],
228
+ default="auto",
229
+ help="Specify scheduler type [指定定时任务方式]",
230
+ )
231
+
232
+
233
+ def load_config(description, doc, version, date):
234
+ # type: (str, str, str, str) -> dict
235
+ """
236
+ 解析命令行参数并返回配置字典。
237
+
238
+ Args:
239
+ description (str): 程序描述
240
+ doc (str): 程序文档
241
+ version (str): 程序版本
242
+ date (str): 构建日期
243
+
244
+ Returns:
245
+ dict: 配置字典
246
+ """
247
+ parser = ArgumentParser(description=description, epilog=doc, formatter_class=RawTextHelpFormatter)
248
+ sysinfo = _get_system_info_str()
249
+ pyinfo = _get_python_info_str()
250
+ compiled = getattr(sys.modules["__main__"], "__compiled__", "")
251
+ version_str = "v{} ({})\n{}\n{}\n{}".format(version, date, pyinfo, sysinfo, compiled)
252
+
253
+ _add_ddns_args(parser) # Add common DDNS arguments to main parser
254
+ # Default behavior (no subcommand) - add all the regular DDNS options
255
+ parser.add_argument("-v", "--version", action="version", version=version_str)
256
+ parser.add_argument(
257
+ "--new-config", metavar="FILE", action=NewConfigAction, nargs="?", help="generate new config [生成配置文件]"
258
+ )
259
+
260
+ # Python 2/3 compatibility: conditionally add subparsers to avoid 'too few arguments' error
261
+ # Subparsers are only needed when user provides a subcommand (non-option argument)
262
+ _add_task_subcommand_if_needed(parser)
263
+
264
+ args, unknown = parser.parse_known_args()
265
+
266
+ # Parse unknown arguments that follow --extra.xxx format
267
+ extra_args = {} # type: dict
268
+ i = 0
269
+ while i < len(unknown):
270
+ arg = unknown[i]
271
+ if arg.startswith("--extra."):
272
+ key = "extra_" + arg[8:] # Remove "--extra." and add "extra_" prefix
273
+ # Check if there's a value for this argument
274
+ if i + 1 < len(unknown) and not unknown[i + 1].startswith("--"):
275
+ extra_args[key] = unknown[i + 1]
276
+ i += 2
277
+ else:
278
+ # No value provided, set to True (flag)
279
+ extra_args[key] = True # type: ignore[assignment]
280
+ i += 1
281
+ else:
282
+ # Unknown argument that doesn't match our pattern
283
+ sys.stderr.write("Warning: Unknown argument: {}\n".format(arg))
284
+ i += 1
285
+
286
+ # Merge extra_args into args namespace
287
+ for k, v in extra_args.items():
288
+ setattr(args, k, v)
289
+
290
+ # Handle task subcommand and exit early if present
291
+ if hasattr(args, "func"):
292
+ args.func(vars(args))
293
+ sys.exit(0)
294
+
295
+ is_debug = getattr(args, "debug", False)
296
+ if is_debug:
297
+ # 如果启用调试模式,则强制设置日志级别为 DEBUG
298
+ args.log_level = log_level("DEBUG")
299
+ if args.cache is None:
300
+ args.cache = False # 禁用缓存
301
+
302
+ # 将 Namespace 对象转换为字典并直接返回
303
+ config = vars(args)
304
+ return {k: v for k, v in config.items() if v is not None} # 过滤掉 None 值的配置项
305
+
306
+
307
+ def _handle_task_command(args): # type: (dict) -> None
308
+ """Handle task subcommand"""
309
+ basicConfig(level=args["debug"] and DEBUG or args.get("log_level", "INFO"))
310
+
311
+ # Use specified scheduler or auto-detect
312
+ scheduler_type = args.get("scheduler", "auto")
313
+ scheduler = get_scheduler(scheduler_type)
314
+
315
+ interval = args.get("install", 5) or 5
316
+ excluded_keys = ("status", "install", "uninstall", "enable", "disable", "command", "scheduler", "func")
317
+ ddns_args = {k: v for k, v in args.items() if k not in excluded_keys and v is not None}
318
+
319
+ # Execute operations
320
+ for op in ["install", "uninstall", "enable", "disable"]:
321
+ if not args.get(op):
322
+ continue
323
+
324
+ # Check if task is installed for enable/disable
325
+ if op in ["enable", "disable"] and not scheduler.is_installed():
326
+ print("DDNS task is not installed" + (" Please install it first." if op == "enable" else "."))
327
+ sys.exit(1)
328
+
329
+ # Execute operation
330
+ print("{} DDNS scheduled task...".format(op.title()))
331
+ func = getattr(scheduler, op)
332
+ result = func(interval, ddns_args) if op == "install" else func()
333
+
334
+ if result:
335
+ past_tense = {
336
+ "install": "installed",
337
+ "uninstall": "uninstalled",
338
+ "enable": "enabled",
339
+ "disable": "disabled",
340
+ }[op]
341
+ suffix = " with {} minute interval".format(interval) if op == "install" else ""
342
+ print("DDNS task {} successfully{}".format(past_tense, suffix))
343
+ else:
344
+ print("Failed to {} DDNS task".format(op))
345
+ sys.exit(1)
346
+ return
347
+
348
+ # Show status or auto-install
349
+ status = scheduler.get_status()
350
+
351
+ if args.get("status") or status["installed"]:
352
+ print("DDNS Task Status:")
353
+ print(" Installed: {}".format("Yes" if status["installed"] else "No"))
354
+ print(" Scheduler: {}".format(status["scheduler"]))
355
+ if status["installed"]:
356
+ print(" Enabled: {}".format(status.get("enabled", "unknown")))
357
+ print(" Interval: {} minutes".format(status.get("interval", "unknown")))
358
+ print(" Command: {}".format(status.get("command", "unknown")))
359
+ print(" Description: {}".format(status.get("description", "")))
360
+ else:
361
+ print("DDNS task is not installed. Installing with default settings...")
362
+ if scheduler.install(interval, ddns_args):
363
+ print("DDNS task installed successfully with {} minute interval".format(interval))
364
+ else:
365
+ print("Failed to install DDNS task")
366
+ sys.exit(1)