/* * 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); // Convert from microseconds 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 }; // specific year isn't important }) }; } 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; } } /* Escape STR so that it is parsed as a single argument in a 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; // writing to file 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])) /* Executing daemon reload after file operations is necessary - * 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(() => { // start calendar timers if (timer_unit.OnCalendar) return systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "StartUnit", [timer_unit.name + ".timer", "replace"]); }); }