LCOV - code coverage report
Current view: top level - pkg/systemd - timer-dialog-helpers.js Coverage Total Hit
Test: cockpit Lines: 92.6 % 176 163
Test Date: 2026-07-17 07:33:01

            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 : }
        

Generated by: LCOV version 2.0-1