* Copyright (C) 2013 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import s_bus from "./busnames.js";
import { systemd_client, clock_realtime_now } from "./services.jsx";
import { CockpitManagedMarker } from "./service-details.jsx";
export function from_boot_usec(value) {
const result = { delay: "system-boot" };
const seconds = Math.floor(value / 1e6);
if (seconds % 604800 === 0) {
result.delayNumber = seconds / 604800;
result.delayUnit = "weeks";
} else if (seconds % 3600 === 0) {
result.delayNumber = seconds / 3600;
result.delayUnit = "hours";
} else if (seconds % 60 === 0) {
result.delayNumber = seconds / 60;
result.delayUnit = "minutes";
} else {
result.delayNumber = seconds;
result.delayUnit = "seconds";
}
return result;
}
export function from_on_calendar(patterns) {
const joined = patterns.join('\n').trim();
let result = null;
const minutely = /^\*-\*-\* \*:\*:(\d{1,2}(?:,\d{1,2})*)$/;
const hourly = /^\*-\*-\* \*:(\d{1,2}(?:,\d{1,2})*)(?::00)?$/;
const daily = /^\*-\*-\* (\d{2}:\d{2})(?::00)?$/;
const weekly = /^([A-Za-z]{3}) \*-\*-\* (\d{2}:\d{2})(?::00)?$/;
const monthly = /^\*-\*-(\d{1,2}) (\d{2}:\d{2})(?::00)?$/;
const yearly = /^\*-(\d{2}-\d{2}) (\d{2}:\d{2})(?::00)?$/;
const specific = /^\d{4}-\d{2}-\d{2} (\d{2}:\d{2})(?::00)?$/;
if (minutely.test(joined)) {
const match = joined.match(minutely);
const seconds = match[1];
result = {
repeat: "minutely",
repeatPatterns: seconds.split(",").map(second => ({ second }))
};
}
if (hourly.test(joined)) {
const match = joined.match(hourly);
const minutes = match[1];
result = {
repeat: "hourly",
repeatPatterns: minutes.split(",").map(minute => ({ minute }))
};
}
if (patterns.every(line => daily.test(line))) {
result = {
repeat: "daily",
repeatPatterns: patterns.map(line => {
const match = line.match(daily);
const time = match[1];
return { time };
})
};
}
if (patterns.every(line => weekly.test(line))) {
result = {
repeat: "weekly",
repeatPatterns: patterns.map(line => {
const match = line.match(weekly);
const weekDay = match[1].toLowerCase();
const time = match[2];
return { day: weekDay, time };
})
};
}
if (patterns.every(line => monthly.test(line))) {
result = {
repeat: "monthly",
repeatPatterns: patterns.map(line => {
const match = line.match(monthly);
const day = Number(match[1]);
const time = match[2];
return { day, time };
})
};
}
if (patterns.every(line => yearly.test(line))) {
result = {
repeat: "yearly",
repeatPatterns: patterns.map((line) => {
const match = line.match(yearly);
const monthAndDay = match[1];
const time = match[2];
return { date: `${(new Date(clock_realtime_now)).getFullYear()}-${monthAndDay}`, time };
})
};
}
if (specific.test(joined)) {
result = {
repeat: "no",
repeatPatterns: [],
specificTime: joined.match(specific)[1]
};
}
if (result) {
result.delay = "specific-time";
result.repeatPatterns = result.repeatPatterns.map((item, index) => {
return { key: index, ...item };
});
return result;
} else {
return null;
}
}
ExecStart= line. We need to escape spaces, newlines, quotes, and
backslashes by prepending a backslash. We also need to escape
specifiers by prepending a "%" character.
*/
function escape_systemd_exec_arg(str) {
return str
.replaceAll("\\", "\\\\")
.replaceAll(" ", "\\s")
.replaceAll("\t", "\\t")
.replaceAll("\n", "\\n")
.replaceAll("\"", "\\\"")
.replaceAll("'", "\\'")
.replaceAll("%", "%%");
}
export function create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, repeatPatterns, specificTime, owner }) {
const timer_unit = {};
const repeat_array = repeatPatterns;
timer_unit.name = name.replace(/\s/g, '');
timer_unit.Description = description;
timer_unit.Command = "/bin/sh -c " + escape_systemd_exec_arg(command);
timer_unit.boot_time = delayNumber;
timer_unit.boot_time_unit = delayUnit;
function month_day_str(d) {
const month_str = (d.getMonth() + 1).toString();
const day_str = (d.getDate()).toString();
return `${month_str.padStart(2, '0')}-${day_str.padStart(2, '0')}`;
}
if (delay === "specific-time" && repeat === "no") {
const today = new Date(clock_realtime_now);
timer_unit.OnCalendar = `OnCalendar=${today.getFullYear()}-${month_day_str(today)} ${specificTime}:00`;
} else if (repeat === "minutely") {
timer_unit.repeat_second = repeat_array.map(item => Number(item.second));
timer_unit.OnCalendar = "OnCalendar=*-*-* *:*:" + timer_unit.repeat_second.join(",");
} else if (repeat === "hourly") {
timer_unit.repeat_minute = repeat_array.map(item => Number(item.minute));
timer_unit.OnCalendar = "OnCalendar=*-*-* *:" + timer_unit.repeat_minute.join(",");
} else if (repeat === "daily") {
timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-* ${item.time}:00`);
} else if (repeat === "weekly") {
timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=${item.day} *-*-* ${item.time}:00`);
} else if (repeat === "monthly") {
timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-${item.day} ${item.time}:00`);
} else if (repeat === "yearly") {
timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-${month_day_str(new Date(item.date))} ${item.time}:00`);
}
if (repeat !== "hourly" && repeat !== "minutely" && delay === "specific-time")
timer_unit.OnCalendar = timer_unit.OnCalendar.toString().replaceAll(",", "\n");
return create_timer_file({ timer_unit, delay, owner });
}
function create_timer_file({ timer_unit, delay, owner }) {
const unit = "[Unit]\nDescription=";
const service = "\n[Service]\nExecStart=";
const timer = "\n[Timer]\n";
const install = "[Install]\nWantedBy=timers.target\n";
const service_file = CockpitManagedMarker + unit + timer_unit.Description + service + timer_unit.Command + "\n";
let timer_file = CockpitManagedMarker;
if (delay === "system-boot") {
const boottimer = timer + "OnBootSec=" + timer_unit.boot_time + timer_unit.boot_time_unit + "\n";
timer_file += unit + timer_unit.Description + boottimer;
} else if (timer_unit.OnCalendar) {
const calendartimer = timer + timer_unit.OnCalendar + "\n";
timer_file += unit + timer_unit.Description + calendartimer;
}
timer_file += install;
const service_path = "/etc/systemd/system/" + timer_unit.name + ".service";
const timer_path = "/etc/systemd/system/" + timer_unit.name + ".timer";
return cockpit.file(service_path, { superuser: 'try' }).replace(service_file)
.then(() => cockpit.file(timer_path, { superuser: 'try' }).replace(timer_file))
.then(tag => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "EnableUnitFiles", [[timer_unit.name + ".timer"], false, false]))
* see https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl.c [enable_unit function] */
.then(() => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null))
.then(() => {
if (timer_unit.OnCalendar)
return systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "StartUnit", [timer_unit.name + ".timer", "replace"]);
});
}