Node-Red configuration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cron.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. (function(root, factory) {
  2. if (typeof define === 'function' && define.amd) {
  3. define(['moment-timezone'], factory);
  4. } else if (typeof exports === 'object') {
  5. module.exports = factory(
  6. require('moment-timezone'),
  7. require('child_process')
  8. );
  9. } else {
  10. root.Cron = factory(root.moment);
  11. }
  12. })(this, function(moment, childProcess) {
  13. var exports = {};
  14. var timeUnits = [
  15. 'second',
  16. 'minute',
  17. 'hour',
  18. 'dayOfMonth',
  19. 'month',
  20. 'dayOfWeek'
  21. ];
  22. var spawn = childProcess && childProcess.spawn;
  23. function CronTime(source, zone, utcOffset) {
  24. this.source = source;
  25. if (zone) {
  26. if (moment.tz.names().indexOf(zone) === -1) {
  27. throw new Error('Invalid timezone.');
  28. }
  29. this.zone = zone;
  30. }
  31. if (typeof utcOffset !== 'undefined') this.utcOffset = utcOffset;
  32. var that = this;
  33. timeUnits.map(function(timeUnit) {
  34. that[timeUnit] = {};
  35. });
  36. if (this.source instanceof Date || this.source._isAMomentObject) {
  37. this.source = moment(this.source);
  38. this.realDate = true;
  39. } else {
  40. this._parse();
  41. this._verifyParse();
  42. }
  43. }
  44. CronTime.constraints = [[0, 59], [0, 59], [0, 23], [1, 31], [0, 11], [0, 6]];
  45. CronTime.monthConstraints = [
  46. 31,
  47. 29, // support leap year...not perfect
  48. 31,
  49. 30,
  50. 31,
  51. 30,
  52. 31,
  53. 31,
  54. 30,
  55. 31,
  56. 30,
  57. 31
  58. ];
  59. CronTime.parseDefaults = ['0', '*', '*', '*', '*', '*'];
  60. CronTime.aliases = {
  61. jan: 0,
  62. feb: 1,
  63. mar: 2,
  64. apr: 3,
  65. may: 4,
  66. jun: 5,
  67. jul: 6,
  68. aug: 7,
  69. sep: 8,
  70. oct: 9,
  71. nov: 10,
  72. dec: 11,
  73. sun: 0,
  74. mon: 1,
  75. tue: 2,
  76. wed: 3,
  77. thu: 4,
  78. fri: 5,
  79. sat: 6
  80. };
  81. CronTime.prototype = {
  82. _verifyParse: function() {
  83. var months = Object.keys(this.month);
  84. var ok = false;
  85. /* if a dayOfMonth is not found in all months, we only need to fix the last
  86. wrong month to prevent infinite loop */
  87. var lastWrongMonth = NaN;
  88. for (var i = 0; i < months.length; i++) {
  89. var m = months[i];
  90. var con = CronTime.monthConstraints[parseInt(m, 10)];
  91. var dsom = Object.keys(this.dayOfMonth);
  92. for (var j = 0; j < dsom.length; j++) {
  93. var dom = dsom[j];
  94. if (dom <= con) {
  95. ok = true;
  96. }
  97. }
  98. if (!ok) {
  99. // save the month in order to be fixed if all months fails (infinite loop)
  100. lastWrongMonth = m;
  101. console.warn("Month '" + m + "' is limited to '" + con + "' days.");
  102. }
  103. }
  104. // infinite loop detected (dayOfMonth is not found in all months)
  105. if (!ok) {
  106. var con = CronTime.monthConstraints[parseInt(lastWrongMonth, 10)];
  107. var dsom = Object.keys(this.dayOfMonth);
  108. for (var k = 0; k < dsom.length; k++) {
  109. var dom = dsom[k];
  110. if (dom > con) {
  111. delete this.dayOfMonth[dom];
  112. var fixedDay = Number(dom) % con;
  113. this.dayOfMonth[fixedDay] = true;
  114. }
  115. }
  116. }
  117. },
  118. /**
  119. * calculates the next send time
  120. */
  121. sendAt: function(i) {
  122. var date = this.realDate ? this.source : moment();
  123. // Set the timezone if given (http://momentjs.com/timezone/docs/#/using-timezones/parsing-in-zone/)
  124. if (this.zone) {
  125. date = date.tz(this.zone);
  126. }
  127. if (typeof this.utcOffset !== 'undefined') {
  128. date = date.utcOffset(this.utcOffset);
  129. }
  130. if (this.realDate) {
  131. const diff = moment().diff(date, 's');
  132. if (diff > 0) {
  133. throw new Error('WARNING: Date in past. Will never be fired.');
  134. }
  135. return date;
  136. }
  137. // If the i argument is not given, return the next send time
  138. if (isNaN(i) || i < 0) {
  139. date = this._getNextDateFrom(date);
  140. return date;
  141. } else {
  142. // Else return the next i send times
  143. var dates = [];
  144. for (; i > 0; i--) {
  145. date = this._getNextDateFrom(date);
  146. dates.push(moment(date));
  147. }
  148. return dates;
  149. }
  150. },
  151. /**
  152. * Get the number of milliseconds in the future at which to fire our callbacks.
  153. */
  154. getTimeout: function() {
  155. return Math.max(-1, this.sendAt() - moment());
  156. },
  157. /**
  158. * writes out a cron string
  159. */
  160. toString: function() {
  161. return this.toJSON().join(' ');
  162. },
  163. /**
  164. * Json representation of the parsed cron syntax.
  165. */
  166. toJSON: function() {
  167. var self = this;
  168. return timeUnits.map(function(timeName) {
  169. return self._wcOrAll(timeName);
  170. });
  171. },
  172. /**
  173. * get next date that matches parsed cron time
  174. */
  175. _getNextDateFrom: function(start, zone) {
  176. var date;
  177. var firstDate = moment(start).valueOf();
  178. if (zone) {
  179. date = moment(start).tz(zone);
  180. } else {
  181. date = moment(start);
  182. }
  183. if (!this.realDate) {
  184. const milliseconds =
  185. (start.milliseconds && start.milliseconds()) ||
  186. (start.getMilliseconds && start.getMilliseconds()) ||
  187. 0;
  188. if (milliseconds > 0) {
  189. date.milliseconds(0);
  190. date.seconds(date.seconds() + 1);
  191. }
  192. }
  193. if (date.toString() === 'Invalid date') {
  194. throw new Error('ERROR: You specified an invalid date.');
  195. }
  196. // it shouldn't take more than 5 seconds to find the next execution time
  197. // being very generous with this. Throw error if it takes too long to find the next time to protect from
  198. // infinite loop.
  199. var timeout = Date.now() + 5000;
  200. // determine next date
  201. while (true) {
  202. var diff = date - start;
  203. var prevMonth = date.month();
  204. var prevDay = date.days();
  205. var prevMinute = date.minutes();
  206. var prevSeconds = date.seconds();
  207. var origDate = new Date(date);
  208. if (Date.now() > timeout) {
  209. throw new Error(
  210. `Something went wrong. cron reached maximum iterations.
  211. Please open an issue (https://github.com/kelektiv/node-cron/issues/new) and provide the following string
  212. Time Zone: ${zone || '""'} - Cron String: ${this} - UTC offset: ${date.format(
  213. 'Z'
  214. )} - current Date: ${moment().toString()}`
  215. );
  216. }
  217. if (
  218. !(date.month() in this.month) &&
  219. Object.keys(this.month).length !== 12
  220. ) {
  221. date.add(1, 'M');
  222. if (date.month() === prevMonth) {
  223. date.add(1, 'M');
  224. }
  225. date.date(1);
  226. date.hours(0);
  227. date.minutes(0);
  228. date.seconds(0);
  229. continue;
  230. }
  231. if (
  232. !(date.date() in this.dayOfMonth) &&
  233. Object.keys(this.dayOfMonth).length !== 31 &&
  234. !(
  235. date.day() in this.dayOfWeek &&
  236. Object.keys(this.dayOfWeek).length !== 7
  237. )
  238. ) {
  239. date.add(1, 'd');
  240. if (date.days() === prevDay) {
  241. date.add(1, 'd');
  242. }
  243. date.hours(0);
  244. date.minutes(0);
  245. date.seconds(0);
  246. continue;
  247. }
  248. if (
  249. !(date.day() in this.dayOfWeek) &&
  250. Object.keys(this.dayOfWeek).length !== 7 &&
  251. !(
  252. date.date() in this.dayOfMonth &&
  253. Object.keys(this.dayOfMonth).length !== 31
  254. )
  255. ) {
  256. date.add(1, 'd');
  257. if (date.days() === prevDay) {
  258. date.add(1, 'd');
  259. }
  260. date.hours(0);
  261. date.minutes(0);
  262. date.seconds(0);
  263. if (date <= origDate) {
  264. date = this._findDST(origDate);
  265. }
  266. continue;
  267. }
  268. if (
  269. !(date.hours() in this.hour) &&
  270. Object.keys(this.hour).length !== 24
  271. ) {
  272. origDate = moment(date);
  273. var curHour = date.hours();
  274. date.hours(
  275. date.hours() === 23 && diff > 86400000 ? 0 : date.hours() + 1
  276. );
  277. /*
  278. * Moment Date will not allow you to set the time to 2 AM if there is no 2 AM (on the day we change the clock)
  279. * We will therefore jump to 3AM if time stayed at 1AM
  280. */
  281. if (curHour === date.hours()) {
  282. date.hours(date.hours() + 2);
  283. }
  284. date.minutes(0);
  285. date.seconds(0);
  286. if (date <= origDate) {
  287. date = this._findDST(origDate);
  288. }
  289. continue;
  290. }
  291. if (
  292. !(date.minutes() in this.minute) &&
  293. Object.keys(this.minute).length !== 60
  294. ) {
  295. origDate = moment(date);
  296. date.minutes(
  297. date.minutes() === 59 && diff > 60 * 60 * 1000
  298. ? 0
  299. : date.minutes() + 1
  300. );
  301. date.seconds(0);
  302. if (date <= origDate) {
  303. date = this._findDST(origDate);
  304. }
  305. continue;
  306. }
  307. if (
  308. !(date.seconds() in this.second) &&
  309. Object.keys(this.second).length !== 60
  310. ) {
  311. origDate = moment(date);
  312. date.seconds(
  313. date.seconds() === 59 && diff > 60 * 1000 ? 0 : date.seconds() + 1
  314. );
  315. if (date <= origDate) {
  316. date = this._findDST(origDate);
  317. }
  318. continue;
  319. }
  320. if (date.valueOf() === firstDate) {
  321. date.seconds(date.seconds() + 1);
  322. continue;
  323. }
  324. break;
  325. }
  326. return date;
  327. },
  328. /**
  329. * get next date that is a valid DST date
  330. */
  331. _findDST: function(date) {
  332. var newDate = moment(date);
  333. while (newDate <= date) {
  334. // eslint seems to trigger here, it is wrong
  335. newDate.add(1, 's');
  336. }
  337. return newDate;
  338. },
  339. /**
  340. * wildcard, or all params in array (for to string)
  341. */
  342. _wcOrAll: function(type) {
  343. if (this._hasAll(type)) return '*';
  344. var all = [];
  345. for (var time in this[type]) {
  346. all.push(time);
  347. }
  348. return all.join(',');
  349. },
  350. _hasAll: function(type) {
  351. var constrain = CronTime.constraints[timeUnits.indexOf(type)];
  352. for (var i = constrain[0], n = constrain[1]; i < n; i++) {
  353. if (!(i in this[type])) return false;
  354. }
  355. return true;
  356. },
  357. _parse: function() {
  358. var aliases = CronTime.aliases;
  359. var source = this.source.replace(/[a-z]{1,3}/gi, function(alias) {
  360. alias = alias.toLowerCase();
  361. if (alias in aliases) {
  362. return aliases[alias];
  363. }
  364. throw new Error('Unknown alias: ' + alias);
  365. });
  366. var split = source.replace(/^\s\s*|\s\s*$/g, '').split(/\s+/);
  367. var cur;
  368. var i = 0;
  369. var len = timeUnits.length;
  370. // seconds are optional
  371. if (split.length < timeUnits.length - 1) {
  372. throw new Error('Too few fields');
  373. }
  374. if (split.length > timeUnits.length) {
  375. throw new Error('Too many fields');
  376. }
  377. for (; i < timeUnits.length; i++) {
  378. // If the split source string doesn't contain all digits,
  379. // assume defaults for first n missing digits.
  380. // This adds support for 5-digit standard cron syntax
  381. cur = split[i - (len - split.length)] || CronTime.parseDefaults[i];
  382. this._parseField(cur, timeUnits[i], CronTime.constraints[i]);
  383. }
  384. },
  385. _parseField: function(field, type, constraints) {
  386. var rangePattern = /^(\d+)(?:-(\d+))?(?:\/(\d+))?$/g;
  387. var typeObj = this[type];
  388. var pointer;
  389. var low = constraints[0];
  390. var high = constraints[1];
  391. var fields = field.split(',');
  392. fields.forEach(function(field) {
  393. var wildcardIndex = field.indexOf('*');
  394. if (wildcardIndex !== -1 && wildcardIndex !== 0) {
  395. throw new Error('Field (' + field + ') has an invalid wildcard expression');
  396. }
  397. });
  398. // * is a shortcut to [lower-upper] range
  399. field = field.replace(/\*/g, low + '-' + high);
  400. // commas separate information, so split based on those
  401. var allRanges = field.split(',');
  402. for (var i = 0; i < allRanges.length; i++) {
  403. if (allRanges[i].match(rangePattern)) {
  404. allRanges[i].replace(rangePattern, function($0, lower, upper, step) {
  405. lower = parseInt(lower, 10);
  406. upper = parseInt(upper, 10) || undefined;
  407. const wasStepDefined = !isNaN(parseInt(step, 10));
  408. if (step === '0') {
  409. throw new Error('Field (' + field + ') has a step of zero');
  410. }
  411. step = parseInt(step, 10) || 1;
  412. if (upper && lower > upper) {
  413. throw new Error('Field (' + field + ') has an invalid range');
  414. }
  415. const outOfRangeError =
  416. lower < low ||
  417. (upper && upper > high) ||
  418. (!upper && lower > high);
  419. if (outOfRangeError) {
  420. throw new Error('Field (' + field + ') value is out of range');
  421. }
  422. // Positive integer higher than constraints[0]
  423. lower = Math.min(Math.max(low, ~~Math.abs(lower)), high);
  424. // Positive integer lower than constraints[1]
  425. if (upper) {
  426. upper = Math.min(high, ~~Math.abs(upper));
  427. } else {
  428. // If step is provided, the default upper range is the highest value
  429. upper = wasStepDefined ? high : lower;
  430. }
  431. // Count from the lower barrier to the upper
  432. pointer = lower;
  433. do {
  434. typeObj[pointer] = true;
  435. pointer += step;
  436. } while (pointer <= upper);
  437. });
  438. } else {
  439. throw new Error('Field (' + field + ') cannot be parsed');
  440. }
  441. }
  442. }
  443. };
  444. function command2function(cmd) {
  445. var command;
  446. var args;
  447. switch (typeof cmd) {
  448. case 'string':
  449. args = cmd.split(' ');
  450. command = args.shift();
  451. cmd = spawn.bind(undefined, command, args);
  452. break;
  453. case 'object':
  454. command = cmd && cmd.command;
  455. if (command) {
  456. args = cmd.args;
  457. var options = cmd.options;
  458. cmd = spawn.bind(undefined, command, args, options);
  459. }
  460. break;
  461. }
  462. return cmd;
  463. }
  464. function CronJob(
  465. cronTime,
  466. onTick,
  467. onComplete,
  468. startNow,
  469. timeZone,
  470. context,
  471. runOnInit,
  472. utcOffset,
  473. unrefTimeout
  474. ) {
  475. var _cronTime = cronTime;
  476. var argCount = 0;
  477. for (var i = 0; i < arguments.length; i++) {
  478. if (arguments[i] !== undefined) {
  479. argCount++;
  480. }
  481. }
  482. if (typeof cronTime !== 'string' && argCount === 1) {
  483. // crontime is an object...
  484. onTick = cronTime.onTick;
  485. onComplete = cronTime.onComplete;
  486. context = cronTime.context;
  487. startNow = cronTime.start || cronTime.startNow || cronTime.startJob;
  488. timeZone = cronTime.timeZone;
  489. runOnInit = cronTime.runOnInit;
  490. _cronTime = cronTime.cronTime;
  491. utcOffset = cronTime.utcOffset;
  492. unrefTimeout = cronTime.unrefTimeout;
  493. }
  494. this.context = context || this;
  495. this._callbacks = [];
  496. this.onComplete = command2function(onComplete);
  497. this.cronTime = new CronTime(_cronTime, timeZone, utcOffset);
  498. this.unrefTimeout = unrefTimeout;
  499. addCallback.call(this, command2function(onTick));
  500. if (runOnInit) {
  501. this.lastExecution = new Date();
  502. fireOnTick.call(this);
  503. }
  504. if (startNow) {
  505. start.call(this);
  506. }
  507. return this;
  508. }
  509. var addCallback = function(callback) {
  510. if (typeof callback === 'function') this._callbacks.push(callback);
  511. };
  512. CronJob.prototype.addCallback = addCallback;
  513. CronJob.prototype.setTime = function(time) {
  514. if (!(time instanceof CronTime))
  515. throw new Error('time must be an instance of CronTime.');
  516. this.stop();
  517. this.cronTime = time;
  518. };
  519. CronJob.prototype.nextDate = function() {
  520. return this.cronTime.sendAt();
  521. };
  522. var fireOnTick = function() {
  523. for (var i = this._callbacks.length - 1; i >= 0; i--)
  524. this._callbacks[i].call(this.context, this.onComplete);
  525. };
  526. CronJob.prototype.fireOnTick = fireOnTick;
  527. CronJob.prototype.nextDates = function(i) {
  528. return this.cronTime.sendAt(i);
  529. };
  530. var start = function() {
  531. if (this.running) return;
  532. var MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
  533. var self = this;
  534. var timeout = this.cronTime.getTimeout();
  535. var remaining = 0;
  536. var startTime;
  537. if (this.cronTime.realDate) this.runOnce = true;
  538. function _setTimeout(timeout) {
  539. startTime = Date.now();
  540. self._timeout = setTimeout(callbackWrapper, timeout);
  541. if (self.unrefTimeout && typeof self._timeout.unref === 'function') {
  542. self._timeout.unref();
  543. }
  544. }
  545. // The callback wrapper checks if it needs to sleep another period or not
  546. // and does the real callback logic when it's time.
  547. function callbackWrapper() {
  548. var diff = startTime + timeout - Date.now();
  549. if (diff > 0) {
  550. var newTimeout = self.cronTime.getTimeout();
  551. if (newTimeout > diff) {
  552. newTimeout = diff;
  553. }
  554. remaining += newTimeout;
  555. }
  556. // If there is sleep time remaining, calculate how long and go to sleep
  557. // again. This processing might make us miss the deadline by a few ms
  558. // times the number of sleep sessions. Given a MAXDELAY of almost a
  559. // month, this should be no issue.
  560. self.lastExecution = new Date();
  561. if (remaining) {
  562. if (remaining > MAXDELAY) {
  563. remaining -= MAXDELAY;
  564. timeout = MAXDELAY;
  565. } else {
  566. timeout = remaining;
  567. remaining = 0;
  568. }
  569. _setTimeout(timeout);
  570. } else {
  571. // We have arrived at the correct point in time.
  572. self.running = false;
  573. // start before calling back so the callbacks have the ability to stop the cron job
  574. if (!self.runOnce) self.start();
  575. self.fireOnTick();
  576. }
  577. }
  578. if (timeout >= 0) {
  579. this.running = true;
  580. // Don't try to sleep more than MAXDELAY ms at a time.
  581. if (timeout > MAXDELAY) {
  582. remaining = timeout - MAXDELAY;
  583. timeout = MAXDELAY;
  584. }
  585. _setTimeout(timeout);
  586. } else {
  587. this.stop();
  588. }
  589. };
  590. CronJob.prototype.start = start;
  591. CronJob.prototype.lastDate = function() {
  592. return this.lastExecution;
  593. };
  594. /**
  595. * Stop the cronjob.
  596. */
  597. CronJob.prototype.stop = function() {
  598. if (this._timeout) clearTimeout(this._timeout);
  599. this.running = false;
  600. if (typeof this.onComplete === 'function') this.onComplete();
  601. };
  602. exports.job = function(
  603. cronTime,
  604. onTick,
  605. onComplete,
  606. startNow,
  607. timeZone,
  608. context,
  609. runOnInit,
  610. utcOffset,
  611. unrefTimeout
  612. ) {
  613. return new CronJob(
  614. cronTime,
  615. onTick,
  616. onComplete,
  617. startNow,
  618. timeZone,
  619. context,
  620. runOnInit,
  621. utcOffset,
  622. unrefTimeout
  623. );
  624. };
  625. exports.time = function(cronTime, timeZone) {
  626. return new CronTime(cronTime, timeZone);
  627. };
  628. exports.sendAt = function(cronTime) {
  629. return exports.time(cronTime).sendAt();
  630. };
  631. exports.timeout = function(cronTime) {
  632. return exports.time(cronTime).getTimeout();
  633. };
  634. exports.CronJob = CronJob;
  635. exports.CronTime = CronTime;
  636. return exports;
  637. });