agent.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // ===========================================
  2. // REQUARKS WIKI - Background Agent
  3. // 1.0.0
  4. // Licensed under AGPLv3
  5. // ===========================================
  6. global.ROOTPATH = __dirname;
  7. global.PROCNAME = 'AGENT';
  8. // ----------------------------------------
  9. // Load Winston
  10. // ----------------------------------------
  11. var _isDebug = process.env.NODE_ENV === 'development';
  12. global.winston = require('./lib/winston')(_isDebug);
  13. // ----------------------------------------
  14. // Fetch internal handshake key
  15. // ----------------------------------------
  16. if(!process.argv[2] || process.argv[2].length !== 40) {
  17. winston.error('[WS] Illegal process start. Missing handshake key.');
  18. process.exit(1);
  19. }
  20. global.WSInternalKey = process.argv[2];
  21. // ----------------------------------------
  22. // Load global modules
  23. // ----------------------------------------
  24. winston.info('[AGENT] Background Agent is initializing...');
  25. var appconfig = require('./models/config')('./config.yml');
  26. global.db = require('./models/mongo').init(appconfig);
  27. global.upl = require('./models/agent/uploads').init(appconfig);
  28. global.git = require('./models/git').init(appconfig);
  29. global.entries = require('./models/entries').init(appconfig);
  30. global.mark = require('./models/markdown');
  31. global.ws = require('socket.io-client')('http://localhost:' + appconfig.wsPort, { reconnectionAttempts: 10 });
  32. // ----------------------------------------
  33. // Load modules
  34. // ----------------------------------------
  35. var _ = require('lodash');
  36. var moment = require('moment');
  37. var Promise = require('bluebird');
  38. var fs = Promise.promisifyAll(require("fs-extra"));
  39. var path = require('path');
  40. var cron = require('cron').CronJob;
  41. // ----------------------------------------
  42. // Start Cron
  43. // ----------------------------------------
  44. var jobIsBusy = false;
  45. var jobUplWatchStarted = false;
  46. var job = new cron({
  47. cronTime: '0 */5 * * * *',
  48. onTick: () => {
  49. // Make sure we don't start two concurrent jobs
  50. if(jobIsBusy) {
  51. winston.warn('[AGENT] Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)');
  52. return;
  53. }
  54. winston.info('[AGENT] Running all jobs...');
  55. jobIsBusy = true;
  56. // Prepare async job collector
  57. let jobs = [];
  58. let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo);
  59. let dataPath = path.resolve(ROOTPATH, appconfig.paths.data);
  60. let uploadsPath = path.join(repoPath, 'uploads');
  61. let uploadsTempPath = path.join(dataPath, 'temp-upload');
  62. // ----------------------------------------
  63. // REGULAR JOBS
  64. // ----------------------------------------
  65. //*****************************************
  66. //-> Sync with Git remote
  67. //*****************************************
  68. jobs.push(git.onReady.then(() => {
  69. return git.resync().then(() => {
  70. //-> Stream all documents
  71. let cacheJobs = [];
  72. let jobCbStreamDocs_resolve = null,
  73. jobCbStreamDocs = new Promise((resolve, reject) => {
  74. jobCbStreamDocs_resolve = resolve;
  75. });
  76. fs.walk(repoPath).on('data', function (item) {
  77. if(path.extname(item.path) === '.md') {
  78. let entryPath = entries.parsePath(entries.getEntryPathFromFullPath(item.path));
  79. let cachePath = entries.getCachePath(entryPath);
  80. //-> Purge outdated cache
  81. cacheJobs.push(
  82. fs.statAsync(cachePath).then((st) => {
  83. return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active';
  84. }).catch((err) => {
  85. return (err.code !== 'EEXIST') ? err : 'new';
  86. }).then((fileStatus) => {
  87. //-> Delete expired cache file
  88. if(fileStatus === 'expired') {
  89. return fs.unlinkAsync(cachePath).return(fileStatus);
  90. }
  91. return fileStatus;
  92. }).then((fileStatus) => {
  93. //-> Update cache and search index
  94. if(fileStatus !== 'active') {
  95. return entries.updateCache(entryPath);
  96. }
  97. return true;
  98. })
  99. );
  100. }
  101. }).on('end', () => {
  102. jobCbStreamDocs_resolve(Promise.all(cacheJobs));
  103. });
  104. return jobCbStreamDocs;
  105. });
  106. }));
  107. //*****************************************
  108. //-> Clear failed temporary upload files
  109. //*****************************************
  110. jobs.push(
  111. fs.readdirAsync(uploadsTempPath).then((ls) => {
  112. let fifteenAgo = moment().subtract(15, 'minutes');
  113. return Promise.map(ls, (f) => {
  114. return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s }; });
  115. }).filter((s) => { return s.stat.isFile(); }).then((arrFiles) => {
  116. return Promise.map(arrFiles, (f) => {
  117. if(moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
  118. return fs.unlinkAsync(path.join(uploadsTempPath, f.filename));
  119. } else {
  120. return true;
  121. }
  122. });
  123. });
  124. })
  125. );
  126. // ----------------------------------------
  127. // Run
  128. // ----------------------------------------
  129. Promise.all(jobs).then(() => {
  130. winston.info('[AGENT] All jobs completed successfully! Going to sleep for now.');
  131. if(!jobUplWatchStarted) {
  132. jobUplWatchStarted = true;
  133. upl.initialScan();
  134. }
  135. return true;
  136. }).catch((err) => {
  137. winston.error('[AGENT] One or more jobs have failed: ', err);
  138. }).finally(() => {
  139. jobIsBusy = false;
  140. });
  141. },
  142. start: false,
  143. timeZone: 'UTC',
  144. runOnInit: true
  145. });
  146. // ----------------------------------------
  147. // Connect to local WebSocket server
  148. // ----------------------------------------
  149. ws.on('connect', function () {
  150. winston.info('[AGENT] Background Agent started successfully! [RUNNING]');
  151. job.start();
  152. });
  153. ws.on('connect_error', function () {
  154. winston.warn('[AGENT] Unable to connect to WebSocket server! Retrying...');
  155. });
  156. ws.on('reconnect_failed', function () {
  157. winston.error('[AGENT] Failed to reconnect to WebSocket server too many times! Stopping agent...');
  158. process.exit(1);
  159. });
  160. // ----------------------------------------
  161. // Shutdown gracefully
  162. // ----------------------------------------
  163. process.on('disconnect', () => {
  164. winston.warn('[AGENT] Lost connection to main server. Exiting...');
  165. job.stop();
  166. process.exit();
  167. });
  168. process.on('exit', () => {
  169. job.stop();
  170. });