'use strict'; const debug = require('debug')('agm:obstacle_worker'), cron = require('node-cron'), util = require('util'), path = require('path'), fs = require('fs-extra'), moment = require('moment'), cheerio = require('cheerio'), puppeteer = require('puppeteer'), env = require('../helpers/env'), isProd = env.PRODUCTION, obsUtil = require('../helpers/file_obstacle'), utils = require('../helpers/utils'), unzip = require('extract-zip'), Obstacle = require('../model/obstacles'), { DBConnection } = require('../helpers/db/connect'); // Initialize database connection const workerDB = new DBConnection('Obstacle Worker'); debug("Is Prod", isProd); const runUTC = moment.utc(); // Register fatal handlers const { registerFatalHandlers } = require('../helpers/process_fatal_handlers'); registerFatalHandlers(process, { env, debug, kindPrefix: 'obstacle_worker', reportFilePath: path.join(__dirname, 'obstacle_worker.rlog'), }); // Initialize the database connection workerDB.initialize({ setupExitHandlers: false }); const stateFileName = path.join(__dirname, 'obstacle_wkr_state.json'); const readObstaclesASync = util.promisify(obsUtil.readObstacles); const downloadAsync = util.promisify(utils.download); /* * * * * * * | | | | | | | | | | | day of week | | | | month | | | day of month | | hour | minute second ( optional ) field value second 0-59 minute 0-59 hour 0-23 day of month 1-31 month 1-12 (or names) day of week 0-7 (or names, 0 or 7 are Sunday) */ const cleanTempFilesTiming = isProd ? '5 1 * * 7' : `*/30 * * * * *`; // In Production mode, run “At 01:05 on every 7th day-of-week.” cron.schedule(cleanTempFilesTiming, async () => { debug("Start cleaning temp. files ...", moment.utc().toISOString()); const tempPaths = isProd ? ['/media/ssd1/agmission/.tmp/', '/media/ssd1/agmission/reports/dat/'] : ['/media/data/trung/work/AgMission/trunk/Development/server/.tmp/', '/media/data/trung/work/AgMission/trunk/Development/server/reports/dat/']; const numOfDays = isProd ? 7 : 1; let delCmd = tempPaths.map(p => `find ${p} -mindepth 1 -mtime +${numOfDays} -daystart -prune -exec rm -f -R {} \\;`); delCmd = delCmd.join(' ; '); try { await utils.execAsync(delCmd); debug("Cleaning temp files is Done."); } catch (err) { debug(err.stack); } }, { scheduled: false, timezone: "Etc/UTC" }); const updateObstaclesData = { schedule: isProd ? '30 1 1 */1 *' : `*/30 * * * * *`, //`${runUTC.minute() + 1} ${runUTC.hour()} ${runUTC.date()} * *`, status: 0 } // In Production mode, run “At 01:30 on day-of-month 1 in every month.” cron.schedule(updateObstaclesData.schedule, async () => { // Check and only proceed when is idle and the db connection is connected if (!workerDB.isReady() || updateObstaclesData.status) return; const task = 'Updating obstacles'; debug(`Start ${task} ...`, moment.utc().toISOString()); try { updateObstaclesData.status = 1; await updateFAAObstacles(); } catch (err) { debug(isProd ? err.message : err.stack); } finally { debug(`${task} is Done.`); updateObstaclesData.status = 0; } }, { scheduled: true, timezone: "Etc/UTC" }); const getDOFLinks = html => { var data = [], a; const $ = cheerio.load(html); $("table caption:contains('Digital Obstacle Files'), tbody > tr > td").each((i, td) => { a = $(td).find('a'); if (a.length && (a.text() && a.text().toUpperCase() === "DOF")) { data.push({ product: a.text(), link: a.attr('href') }); } }); return data; } async function getLatestFAAObsZip(lastState) { try { const browser = await puppeteer.launch({ headless: "new", args: ['--incognito'], ignoreHTTPSErrors: true, ignoreDefaultArgs: ['--disable-dev-shm-usage'], }); const page = await browser.newPage(); await page.goto(env.FAA_DOF_URL, { waitUntil: 'networkidle2' }); const response = await page.evaluate(() => document.querySelector('body').innerHTML); await browser.close(); let dofs = getDOFLinks(response); if (dofs.length) { let dof = dofs[dofs.length - 1]; if (!dof.link) throw new Error('no_obs_file'); if (lastState.dofFile && path.basename(lastState.dofFile.toLowerCase()) === path.basename(dof.link).toLowerCase()) throw new Error('obs_file_already_updated'); return dof.link; } return null; } catch (err) { throw err; } } async function updateFAAObstacles() { const unzipPath = path.join(env.TEMP_DIR, '/obs/'); const dofFilePath = path.join(unzipPath, 'DOF.DAT'); const lastState = fs.readJsonSync(stateFileName, { throws: false }) || {}; let fileURL, zipFile; let obstacles = []; try { fileURL = await getLatestFAAObsZip(lastState); if (fileURL) { zipFile = path.join(unzipPath, path.basename(fileURL)); fs.ensureDirSync(unzipPath); // Check if the file is already downloaded if (!fs.existsSync(zipFile)) { debug("Downloading FAA Obstacle file :" + fileURL); await downloadAsync(fileURL, zipFile); } else { debug("Skipped. FAA Obstacle file already downloaded :" + zipFile); } } else { throw new Error('no_obs_file'); } debug("Unzipping :" + zipFile); await unzip(zipFile, { dir: path.resolve(unzipPath) }); if (!fs.pathExistsSync(dofFilePath)) throw new Error('no_obs_dof_file'); obstacles = await readObstaclesASync(dofFilePath); debug("Removing old obstacles !"); await Obstacle.deleteMany({ "properties.type": { $ne: "USER" } }); if (obstacles && obstacles.length) { debug(`Importing ${obstacles.length} new records ...`); const chunks = utils.chunkArray(obstacles, 1000); for (const chunk of chunks) { await Obstacle.insertMany(chunk); } } debug(`Imported Obstacle file ${dofFilePath} with ${obstacles.length ? obstacles.length : 0} records`); lastState['dofFile'] = fileURL; lastState['total'] = obstacles.length; lastState['date'] = moment.utc().toISOString(); fs.writeJsonSync(stateFileName, lastState); return obstacles.length; } catch (err) { if (err.message && !err.message.includes('_')) { debug('Error when updating FAA Obstacles', err); } throw err; } }