UNPKG

at-bindings

Version:

A bindings library for datetime-based task scheduling on POSIX operating systems.

164 lines (138 loc) 4.34 kB
// @format const { execSync, spawnSync } = require("child_process"); const diffInSecs = require("date-fns/differenceInSeconds"); const parse = require("date-fns/parse"); // NOTE: This is the format `at` expects. const dateFormat = "+%OI:%M %p %m/%d/%y"; // NOTE: Since Mac OS X implements a different version of date, we check for // the user's platform here. We use coreutils's gdate when this software is // executed on a Mac. const dateTool = process.platform === "darwin" ? "gdate" : "date"; class ScheduleError extends Error { constructor(...params) { super(...params); if (Error.captureStackTrace) { Error.captureStackTrace(this, ScheduleError); } this.name = "ScheduleError"; } } class IndexError extends Error { constructor(...params) { super(...params); if (Error.captureStackTrace) { Error.captureStackTrace(this, IndexError); } this.name = "IndexError"; } } // WARN/TODO: We're not sanitizing any inputs here. function shift(datetime) { const cmd = `${dateTool} -d "${datetime}" "${dateFormat}"`; return execSync(cmd) .toString() .trim(); } function jobParser(output, type) { let pattern, res; switch (type) { case "create": pattern = new RegExp("job ([0-9]+) at (.+)"); const [_, id, date] = output.match(pattern); res = { id: parseInt(id, 10), date: { plain: new Date(date).toISOString(), obj: new Date(date) } }; break; case "list": // NOTE: In some cases, the OS also displays which user has scheduled a // task, which is usually appended with e.g. "as root". We account for // this in the following regexp: pattern = new RegExp("^([0-9]+)\t(.+?)(?= a |$)", "gm"); res = [...output.matchAll(pattern)]; res = res.map(([_, id, date]) => { return { id: parseInt(id, 10), // NOTE: By taking our parsed string date here and casting it to a // JS date without an information about a timezone, we implicitly // assume the machine's timezone configuration here. date: { plain: new Date(date).toISOString(), obj: new Date(date) } }; }); break; default: throw new Error("jobParser expects type to be 'create' or 'list'"); break; } return res; } function isPast(datetime) { const jsDate = parse(datetime, "hh:mm a MM/dd/yy", new Date()); const diff = diffInSecs(new Date(), jsDate); return diff >= 0; } function schedule(cmd, dateVal) { const datetime = shift(dateVal); if (isPast(datetime)) { throw new ScheduleError("schedule expectes a datetime in the future (#2)"); } // NOTE: Using a pipe with spawn: https://stackoverflow.com/a/39482554 const scheduleOut = spawnSync("sh", [ "-c", `echo "${cmd}" | at ${datetime}` ]).stderr.toString(); // NOTE: Potential error messages can contains carriage returns, so we're // trimming the strings here. if (scheduleOut.trim() === "at: trying to travel back in time".trim()) { throw new ScheduleError("schedule expectes a datetime in the future (#1)"); } const job = jobParser(scheduleOut, "create"); return job; } function list() { const out = execSync("at -l").toString(); const jobs = jobParser(out, "list"); return jobs; } function remove(jobId) { // NOTE: We check if the job exists as we'd like to provide a unified user // experience. By default, UNIX and Mac OS react differently to a non- // existing jobs. UNIX exits the process with an error, Mac OS doesn't. if (exists(jobId)) { execSync(`at -r ${jobId}`).toString(); } else { throw new IndexError(`Job with id: "${jobId}" doesn't exist`); } } function getContent(jobId) { let content; try { content = execSync(`at -c ${jobId}`).toString(); } catch (err) { if (err.toString().includes(`Cannot find jobid ${jobId}`)) { throw new IndexError(`Job with id: "${jobId}" doesn't exist`); } } if (!content) { throw new IndexError(`Job with id: "${jobId}" doesn't exist`); } return content; } function exists(jobId) { return list().find(job => job.id === jobId) !== undefined; } module.exports = { schedule, shift, list, remove, getContent, exists, ScheduleError, IndexError, jobParser, isPast };