Skip to content

Commit 7575480

Browse files
authored
Merge pull request #10 from ZPascal/issue-9
fix: Adjust the Cron process in general and include it inside supervisord
2 parents b04361e + 1f7ebb6 commit 7575480

File tree

6 files changed

+241
-7
lines changed

6 files changed

+241
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ You can execute `docker-compose up -d --build --force-recreate` to start and bui
77

88
### Cronjobs
99

10-
It is possible to adapt the `pretixuser` crontab entries by modifying the [crontab.bak](docker/pretix/crontab.bak) file.
10+
It is possible to adapt the `pretixuser` crontab entries by modifying the [crontab](docker/pretix/crontab.bak) file.
1111

1212
## Contribution
1313
If you would like to contribute something, have an improvement request, or want to make a change inside the code, please open a pull request.

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
volumes:
1313
- pretix_data:/data
1414
- ./docker/pretix/pretix.cfg:/etc/pretix/pretix.cfg
15+
- ./docker/pretix/crontab:/tmp/crontab
1516
ports:
1617
- "8000:80"
1718
networks:

docker/pretix/Dockerfile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ FROM pretix/standalone:stable
22

33
USER root
44

5-
RUN apt update && apt install cron nano -y
5+
ENV IMAGE_CRON_DIR="/image/cron"
66

7-
USER pretixuser
7+
ADD files /image
8+
COPY crontab /tmp/crontab
9+
10+
RUN mv /image/supervisord/crond.conf /etc/supervisord/crond.conf && \
11+
pip install crontab && chmod +x $IMAGE_CRON_DIR/cron.py
812

9-
COPY crontab.bak /tmp/crontab.bak
10-
RUN crontab /tmp/crontab.bak
13+
USER pretixuser
1114

12-
EXPOSE 80
1315
ENTRYPOINT ["pretix"]
1416
CMD ["all"]

docker/pretix/crontab.bak renamed to docker/pretix/crontab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
# For more information see the manual pages of crontab(5) and cron(8)
2222
#
2323
# m h dom mon dow command
24-
15,45 * * * * PRETIX_CONFIG_FILE=/etc/pretix/pretix.cfg python -m pretix runperiodic
24+
15,45 * * * * su pretixuser -c "PRETIX_CONFIG_FILE=/etc/pretix/pretix.cfg python -m pretix runperiodic"

