From 02ef2b2241a1c42d1f3ac8fdd38f74814c20f35f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 06:35:55 -0500 Subject: [PATCH] Improve error handling. Better support for spawn errors encountered during generation (for ex, PDFs through wkhtml) + general refactoring. --- src/core/error-handler.js | 93 +++++++++++++++++++++++++++++++++++ src/core/spawn-watch.js | 22 +++++++++ src/gen/html-pdf-generator.js | 76 +++++++++++++++------------- src/gen/template-generator.js | 2 +- src/index.js | 83 +++---------------------------- test/test-themes.js | 5 +- 6 files changed, 169 insertions(+), 112 deletions(-) create mode 100644 src/core/error-handler.js create mode 100644 src/core/spawn-watch.js diff --git a/src/core/error-handler.js b/src/core/error-handler.js new file mode 100644 index 0000000..53aa014 --- /dev/null +++ b/src/core/error-handler.js @@ -0,0 +1,93 @@ +/** +@module error-handler.js +*/ + +(function() { + + var HACKMYSTATUS = require('./status-codes') + , PKG = require('../../package.json') + , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white; + + var ErrorHandler = module.exports = { + + + err: function( ex, shouldExit ) { + var msg = '', exitCode; + + if( ex.fluenterror ){ + switch( ex.fluenterror ) { // TODO: Remove magic numbers + + case HACKMYSTATUS.themeNotFound: + msg = "The specified theme couldn't be found: " + ex.data; + break; + + case HACKMYSTATUS.copyCSS: + msg = "Couldn't copy CSS file to destination folder"; + break; + + case HACKMYSTATUS.resumeNotFound: + msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + ' in FRESH or JSON Resume format.'.guide; + break; + + case HACKMYSTATUS.missingCommand: + msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { + return (idx === ar.length - 1 ? 'or '.guide : '') + + v.toUpperCase().guide; + }).join(', '.guide) + ").\n\n".guide + + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; + break; + + case HACKMYSTATUS.invalidCommand: + msg = 'Please '.guide + 'specify the output resume file'.guide.bold + + ' that should be created.'.guide; + break; + + case HACKMYSTATUS.resumeNotFoundAlt: + msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + ' in either FRESH or JSON Resume format.'.guide; + break; + + case HACKMYSTATUS.inputOutputParity: + msg = 'Please '.guide + 'specify an output file name'.guide.bold + + ' for every input file you wish to convert.'.guide; + break; + + case HACKMYSTATUS.createNameMissing: + msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + + ' to create.'.guide; + break; + + case HACKMYSTATUS.wkhtmltopdf: + msg = 'ERROR: PDF generation failed. '.red.bold + ('Make sure wkhtmltopdf is ' + + 'installed and accessible from your path.').red; + break; + + } + exitCode = ex.fluenterror; + + } + else { + msg = ex.toString(); + exitCode = 4; + } + + var idx = msg.indexOf('Error: '); + var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); + if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s + console.log( ('ERROR: ' + trimmed.toString()).red.bold ); + console.log( ex.stack.gray); + } + else { + console.log( trimmed.toString() ); + } + + if( shouldExit ) + process.exit( exitCode ); + + } + + }; + +}()); diff --git a/src/core/spawn-watch.js b/src/core/spawn-watch.js new file mode 100644 index 0000000..ac06849 --- /dev/null +++ b/src/core/spawn-watch.js @@ -0,0 +1,22 @@ +/** +@module spawn-watch.js +*/ + +(function() { + +// Catch various out-of-band child process errors such as ENOENT for PDFs +// http://stackoverflow.com/q/27688804 +var SpawnWatcher = module.exports = function() { + var childProcess = require("child_process"); + var oldSpawn = childProcess.spawn; + childProcess.spawn = function() { + return oldSpawn.apply(this, arguments) + .on('error', function(err) { + require('./error-handler').err( err, false ); + }); + }; +}(); + +//SpawnWatcher(); + +}()); diff --git a/src/gen/html-pdf-generator.js b/src/gen/html-pdf-generator.js index 853de90..705c4b3 100644 --- a/src/gen/html-pdf-generator.js +++ b/src/gen/html-pdf-generator.js @@ -1,6 +1,5 @@ /** Definition of the HtmlPdfGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module html-pdf-generator.js */ @@ -23,7 +22,7 @@ Definition of the HtmlPdfGenerator class. Generate the binary PDF. */ onBeforeSave: function( info ) { - pdf( info.mk, info.outputFile ); + pdf.call( this, info.mk, info.outputFile ); return null; // halt further processing } @@ -34,39 +33,50 @@ Definition of the HtmlPdfGenerator class. */ function pdf( markup, fOut ) { - var pdfCount = 0; - if( false ) { //( _opts.pdf === 'phantom' || _opts.pdf == 'all' ) { - pdfCount++; - require('phantom').create( function( ph ) { - ph.createPage( function( page ) { - page.setContent( markup ); - page.set('paperSize', { - format: 'A4', - orientation: 'portrait', - margin: '1cm' - }); - page.set("viewportSize", { - width: 1024, // TODO: option-ify - height: 768 // TODO: Use "A" sizes - }); - page.set('onLoadFinished', function(success) { - page.render( fOut ); - pdfCount++; - ph.exit(); - }); - }, - { dnodeOpts: { weak: false } } ); - }); + pdf_wkhtmltopdf.call( this, markup, fOut ); + + } + + /** + Generate a PDF from HTML using wkhtmltopdf. + */ + function pdf_wkhtmltopdf( markup, fOut ) { + var wk; + try { + wk = require('wkhtmltopdf'); + wk( markup, { pageSize: 'letter' } ) + .pipe( FS.createWriteStream( fOut ) ); } - if( true ) { // _opts.pdf === 'wkhtmltopdf' || _opts.pdf == 'all' ) { - var fOut2 = fOut; - if( pdfCount == 1 ) { - fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf'); - } - require('wkhtmltopdf')( markup, { pageSize: 'letter' } ) - .pipe( FS.createWriteStream( fOut2 ) ); - pdfCount++; + catch(ex) { + // { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', syscall: 'write' } + // { [Error: ENOENT] } + throw { fluenterror: this.codes.wkhtmltopdf }; } } + + // function pdf_phantom() { + // pdfCount++; + // require('phantom').create( function( ph ) { + // ph.createPage( function( page ) { + // page.setContent( markup ); + // page.set('paperSize', { + // format: 'A4', + // orientation: 'portrait', + // margin: '1cm' + // }); + // page.set("viewportSize", { + // width: 1024, // TODO: option-ify + // height: 768 // TODO: Use "A" sizes + // }); + // page.set('onLoadFinished', function(success) { + // page.render( fOut ); + // pdfCount++; + // ph.exit(); + // }); + // }, + // { dnodeOpts: { weak: false } } ); + // }); + // } + }()); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 86687e8..60a7a6f 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -156,7 +156,7 @@ Definition of the TemplateGenerator class. { outputFile: fileName, mk: file.data } ); } catch(ex) { - console.log(ex); + require('../core/error-handler').err(ex, false); } } else if( file.info.action === null/* && theme.explicit*/ ) { diff --git a/src/index.js b/src/index.js index 07081da..48d2503 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,14 @@ /** Command-line interface (CLI) for HackMyResume. -@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. +@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot) @module index.js */ -var ARGS = require( 'minimist' ) + + +var SPAWNW = require('./core/spawn-watch') + , ARGS = require( 'minimist' ) , FCMD = require( './hackmycmd') , PKG = require('../package.json') , COLORS = require('colors') @@ -19,11 +22,12 @@ var ARGS = require( 'minimist' ) + try { main(); } catch( ex ) { - handleError( ex ); + require('./core/error-handler').err( ex, true ); } @@ -91,76 +95,3 @@ function getOpts( args ) { silent: args.s || args.silent }; } - -// TODO: refactor -function handleError( ex ) { - var msg = '', exitCode; - - - - if( ex.fluenterror ){ - switch( ex.fluenterror ) { // TODO: Remove magic numbers - - case HACKMYSTATUS.themeNotFound: - msg = "The specified theme couldn't be found: " + ex.data; - break; - - case HACKMYSTATUS.copyCSS: - msg = "Couldn't copy CSS file to destination folder"; - break; - - case HACKMYSTATUS.resumeNotFound: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + - ' in FRESH or JSON Resume format.'.guide; - break; - - case HACKMYSTATUS.missingCommand: - msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + - Object.keys( FCMD.verbs ).map( function(v, idx, ar) { - return (idx === ar.length - 1 ? 'or '.guide : '') + - v.toUpperCase().guide; - }).join(', '.guide) + ").\n\n".guide + - FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; - break; - - case HACKMYSTATUS.invalidCommand: - msg = 'Please '.guide + 'specify the output resume file'.guide.bold + - ' that should be created.'.guide; - break; - - case HACKMYSTATUS.resumeNotFoundAlt: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + - ' in either FRESH or JSON Resume format.'.guide; - break; - - case HACKMYSTATUS.inputOutputParity: - msg = 'Please '.guide + 'specify an output file name'.guide.bold + - ' for every input file you wish to convert.'.guide; - break; - - case HACKMYSTATUS.createNameMissing: - msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + - ' to create.'.guide; - break; - - } - exitCode = ex.fluenterror; - - } - else { - msg = ex.toString(); - exitCode = 4; - } - - var idx = msg.indexOf('Error: '); - var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); - if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s - console.log( ('ERROR: ' + trimmed.toString()).red.bold ); - console.log( ex.stack.gray); - } - else - console.log( trimmed.toString() ); - - process.exit( exitCode ); - -} diff --git a/test/test-themes.js b/test/test-themes.js index 8d195b3..5a6ee1d 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -1,5 +1,6 @@ -var chai = require('chai') +var SPAWNWATCHER = require('../src/core/spawn-watch') + , chai = require('chai') , expect = chai.expect , should = chai.should() , path = require('path') @@ -64,7 +65,7 @@ describe('Testing themes', function () { genTheme('JRS', src, 'minimist'); genTheme('JRS', src, 'awesome'); genTheme('JRS', src, 'positive'); - genTheme('JRS', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); + genTheme('JRS', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); genTheme('JRS', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); genTheme('JRS', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); genTheme('JRS', src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' );