ddns 4.1.0b4__py2.py3-none-any.whl → 4.1.1b1__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of ddns might be problematic. Click here for more details.
- ddns/__init__.py +2 -2
- ddns/__main__.py +7 -2
- ddns/config/__init__.py +171 -0
- ddns/config/cli.py +366 -0
- ddns/config/config.py +214 -0
- ddns/config/env.py +77 -0
- ddns/config/file.py +169 -0
- ddns/provider/cloudflare.py +25 -6
- ddns/scheduler/__init__.py +72 -0
- ddns/scheduler/_base.py +80 -0
- ddns/scheduler/cron.py +111 -0
- ddns/scheduler/launchd.py +130 -0
- ddns/scheduler/schtasks.py +120 -0
- ddns/scheduler/systemd.py +139 -0
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/METADATA +1 -1
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/RECORD +20 -9
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/WHEEL +0 -0
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/entry_points.txt +0 -0
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.1.0b4.dist-info → ddns-4.1.1b1.dist-info}/top_level.txt +0 -0
ddns/scheduler/cron.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Cron-based task scheduler for Unix-like systems
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from ..util.fileio import write_file
|
|
12
|
+
from ..util.try_run import try_run
|
|
13
|
+
from ._base import BaseScheduler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CronScheduler(BaseScheduler):
|
|
17
|
+
"""Cron-based task scheduler for Unix-like systems"""
|
|
18
|
+
|
|
19
|
+
SCHEDULER_NAME = "cron"
|
|
20
|
+
|
|
21
|
+
KEY = "# DDNS:"
|
|
22
|
+
|
|
23
|
+
def _update_crontab(self, lines): # type: (list[str]) -> bool
|
|
24
|
+
"""Update crontab with new content"""
|
|
25
|
+
try:
|
|
26
|
+
temp_path = tempfile.mktemp(suffix=".cron")
|
|
27
|
+
write_file(temp_path, u"\n".join(lines) + u"\n") # fmt: skip
|
|
28
|
+
subprocess.check_call(["crontab", temp_path])
|
|
29
|
+
os.unlink(temp_path)
|
|
30
|
+
return True
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.logger.error("Failed to update crontab: %s", e)
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def is_installed(self, crontab_content=None): # type: (str | None) -> bool
|
|
36
|
+
result = crontab_content or try_run(["crontab", "-l"], logger=self.logger) or ""
|
|
37
|
+
return self.KEY in result
|
|
38
|
+
|
|
39
|
+
def get_status(self):
|
|
40
|
+
status = {"scheduler": "cron", "installed": False} # type: dict[str, str | bool | int | None]
|
|
41
|
+
# Get crontab content once and reuse it for all checks
|
|
42
|
+
crontab_content = try_run(["crontab", "-l"], logger=self.logger) or ""
|
|
43
|
+
lines = crontab_content.splitlines()
|
|
44
|
+
line = next((i for i in lines if self.KEY in i), "").strip()
|
|
45
|
+
|
|
46
|
+
if line: # Task is installed
|
|
47
|
+
status["installed"] = True
|
|
48
|
+
status["enabled"] = bool(line and not line.startswith("#"))
|
|
49
|
+
else: # Task not installed
|
|
50
|
+
status["enabled"] = False
|
|
51
|
+
|
|
52
|
+
cmd_groups = line.split(self.KEY, 1) if line else ["", ""]
|
|
53
|
+
parts = cmd_groups[0].strip(" #\t").split() if cmd_groups[0] else []
|
|
54
|
+
status["interval"] = int(parts[0][2:]) if len(parts) >= 5 and parts[0].startswith("*/") else None
|
|
55
|
+
status["command"] = " ".join(parts[5:]) if len(parts) >= 6 else None
|
|
56
|
+
status["description"] = cmd_groups[1].strip() if len(cmd_groups) > 1 else None
|
|
57
|
+
|
|
58
|
+
return status
|
|
59
|
+
|
|
60
|
+
def install(self, interval, ddns_args=None):
|
|
61
|
+
ddns_commands = self._build_ddns_command(ddns_args)
|
|
62
|
+
# Convert array to properly quoted command string for cron
|
|
63
|
+
ddns_command = self._quote_command_array(ddns_commands)
|
|
64
|
+
description = self._get_description()
|
|
65
|
+
cron_entry = '*/{} * * * * cd "{}" && {} # DDNS: {}'.format(interval, os.getcwd(), ddns_command, description)
|
|
66
|
+
|
|
67
|
+
crontext = try_run(["crontab", "-l"], logger=self.logger) or ""
|
|
68
|
+
lines = [line for line in crontext.splitlines() if self.KEY not in line]
|
|
69
|
+
lines.append(cron_entry)
|
|
70
|
+
|
|
71
|
+
if self._update_crontab(lines):
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
self.logger.error("Failed to install DDNS cron job")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def uninstall(self):
|
|
78
|
+
return self._modify_cron_lines("uninstall")
|
|
79
|
+
|
|
80
|
+
def enable(self):
|
|
81
|
+
return self._modify_cron_lines("enable")
|
|
82
|
+
|
|
83
|
+
def disable(self):
|
|
84
|
+
return self._modify_cron_lines("disable")
|
|
85
|
+
|
|
86
|
+
def _modify_cron_lines(self, action): # type: (str) -> bool
|
|
87
|
+
"""Helper to enable, disable, or uninstall cron lines"""
|
|
88
|
+
crontext = try_run(["crontab", "-l"], logger=self.logger)
|
|
89
|
+
if not crontext or self.KEY not in crontext:
|
|
90
|
+
self.logger.info("No crontab found")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
modified_lines = []
|
|
94
|
+
for line in crontext.rstrip("\n").splitlines():
|
|
95
|
+
if self.KEY not in line:
|
|
96
|
+
modified_lines.append(line)
|
|
97
|
+
elif action == "uninstall":
|
|
98
|
+
continue # Skip DDNS lines (remove them)
|
|
99
|
+
elif action == "enable" and line.strip().startswith("#"):
|
|
100
|
+
uncommented = line.lstrip(" #\t").lstrip() # Enable: uncomment the line
|
|
101
|
+
modified_lines.append(uncommented if uncommented else line)
|
|
102
|
+
elif action == "disable" and not line.strip().startswith("#"):
|
|
103
|
+
modified_lines.append("# " + line) # Disable: comment the line
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError("Invalid action: {}".format(action))
|
|
106
|
+
|
|
107
|
+
if self._update_crontab(modified_lines):
|
|
108
|
+
return True
|
|
109
|
+
else:
|
|
110
|
+
self.logger.error("Failed to %s DDNS cron job", action)
|
|
111
|
+
return False
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
macOS launchd-based task scheduler
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from ..util.fileio import read_file_safely, write_file
|
|
11
|
+
from ..util.try_run import try_run
|
|
12
|
+
from ._base import BaseScheduler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LaunchdScheduler(BaseScheduler):
|
|
16
|
+
"""macOS launchd-based task scheduler"""
|
|
17
|
+
|
|
18
|
+
LABEL = "cc.newfuture.ddns"
|
|
19
|
+
|
|
20
|
+
def _get_plist_path(self):
|
|
21
|
+
return os.path.expanduser("~/Library/LaunchAgents/{}.plist".format(self.LABEL))
|
|
22
|
+
|
|
23
|
+
def is_installed(self):
|
|
24
|
+
return os.path.exists(self._get_plist_path())
|
|
25
|
+
|
|
26
|
+
def get_status(self):
|
|
27
|
+
# Read plist content once and use it to determine installation status
|
|
28
|
+
content = read_file_safely(self._get_plist_path())
|
|
29
|
+
status = {"scheduler": "launchd", "installed": bool(content)}
|
|
30
|
+
if not content:
|
|
31
|
+
return status
|
|
32
|
+
|
|
33
|
+
# For launchd, check if service is actually loaded/enabled
|
|
34
|
+
result = try_run(["launchctl", "list"], logger=self.logger)
|
|
35
|
+
status["enabled"] = bool(result) and self.LABEL in result
|
|
36
|
+
|
|
37
|
+
# Get interval
|
|
38
|
+
interval_match = re.search(r"<key>StartInterval</key>\s*<integer>(\d+)</integer>", content)
|
|
39
|
+
status["interval"] = int(interval_match.group(1)) // 60 if interval_match else None
|
|
40
|
+
|
|
41
|
+
# Get command
|
|
42
|
+
program_match = re.search(r"<key>Program</key>\s*<string>([^<]+)</string>", content)
|
|
43
|
+
if program_match:
|
|
44
|
+
status["command"] = program_match.group(1)
|
|
45
|
+
else:
|
|
46
|
+
args_section = re.search(r"<key>ProgramArguments</key>\s*<array>(.*?)</array>", content, re.DOTALL)
|
|
47
|
+
if args_section:
|
|
48
|
+
strings = re.findall(r"<string>([^<]+)</string>", args_section.group(1))
|
|
49
|
+
if strings:
|
|
50
|
+
status["command"] = " ".join(strings)
|
|
51
|
+
|
|
52
|
+
# Get comments
|
|
53
|
+
desc_match = re.search(r"<key>Description</key>\s*<string>([^<]+)</string>", content)
|
|
54
|
+
status["description"] = desc_match.group(1) if desc_match else None
|
|
55
|
+
|
|
56
|
+
return status
|
|
57
|
+
|
|
58
|
+
def install(self, interval, ddns_args=None):
|
|
59
|
+
plist_path = self._get_plist_path()
|
|
60
|
+
program_args = self._build_ddns_command(ddns_args)
|
|
61
|
+
|
|
62
|
+
# Create comment with version and install date (consistent with Windows)
|
|
63
|
+
comment = self._get_description()
|
|
64
|
+
|
|
65
|
+
# Generate plist XML using template string for proper macOS plist format
|
|
66
|
+
program_args_xml = "".join(" <string>{}</string>\n".format(arg) for arg in program_args)
|
|
67
|
+
|
|
68
|
+
plist_content = """<?xml version="1.0" encoding="UTF-8"?>
|
|
69
|
+
<plist version="1.0">
|
|
70
|
+
<dict>
|
|
71
|
+
<key>Label</key>
|
|
72
|
+
<string>{label}</string>
|
|
73
|
+
<key>Description</key>
|
|
74
|
+
<string>{description}</string>
|
|
75
|
+
<key>ProgramArguments</key>
|
|
76
|
+
<array>{program}</array>
|
|
77
|
+
<key>StartInterval</key>
|
|
78
|
+
<integer>{interval}</integer>
|
|
79
|
+
<key>RunAtLoad</key>
|
|
80
|
+
<true/>
|
|
81
|
+
<key>WorkingDirectory</key>
|
|
82
|
+
<string>{dir}</string>
|
|
83
|
+
</dict>
|
|
84
|
+
</plist>""".format(
|
|
85
|
+
label=self.LABEL, description=comment, program=program_args_xml, interval=interval * 60, dir=os.getcwd()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
write_file(plist_path, plist_content)
|
|
89
|
+
result = try_run(["launchctl", "load", plist_path], logger=self.logger)
|
|
90
|
+
if result is not None:
|
|
91
|
+
self.logger.info("DDNS launchd service installed successfully")
|
|
92
|
+
return True
|
|
93
|
+
else:
|
|
94
|
+
self.logger.error("Failed to load launchd service")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def uninstall(self):
|
|
98
|
+
plist_path = self._get_plist_path()
|
|
99
|
+
try_run(["launchctl", "unload", plist_path], logger=self.logger) # Ignore errors
|
|
100
|
+
try:
|
|
101
|
+
os.remove(plist_path)
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
self.logger.info("DDNS launchd service uninstalled successfully")
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def enable(self):
|
|
109
|
+
plist_path = self._get_plist_path()
|
|
110
|
+
if not os.path.exists(plist_path):
|
|
111
|
+
self.logger.error("DDNS launchd service not found")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
result = try_run(["launchctl", "load", plist_path], logger=self.logger)
|
|
115
|
+
if result is not None:
|
|
116
|
+
self.logger.info("DDNS launchd service enabled successfully")
|
|
117
|
+
return True
|
|
118
|
+
else:
|
|
119
|
+
self.logger.error("Failed to enable launchd service")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def disable(self):
|
|
123
|
+
plist_path = self._get_plist_path()
|
|
124
|
+
result = try_run(["launchctl", "unload", plist_path], logger=self.logger)
|
|
125
|
+
if result is not None:
|
|
126
|
+
self.logger.info("DDNS launchd service disabled successfully")
|
|
127
|
+
return True
|
|
128
|
+
else:
|
|
129
|
+
self.logger.error("Failed to disable launchd service")
|
|
130
|
+
return False
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
schtasks-based task scheduler
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from ..util.try_run import try_run
|
|
12
|
+
from ._base import BaseScheduler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SchtasksScheduler(BaseScheduler):
|
|
16
|
+
"""schtasks-based task scheduler"""
|
|
17
|
+
|
|
18
|
+
NAME = "DDNS"
|
|
19
|
+
|
|
20
|
+
def _schtasks(self, *args):
|
|
21
|
+
"""Helper to run schtasks commands with consistent error handling"""
|
|
22
|
+
result = try_run(["schtasks"] + list(args), logger=self.logger)
|
|
23
|
+
return result is not None
|
|
24
|
+
|
|
25
|
+
def _extract_xml(self, xml_text, tag_name):
|
|
26
|
+
"""Extract XML tag content using regex for better performance and flexibility"""
|
|
27
|
+
pattern = r"<{0}[^>]*>(.*?)</{0}>".format(re.escape(tag_name))
|
|
28
|
+
match = re.search(pattern, xml_text, re.DOTALL)
|
|
29
|
+
return match.group(1).strip() if match else None
|
|
30
|
+
|
|
31
|
+
def is_installed(self):
|
|
32
|
+
result = try_run(["schtasks", "/query", "/tn", self.NAME], logger=self.logger) or ""
|
|
33
|
+
return self.NAME in result
|
|
34
|
+
|
|
35
|
+
def get_status(self):
|
|
36
|
+
# Use XML format for language-independent parsing
|
|
37
|
+
task_xml = try_run(["schtasks", "/query", "/tn", self.NAME, "/xml"], logger=self.logger)
|
|
38
|
+
status = {"scheduler": "schtasks", "installed": bool(task_xml)}
|
|
39
|
+
|
|
40
|
+
if not task_xml:
|
|
41
|
+
return status # Task not installed, return minimal status
|
|
42
|
+
|
|
43
|
+
status["enabled"] = self._extract_xml(task_xml, "Enabled") != "false"
|
|
44
|
+
command = self._extract_xml(task_xml, "Command")
|
|
45
|
+
arguments = self._extract_xml(task_xml, "Arguments")
|
|
46
|
+
status["command"] = "{} {}".format(command, arguments) if command and arguments else command
|
|
47
|
+
|
|
48
|
+
# Parse interval: PT10M -> 10, fallback to original string
|
|
49
|
+
interval_str = self._extract_xml(task_xml, "Interval")
|
|
50
|
+
interval_match = re.search(r"PT(\d+)M", interval_str) if interval_str else None
|
|
51
|
+
status["interval"] = int(interval_match.group(1)) if interval_match else interval_str
|
|
52
|
+
|
|
53
|
+
# Show description if exists, otherwise show installation date
|
|
54
|
+
description = self._extract_xml(task_xml, "Description") or self._extract_xml(task_xml, "Date")
|
|
55
|
+
if description:
|
|
56
|
+
status["description"] = description
|
|
57
|
+
return status
|
|
58
|
+
|
|
59
|
+
def install(self, interval, ddns_args=None):
|
|
60
|
+
# Build command line as array: prefer pythonw for script mode, or compiled exe directly
|
|
61
|
+
cmd_array = self._build_ddns_command(ddns_args)
|
|
62
|
+
workdir = os.getcwd()
|
|
63
|
+
|
|
64
|
+
# Split array into executable and arguments for schtasks XML format
|
|
65
|
+
# For Windows scheduler, prefer pythonw.exe to avoid console window
|
|
66
|
+
executable = cmd_array[0].replace("python.exe", "pythonw.exe")
|
|
67
|
+
arguments = self._quote_command_array(cmd_array[1:]) if len(cmd_array) > 1 else ""
|
|
68
|
+
|
|
69
|
+
# Create XML task definition with working directory support
|
|
70
|
+
description = self._get_description()
|
|
71
|
+
|
|
72
|
+
# Use template string to generate Windows Task Scheduler XML
|
|
73
|
+
xml_content = """<?xml version="1.0" encoding="UTF-16"?>
|
|
74
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
75
|
+
<RegistrationInfo>
|
|
76
|
+
<Description>{description}</Description>
|
|
77
|
+
</RegistrationInfo>
|
|
78
|
+
<Triggers>
|
|
79
|
+
<TimeTrigger>
|
|
80
|
+
<StartBoundary>1900-01-01T00:00:00</StartBoundary>
|
|
81
|
+
<Repetition>
|
|
82
|
+
<Interval>PT{interval}M</Interval>
|
|
83
|
+
</Repetition>
|
|
84
|
+
<Enabled>true</Enabled>
|
|
85
|
+
</TimeTrigger>
|
|
86
|
+
</Triggers>
|
|
87
|
+
<Actions>
|
|
88
|
+
<Exec>
|
|
89
|
+
<Command>{exe}</Command>
|
|
90
|
+
<Arguments>{arguments}</Arguments>
|
|
91
|
+
<WorkingDirectory>{dir}</WorkingDirectory>
|
|
92
|
+
</Exec>
|
|
93
|
+
</Actions>
|
|
94
|
+
<Settings>
|
|
95
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
96
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
97
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
98
|
+
</Settings>
|
|
99
|
+
</Task>""".format(description=description, interval=interval, exe=executable, arguments=arguments, dir=workdir)
|
|
100
|
+
|
|
101
|
+
# Write XML to temp file and use it to create task
|
|
102
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
|
|
103
|
+
f.write(xml_content)
|
|
104
|
+
xml_file = f.name
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
success = self._schtasks("/Create", "/XML", xml_file, "/TN", self.NAME, "/F")
|
|
108
|
+
return success
|
|
109
|
+
finally:
|
|
110
|
+
os.unlink(xml_file)
|
|
111
|
+
|
|
112
|
+
def uninstall(self):
|
|
113
|
+
success = self._schtasks("/Delete", "/TN", self.NAME, "/F")
|
|
114
|
+
return success
|
|
115
|
+
|
|
116
|
+
def enable(self):
|
|
117
|
+
return self._schtasks("/Change", "/TN", self.NAME, "/Enable")
|
|
118
|
+
|
|
119
|
+
def disable(self):
|
|
120
|
+
return self._schtasks("/Change", "/TN", self.NAME, "/Disable")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Systemd timer-based task scheduler for Linux
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from ..util.fileio import read_file_safely, write_file
|
|
11
|
+
from ..util.try_run import try_run
|
|
12
|
+
from ._base import BaseScheduler
|
|
13
|
+
|
|
14
|
+
try: # python 3
|
|
15
|
+
PermissionError # type: ignore
|
|
16
|
+
except NameError: # python 2 doesn't have PermissionError, use OSError instead
|
|
17
|
+
PermissionError = IOError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SystemdScheduler(BaseScheduler):
|
|
21
|
+
"""Systemd timer-based task scheduler for Linux"""
|
|
22
|
+
|
|
23
|
+
SERVICE_NAME = "ddns.service"
|
|
24
|
+
TIMER_NAME = "ddns.timer"
|
|
25
|
+
SERVICE_PATH = "/etc/systemd/system/ddns.service"
|
|
26
|
+
TIMER_PATH = "/etc/systemd/system/ddns.timer"
|
|
27
|
+
|
|
28
|
+
def _systemctl(self, *args):
|
|
29
|
+
"""Run systemctl command and return success status"""
|
|
30
|
+
result = try_run(["systemctl"] + list(args), logger=self.logger)
|
|
31
|
+
return result is not None
|
|
32
|
+
|
|
33
|
+
def is_installed(self):
|
|
34
|
+
"""Check if systemd timer files exist"""
|
|
35
|
+
return os.path.exists(self.SERVICE_PATH) and os.path.exists(self.TIMER_PATH)
|
|
36
|
+
|
|
37
|
+
def get_status(self):
|
|
38
|
+
"""Get comprehensive status information"""
|
|
39
|
+
installed = self.is_installed()
|
|
40
|
+
status = {"scheduler": "systemd", "installed": installed}
|
|
41
|
+
if not installed:
|
|
42
|
+
return status
|
|
43
|
+
|
|
44
|
+
# Check if timer is enabled
|
|
45
|
+
result = try_run(["systemctl", "is-enabled", self.TIMER_NAME], logger=self.logger)
|
|
46
|
+
status["enabled"] = bool(result and result.strip() == "enabled")
|
|
47
|
+
|
|
48
|
+
# Extract interval from timer file
|
|
49
|
+
timer_content = read_file_safely(self.TIMER_PATH) or ""
|
|
50
|
+
match = re.search(r"OnUnitActiveSec=(\d+)m", timer_content)
|
|
51
|
+
status["interval"] = int(match.group(1)) if match else None
|
|
52
|
+
|
|
53
|
+
# Extract command and description from service file
|
|
54
|
+
service_content = read_file_safely(self.SERVICE_PATH) or ""
|
|
55
|
+
match = re.search(r"ExecStart=(.+)", service_content)
|
|
56
|
+
status["command"] = match.group(1).strip() if match else None
|
|
57
|
+
desc_match = re.search(r"Description=(.+)", service_content)
|
|
58
|
+
status["description"] = desc_match.group(1).strip() if desc_match else None
|
|
59
|
+
|
|
60
|
+
return status
|
|
61
|
+
|
|
62
|
+
def install(self, interval, ddns_args=None):
|
|
63
|
+
"""Install systemd timer with specified interval"""
|
|
64
|
+
ddns_commands = self._build_ddns_command(ddns_args)
|
|
65
|
+
# Convert array to properly quoted command string for ExecStart
|
|
66
|
+
ddns_command = self._quote_command_array(ddns_commands)
|
|
67
|
+
work_dir = os.getcwd()
|
|
68
|
+
description = self._get_description()
|
|
69
|
+
|
|
70
|
+
# Create service file content
|
|
71
|
+
service_content = u"""[Unit]
|
|
72
|
+
Description={}
|
|
73
|
+
After=network.target
|
|
74
|
+
|
|
75
|
+
[Service]
|
|
76
|
+
Type=oneshot
|
|
77
|
+
WorkingDirectory={}
|
|
78
|
+
ExecStart={}
|
|
79
|
+
""".format(description, work_dir, ddns_command) # fmt: skip
|
|
80
|
+
|
|
81
|
+
# Create timer file content
|
|
82
|
+
timer_content = u"""[Unit]
|
|
83
|
+
Description=DDNS automatic IP update timer
|
|
84
|
+
Requires={}
|
|
85
|
+
|
|
86
|
+
[Timer]
|
|
87
|
+
OnUnitActiveSec={}m
|
|
88
|
+
Unit={}
|
|
89
|
+
|
|
90
|
+
[Install]
|
|
91
|
+
WantedBy=multi-user.target
|
|
92
|
+
""".format(self.SERVICE_NAME, interval, self.SERVICE_NAME) # fmt: skip
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Write service and timer files
|
|
96
|
+
write_file(self.SERVICE_PATH, service_content)
|
|
97
|
+
write_file(self.TIMER_PATH, timer_content)
|
|
98
|
+
except PermissionError as e:
|
|
99
|
+
self.logger.debug("Permission denied when writing systemd files: %s", e)
|
|
100
|
+
print("Permission denied. Please run as root or use sudo.")
|
|
101
|
+
print("or use cron scheduler (with --scheduler=cron) instead.")
|
|
102
|
+
return False
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.error("Failed to write systemd files: %s", e)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
if self._systemctl("daemon-reload") and self._systemctl("enable", self.TIMER_NAME):
|
|
108
|
+
self._systemctl("start", self.TIMER_NAME)
|
|
109
|
+
return True
|
|
110
|
+
else:
|
|
111
|
+
self.logger.error("Failed to enable/start systemd timer")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def uninstall(self):
|
|
115
|
+
"""Uninstall systemd timer and service"""
|
|
116
|
+
self.disable() # Stop and disable timer
|
|
117
|
+
# Remove systemd files
|
|
118
|
+
try:
|
|
119
|
+
os.remove(self.SERVICE_PATH)
|
|
120
|
+
os.remove(self.TIMER_PATH)
|
|
121
|
+
self._systemctl("daemon-reload") # Reload systemd configuration
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
except PermissionError as e:
|
|
125
|
+
self.logger.debug("Permission denied when removing systemd files: %s", e)
|
|
126
|
+
print("Permission denied. Please run as root or use sudo.")
|
|
127
|
+
return False
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self.logger.error("Failed to remove systemd files: %s", e)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def enable(self):
|
|
133
|
+
"""Enable and start systemd timer"""
|
|
134
|
+
return self._systemctl("enable", self.TIMER_NAME) and self._systemctl("start", self.TIMER_NAME)
|
|
135
|
+
|
|
136
|
+
def disable(self):
|
|
137
|
+
"""Disable and stop systemd timer"""
|
|
138
|
+
self._systemctl("stop", self.TIMER_NAME)
|
|
139
|
+
return self._systemctl("disable", self.TIMER_NAME)
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
ddns/__builtins__.pyi,sha256=9ZCOh51Aq6nBUZTt_0TBKpzcUeTxi-KBw-9TkXIGSH4,128
|
|
2
|
-
ddns/__init__.py,sha256=
|
|
3
|
-
ddns/__main__.py,sha256=
|
|
2
|
+
ddns/__init__.py,sha256=8cTinbrm5HpOYO2woZmVAJlWP1l3-epVwJrAHyrvCdo,258
|
|
3
|
+
ddns/__main__.py,sha256=gZjSw-FUxr4Z8l0h0hPpdp2B46ANiCtyAXAtL1myVa0,5423
|
|
4
4
|
ddns/cache.py,sha256=A1s3rnOJbPrGEjbGbbISrVg46lZFzZ84WPdzHaCtYBk,5971
|
|
5
5
|
ddns/ip.py,sha256=6H5jYv-TT-o0wSoFAGOlTAbf-TAS92Kx6vafi3LXTA4,5151
|
|
6
|
+
ddns/config/__init__.py,sha256=p8AoT2ZJ4LXAf0ij3i2zq4sq2rbIkv70lf_NQQVugKM,6871
|
|
7
|
+
ddns/config/cli.py,sha256=LcNXWTYimCCHm0kUVAb6ERdJVq9SsadEZyRiPSlT2PU,14447
|
|
8
|
+
ddns/config/config.py,sha256=Qhlz3UNcyQ8Evcp4T3xvM54LvMMD-wSEI4Xq3YgtGpY,7234
|
|
9
|
+
ddns/config/env.py,sha256=RSf4XfMN50zCJQ7QxLQE_4gQdNCiD-z3Gu8UCZHfqPM,2503
|
|
10
|
+
ddns/config/file.py,sha256=TzSU2qIgHKeJetkfM7doQVS16TfPcnD4ji-wOnp8ZRo,6450
|
|
6
11
|
ddns/provider/__init__.py,sha256=qAw-R-l7nUA9L96Tmr2n-T7_g8T9Mah7u615yg2zScY,2793
|
|
7
12
|
ddns/provider/_base.py,sha256=q31rqiiqEmM5Kt7SLdRjcREDrlGdRAQX6mNoe8ULPz8,20065
|
|
8
13
|
ddns/provider/_signature.py,sha256=fF8XxMDkXjia96d1gIVYWc72MVVe65AAnD7qr51qKXA,4140
|
|
9
14
|
ddns/provider/alidns.py,sha256=zymb1lNaj8yfUaQr8xfSxKojsLPfbo5orZcxBigcblM,6123
|
|
10
15
|
ddns/provider/aliesa.py,sha256=-4pBmV03NWot5CVMQYluDHChk-nwb1kQpmUdlZSLy6A,4722
|
|
11
16
|
ddns/provider/callback.py,sha256=QAOSWr-LWN8UG3NIEMWTvjax1g9R0rCJz6Xtr4KbQDo,2925
|
|
12
|
-
ddns/provider/cloudflare.py,sha256=
|
|
17
|
+
ddns/provider/cloudflare.py,sha256=_O_0H3mEehue8fBqQIhP_3hga71lb4or7SBQaezUMLE,5532
|
|
13
18
|
ddns/provider/debug.py,sha256=JKneEkOMasYxis9kcU97mHJMo3jdLRuU1w7AAdAqMhY,586
|
|
14
19
|
ddns/provider/dnscom.py,sha256=4r2ijL158dnSojj2WuLRWyxvUtGvibuowHvDzUdV-QU,3681
|
|
15
20
|
ddns/provider/dnspod.py,sha256=kgFsTULdooPDd6LmNt2cmLJjw8JRsmq8sfMpTs8REPc,4423
|
|
@@ -20,14 +25,20 @@ ddns/provider/huaweidns.py,sha256=upKKfBXYhQIzqxuoSRQYBXCx4lpHVjHJJQL0BS8rFsE,56
|
|
|
20
25
|
ddns/provider/namesilo.py,sha256=jgqkQUcHVCyVjYoxazmcN9Gu_Zd-EAv3aSnNmHHJ1jk,5843
|
|
21
26
|
ddns/provider/noip.py,sha256=RivoLhWJJJuiBzEbtRnnHiBkcLfbvAGglZKKY9s_lcY,4035
|
|
22
27
|
ddns/provider/tencentcloud.py,sha256=aEWqQ00EWUG-JWW2D0IuJDWZ1OdbTPheGeYSlIh7bqQ,7159
|
|
28
|
+
ddns/scheduler/__init__.py,sha256=yBoxs0cnCVdGKk4T5m1tZe75do-iH2806bq8ZHW-dG4,2454
|
|
29
|
+
ddns/scheduler/_base.py,sha256=PSWiDQ3FCb5eFT4ZO9zuPu2ADKswlLwhrpgc2AVLEoY,2813
|
|
30
|
+
ddns/scheduler/cron.py,sha256=X317yye2Kg_Ow9W1OOaPBipRmJTBZYetyM9CaqPkW6I,4372
|
|
31
|
+
ddns/scheduler/launchd.py,sha256=HP7G6K9CGtsqWcSGl25NFKquSv9Ak0oDEhxBSmyOr5Y,4648
|
|
32
|
+
ddns/scheduler/schtasks.py,sha256=P5APkh6eWwZh8AedYNq4HQWFajleTo-nezcZxP_NyLI,4608
|
|
33
|
+
ddns/scheduler/systemd.py,sha256=m3P_TJYMSgZZH_CndrfKVu0B3o7lXZw7D9oA5c2yguc,4960
|
|
23
34
|
ddns/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
35
|
ddns/util/comment.py,sha256=_B8sRpCJksNspsdld4ha03W1TAi-MyPRK7pL7ym0j9k,2199
|
|
25
36
|
ddns/util/fileio.py,sha256=bykJgmdy3gZ79iUQSVpMprRej1PGMJSDSHedIngFdG4,3169
|
|
26
37
|
ddns/util/http.py,sha256=qxfvpN0xt7GZ-GsHFhij4yjH0frWAC52XfaX5ZigFMw,12942
|
|
27
38
|
ddns/util/try_run.py,sha256=juDPxvT5xUabK2DiQNtFIyaATehtserHCVkP7_joSyU,1304
|
|
28
|
-
ddns-4.1.
|
|
29
|
-
ddns-4.1.
|
|
30
|
-
ddns-4.1.
|
|
31
|
-
ddns-4.1.
|
|
32
|
-
ddns-4.1.
|
|
33
|
-
ddns-4.1.
|
|
39
|
+
ddns-4.1.1b1.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
|
|
40
|
+
ddns-4.1.1b1.dist-info/METADATA,sha256=mp_lkZG2f-2elvfaieNvB8V_d2TITUgaOvLi-qGa4bg,19636
|
|
41
|
+
ddns-4.1.1b1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
42
|
+
ddns-4.1.1b1.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
|
|
43
|
+
ddns-4.1.1b1.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
|
|
44
|
+
ddns-4.1.1b1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|