docker/pretix/files/cron/cron.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/local/bin/python3
2+
3+
from crontab import CronTab
4+
import argparse
5+
import logging
6+
import time
7+
import subprocess
8+
import sys
9+
import signal
10+
import os
11+
12+
13+
def _parse_crontab(crontab_file: str) -> list:
14+
"""The method includes a functionality to parse the crontab file, and it returns a list of CronTab jobs
15+
16+
Keyword arguments:
17+
crontab_file -> Specify the inserted crontab file
18+
"""
19+
20+
logger = logging.getLogger("parser")
21+
22+
logger.info(f"Reading crontab from {crontab_file}")
23+
24+
if not os.path.isfile(crontab_file):
25+
logger.error(f"Crontab {crontab_file} does not exist. Exiting!")
26+
sys.exit(1)
27+
28+
with open(crontab_file, "r") as crontab:
29+
lines: list = crontab.readlines()
30+
31+
logger.info(f"{len(lines)} lines read from crontab {crontab_file}")
32+
33+
jobs: list = list()
34+
35+
for i, line in enumerate(lines):
36+
line: str = line.strip()
37+
38+
if not line:
39+
continue
40+
41+
if line.startswith("#"):
42+
continue
43+
44+
logger.info(f"Parsing line {line}")
45+
46+
expression: list = line.split(" ", 5)
47+
cron_expression: str = " ".join(expression[0:5])
48+
49+
logger.info(f"Cron expression is {cron_expression}")
50+
51+
try:
52+
cron_entry = CronTab(cron_expression)
53+
except ValueError as e:
54+
logger.critical(
55+
f"Unable to parse crontab. Line {i + 1}: Illegal cron expression {cron_expression}. Error message: {e}"
56+
)
57+
sys.exit(1)
58+
59+
command: str = expression[5]
60+
61+
logger.info(f"Command is {command}")
62+
63+
jobs.append([cron_entry, command])
64+
65+
if len(jobs) == 0:
66+
logger.error(
67+
"Specified crontab does not contain any scheduled execution. Exiting!"
68+
)
69+
sys.exit(1)
70+
71+
return jobs
72+
73+
74+
def _get_next_executions(jobs: list):
75+
"""The method includes a functionality to extract the execution time and job itself from the submitted job list
76+
77+
Keyword arguments:
78+
jobs -> Specify the inserted list of jobs
79+
"""
80+
81+
logger = logging.getLogger("next-exec")
82+
83+
scheduled_executions: tuple = tuple(
84+
(x[1], int(x[0].next(default_utc=True)) + 1) for x in jobs
85+
)
86+
87+
logger.debug(f"Next executions of scheduled are {scheduled_executions}")
88+
89+
next_exec_time: int = int(min(scheduled_executions, key=lambda x: x[1])[1])
90+
91+
logger.debug(f"Next execution is in {next_exec_time} second(s)")
92+
93+
next_commands: list = [x[0] for x in scheduled_executions if x[1] == next_exec_time]
94+
95+
logger.debug(
96+
f"Next commands to be executed in {next_exec_time} are {next_commands}"
97+
)
98+
99+
return next_exec_time, next_commands
100+
101+
102+
def _loop(jobs: list, test_mode: bool = False):
103+
"""The method includes a functionality to loop over all jobs inside the crontab file and execute them
104+
105+
Keyword arguments:
106+
jobs -> Specify the inserted jobs as list
107+
test_mode -> Specify if you want to use the test mode or not (default False)
108+
"""
109+
110+
logger = logging.getLogger("loop")
111+
112+
logger.info("Entering main loop")
113+
114+
if test_mode is False:
115+
while True:
116+
sleep_time, commands = _get_next_executions(jobs)
117+
118+
logger.debug(f"Sleeping for {sleep_time} second(s)")
119+
120+
if sleep_time <= 1:
121+
logger.debug("Sleep time <= 1 second, ignoring.")
122+
time.sleep(1)
123+
continue
124+
125+
time.sleep(sleep_time)
126+
127+
for command in commands:
128+
_execute_command(command)
129+
else:
130+
sleep_time, commands = _get_next_executions(jobs)
131+
132+
logger.debug(f"Sleeping for {sleep_time} second(s)")
133+
134+
if sleep_time <= 1:
135+
logger.debug("Sleep time <= 1 second, ignoring.")
136+
time.sleep(1)
137+
138+
time.sleep(sleep_time)
139+
140+
for command in commands:
141+
_execute_command(command)
142+
143+
144+
def _execute_command(command: str):
145+
"""The method includes a functionality to execute a crontab command
146+
147+
Keyword arguments:
148+
command -> Specify the inserted command for the execution
149+
"""
150+
151+
logger = logging.getLogger("exec")
152+
153+
logger.info(f"Executing command {command}")
154+
155+
result = subprocess.run(
156+
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
157+
)
158+
159+
logger.info(f"Standard output: {result.stdout}")
160+
logger.info(f"Standard error: {result.stderr}")
161+
162+
163+
def _signal_handler():
164+
"""The method includes a functionality for the signal handler to exit a process"""
165+
166+
logger = logging.getLogger("signal")
167+
logger.info("Exiting")
168+
sys.exit(0)
169+
170+
171+
def main():
172+
"""The method includes a functionality to control and execute crontab entries
173+
174+
Arguments:
175+
-c -> Specify the inserted crontab file
176+
-L -> Specify the inserted log file
177+
-C -> Specify the if the output should be forwarded to the console
178+
-l -> Specify the log level
179+
"""
180+
signal.signal(signal.SIGINT, _signal_handler)
181+
signal.signal(signal.SIGTERM, _signal_handler)
182+
183+
parser = argparse.ArgumentParser(description="cron")
184+
parser.add_argument("-c", "--crontab", required=True, type=str)
185+
logging_target = parser.add_mutually_exclusive_group(required=True)
186+
logging_target.add_argument("-L", "--logfile", type=str)
187+
logging_target.add_argument("-C", "--console", action="store_true")
188+
parser.add_argument(
189+
"-l",
190+
"--loglevel",
191+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
192+
default="INFO",
193+
type=str,
194+
)
195+
196+
args = parser.parse_args()
197+
198+
log_level = getattr(logging, args.loglevel.upper(), logging.INFO)
199+
200+
if args.console:
201+
logging.basicConfig(
202+
filemode="w",
203+
level=log_level,
204+
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
205+
)
206+
else:
207+
logging.basicConfig(
208+
filename=args.logfile,
209+
filemode="a+",
210+
level=log_level,
211+
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
212+
)
213+
214+
logger = logging.getLogger("main")
215+
216+
logger.info("Starting cron")
217+
218+
jobs: list = _parse_crontab(args.crontab)
219+
220+
_loop(jobs)
221+
222+
223+
if __name__ == "__main__":
224+
main()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[program:crond]
2+
command = %(ENV_IMAGE_CRON_DIR)s/cron.py --crontab /tmp/crontab --loglevel INFO --logfile /var/log/crond.log
3+
autostart = true
4+
redirect_stderr = true
5+
stdout_logfile = /var/log/crond.log
6+
stdout_logfile_maxbytes = 1MB
7+
stdout_logfile_backups = 2

0 commit comments

Comments
 (0)