Line data Source code
1 : /*
2 : * Copyright (C) 2013 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 33 : import cockpit from "cockpit";
7 : import s_bus from "./busnames.js";
8 : import { systemd_client, clock_realtime_now } from "./services.jsx";
9 : import { CockpitManagedMarker } from "./service-details.jsx";
10 :
11 1 : export function from_boot_usec(value) {
12 1 : const result = { delay: "system-boot" };
13 1 : const seconds = Math.floor(value / 1e6); // Convert from microseconds
14 :
15 0 : if (seconds % 604800 === 0) {
16 0 : result.delayNumber = seconds / 604800;
17 0 : result.delayUnit = "weeks";
18 0 : } else if (seconds % 3600 === 0) {
19 0 : result.delayNumber = seconds / 3600;
20 0 : result.delayUnit = "hours";
21 0 : } else if (seconds % 60 === 0) {
22 0 : result.delayNumber = seconds / 60;
23 0 : result.delayUnit = "minutes";
24 0 : } else {
25 1 : result.delayNumber = seconds;
26 1 : result.delayUnit = "seconds";
27 1 : }
28 :
29 1 : return result;
30 1 : }
31 :
32 1 : export function from_on_calendar(patterns) {
33 1 : const joined = patterns.join('\n').trim();
34 1 : let result = null;
35 :
36 1 : const minutely = /^\*-\*-\* \*:\*:(\d{1,2}(?:,\d{1,2})*)$/;
37 1 : const hourly = /^\*-\*-\* \*:(\d{1,2}(?:,\d{1,2})*)(?::00)?$/;
38 1 : const daily = /^\*-\*-\* (\d{2}:\d{2})(?::00)?$/;
39 1 : const weekly = /^([A-Za-z]{3}) \*-\*-\* (\d{2}:\d{2})(?::00)?$/;
40 1 : const monthly = /^\*-\*-(\d{1,2}) (\d{2}:\d{2})(?::00)?$/;
41 1 : const yearly = /^\*-(\d{2}-\d{2}) (\d{2}:\d{2})(?::00)?$/;
42 1 : const specific = /^\d{4}-\d{2}-\d{2} (\d{2}:\d{2})(?::00)?$/;
43 :
44 1 : if (minutely.test(joined)) {
45 1 : const match = joined.match(minutely);
46 1 : const seconds = match[1];
47 1 : result = {
48 1 : repeat: "minutely",
49 1 : repeatPatterns: seconds.split(",").map(second => ({ second }))
50 1 : };
51 1 : }
52 :
53 1 : if (hourly.test(joined)) {
54 1 : const match = joined.match(hourly);
55 1 : const minutes = match[1];
56 1 : result = {
57 1 : repeat: "hourly",
58 1 : repeatPatterns: minutes.split(",").map(minute => ({ minute }))
59 1 : };
60 1 : }
61 :
62 1 : if (patterns.every(line => daily.test(line))) {
63 1 : result = {
64 1 : repeat: "daily",
65 1 : repeatPatterns: patterns.map(line => {
66 1 : const match = line.match(daily);
67 1 : const time = match[1];
68 1 : return { time };
69 1 : })
70 1 : };
71 1 : }
72 :
73 1 : if (patterns.every(line => weekly.test(line))) {
74 1 : result = {
75 1 : repeat: "weekly",
76 1 : repeatPatterns: patterns.map(line => {
77 1 : const match = line.match(weekly);
78 1 : const weekDay = match[1].toLowerCase();
79 1 : const time = match[2];
80 1 : return { day: weekDay, time };
81 1 : })
82 1 : };
83 1 : }
84 :
85 1 : if (patterns.every(line => monthly.test(line))) {
86 1 : result = {
87 1 : repeat: "monthly",
88 1 : repeatPatterns: patterns.map(line => {
89 1 : const match = line.match(monthly);
90 1 : const day = Number(match[1]);
91 1 : const time = match[2];
92 1 : return { day, time };
93 1 : })
94 1 : };
95 1 : }
96 :
97 1 : if (patterns.every(line => yearly.test(line))) {
98 1 : result = {
99 1 : repeat: "yearly",
100 1 : repeatPatterns: patterns.map((line) => {
101 1 : const match = line.match(yearly);
102 1 : const monthAndDay = match[1];
103 1 : const time = match[2];
104 1 : return { date: `${(new Date(clock_realtime_now)).getFullYear()}-${monthAndDay}`, time }; // specific year isn't important
105 1 : })
106 1 : };
107 1 : }
108 :
109 1 : if (specific.test(joined)) {
110 1 : result = {
111 1 : repeat: "no",
112 1 : repeatPatterns: [],
113 1 : specificTime: joined.match(specific)[1]
114 1 : };
115 1 : }
116 :
117 1 : if (result) {
118 1 : result.delay = "specific-time";
119 1 : result.repeatPatterns = result.repeatPatterns.map((item, index) => {
120 1 : return { key: index, ...item };
121 1 : });
122 :
123 1 : return result;
124 0 : } else {
125 0 : return null;
126 0 : }
127 1 : }
128 :
129 : /* Escape STR so that it is parsed as a single argument in a
130 : ExecStart= line. We need to escape spaces, newlines, quotes, and
131 : backslashes by prepending a backslash. We also need to escape
132 : specifiers by prepending a "%" character.
133 : */
134 :
135 2 : function escape_systemd_exec_arg(str) {
136 2 : return str
137 2 : .replaceAll("\\", "\\\\")
138 2 : .replaceAll(" ", "\\s")
139 2 : .replaceAll("\t", "\\t")
140 2 : .replaceAll("\n", "\\n")
141 2 : .replaceAll("\"", "\\\"")
142 2 : .replaceAll("'", "\\'")
143 2 : .replaceAll("%", "%%");
144 2 : }
145 :
146 2 : export function create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, repeatPatterns, specificTime, owner }) {
147 2 : const timer_unit = {};
148 2 : const repeat_array = repeatPatterns;
149 2 : timer_unit.name = name.replace(/\s/g, '');
150 2 : timer_unit.Description = description;
151 2 : timer_unit.Command = "/bin/sh -c " + escape_systemd_exec_arg(command);
152 2 : timer_unit.boot_time = delayNumber;
153 2 : timer_unit.boot_time_unit = delayUnit;
154 :
155 2 : function month_day_str(d) {
156 2 : const month_str = (d.getMonth() + 1).toString();
157 2 : const day_str = (d.getDate()).toString();
158 2 : return `${month_str.padStart(2, '0')}-${day_str.padStart(2, '0')}`;
159 2 : }
160 :
161 2 : if (delay == "specific-time" && repeat == "no") {
162 2 : const today = new Date(clock_realtime_now);
163 2 : timer_unit.OnCalendar = `OnCalendar=${today.getFullYear()}-${month_day_str(today)} ${specificTime}:00`;
164 2 : } else if (repeat == "minutely") {
165 2 : timer_unit.repeat_second = repeat_array.map(item => Number(item.second));
166 2 : timer_unit.OnCalendar = "OnCalendar=*-*-* *:*:" + timer_unit.repeat_second.join(",");
167 2 : } else if (repeat == "hourly") {
168 2 : timer_unit.repeat_minute = repeat_array.map(item => Number(item.minute));
169 2 : timer_unit.OnCalendar = "OnCalendar=*-*-* *:" + timer_unit.repeat_minute.join(",");
170 2 : } else if (repeat == "daily") {
171 2 : timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-* ${item.time}:00`);
172 2 : } else if (repeat == "weekly") {
173 2 : timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=${item.day} *-*-* ${item.time}:00`);
174 2 : } else if (repeat == "monthly") {
175 2 : timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-${item.day} ${item.time}:00`);
176 2 : } else if (repeat == "yearly") {
177 2 : timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-${month_day_str(new Date(item.date))} ${item.time}:00`);
178 2 : }
179 2 : if (repeat != "hourly" && repeat != "minutely" && delay == "specific-time")
180 2 : timer_unit.OnCalendar = timer_unit.OnCalendar.toString().replaceAll(",", "\n");
181 2 : return create_timer_file({ timer_unit, delay, owner });
182 2 : }
183 :
184 2 : function create_timer_file({ timer_unit, delay, owner }) {
185 2 : const unit = "[Unit]\nDescription=";
186 2 : const service = "\n[Service]\nExecStart=";
187 2 : const timer = "\n[Timer]\n";
188 2 : const install = "[Install]\nWantedBy=timers.target\n";
189 2 : const service_file = CockpitManagedMarker + unit + timer_unit.Description + service + timer_unit.Command + "\n";
190 2 : let timer_file = CockpitManagedMarker;
191 2 : if (delay == "system-boot") {
192 2 : const boottimer = timer + "OnBootSec=" + timer_unit.boot_time + timer_unit.boot_time_unit + "\n";
193 2 : timer_file += unit + timer_unit.Description + boottimer;
194 2 : } else if (timer_unit.OnCalendar) {
195 2 : const calendartimer = timer + timer_unit.OnCalendar + "\n";
196 2 : timer_file += unit + timer_unit.Description + calendartimer;
197 2 : }
198 2 : timer_file += install;
199 : // writing to file
200 2 : const service_path = "/etc/systemd/system/" + timer_unit.name + ".service";
201 2 : const timer_path = "/etc/systemd/system/" + timer_unit.name + ".timer";
202 2 : return cockpit.file(service_path, { superuser: 'try' }).replace(service_file)
203 2 : .then(() => cockpit.file(timer_path, { superuser: 'try' }).replace(timer_file))
204 2 : .then(tag => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "EnableUnitFiles", [[timer_unit.name + ".timer"], false, false]))
205 : /* Executing daemon reload after file operations is necessary -
206 : * see https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl.c [enable_unit function] */
207 2 : .then(() => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null))
208 2 : .then(() => {
209 : // start calendar timers
210 2 : if (timer_unit.OnCalendar)
211 2 : return systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "StartUnit", [timer_unit.name + ".timer", "replace"]);
212 2 : });
213 2 : }
|