lp-daemon
Version:
line printer daemon
505 lines (438 loc) • 13.3 kB
JavaScript
/**
* (c) 2018 cepharum GmbH, Berlin, http://cepharum.de
*
* The MIT License (MIT)
*
* Copyright (c) 2018 cepharum GmbH
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author: cepharum
*/
;
const { Transform } = require( "stream" );
const Debug = require( "debug" )( "processor" );
/**
* @typedef {object} RequestContext
* @property {int} requestId ID of request to be unique in context of current runtime
*/
/**
* Implements code processing tokens generated by RequestTokenizer.
*/
class RequestProcessor extends Transform {
/**
* @param {AbstractBackend} backend backend for actually performing requested actions
* @param {object} streamOptions options customizing stream
*/
constructor( backend, streamOptions = {} ) {
super( Object.assign( {}, streamOptions, {
writableObjectMode: true,
readableObjectMode: false,
objectMode: false,
} ) );
Object.defineProperties( this, {
/**
* Provides backend used by processor for actually performing
* requested actions.
*
* @name RequestProcessor#backend
* @property {AbstractBackend}
* @readonly
*/
backend: { value: backend },
} );
if ( !this.constructor.SUCCESS ) {
Object.defineProperties( this.constructor, {
/**
* Provides sole buffer to be pushed for responding on success.
*
* @name RequestProcessor.SUCCESS
* @property {Buffer}
* @readonly
*/
SUCCESS: { value: Buffer.from( [0x00] ) },
/**
* Provides sole buffer to be pushed for responding on generic
* failure.
*
* @name RequestProcessor.FAILURE
* @property {Buffer}
* @readonly
*/
FAILURE: { value: Buffer.from( [0x01] ) },
} );
}
}
/**
* Maps one or more operands to strings handling encoding if either operand
* is given as instance of `Buffer`.
*
* @param {Buffer|string|Array<Buffer|String>} operands one or more operands to be mapped
* @returns {string|string[]} provided operand(s) mapped to string value(s)
* @protected
*/
static _mapToStrings( operands ) {
const gotArray = Array.isArray( operands );
const _operands = Array.isArray( operands ) ? operands : operands ? [operands] : [];
const numOperands = _operands.length;
const copies = new Array( numOperands );
for ( let i = 0; i < numOperands; i++ ) {
const operand = _operands[i];
if ( Buffer.isBuffer( operand ) ) {
copies[i] = this._fixEncoding( operand );
} else {
copies[i] = String( operand );
}
}
return gotArray ? copies : copies[0];
}
/**
* Converts buffer to string replacing any non-UTF8-encoded non-ASCII
* characters with question mark.
*
* @param {Buffer} buffer buffer containing some probably UTF8-encoded string
* @returns {string} string extracted from buffer with invalid bytes replaced with `?`
* @protected
*/
static _fixEncoding( buffer ) {
const numOctets = buffer.length;
let result = "";
const masks = [ 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 ];
for ( let i = 0; i < numOctets; i++ ) {
const octet = buffer[i];
if ( octet & 0x80 ) {
// got probable start of a UTF8-encoded character
let size = 0;
for ( ; size < 7; size++ ) {
if ( !( octet & masks[size] ) ) {
break;
}
}
if ( size ) {
// check to have declared number of follow-up bytes
let offset = 1;
for ( ; offset <= size; offset++ ) {
if ( buffer[i + offset] & 0xC0 !== 0x80 ) {
// not a follow-up byte as expected -> not a UTF-8 encoded character either
break;
}
}
if ( offset <= size ) {
result += "_";
} else {
result += buffer.slice( i, i + offset ).toString( "utf8" );
}
} else {
// got an unexpected follow-up byte? -> not a UTF8-encoded character
result += "_";
}
} else {
result += String.fromCharCode( octet );
}
}
return result;
}
/**
* Parses content of control file.
*
* @param {Buffer} content content of control file to be parsed
* @returns {object} parsed elements of control file
* @private
*/
static _parseControlFile( content ) {
const result = {
print: [],
};
let offset = 0;
let nextLF;
while ( ( nextLF = content.indexOf( 0x0a, offset ) ) > -1 ) {
const lineType = String.fromCharCode( content[offset] );
const operand = this._fixEncoding( content.slice( offset + 1, nextLF ) );
offset = nextLF + 1;
switch ( lineType ) {
case "C" :
result.bannerPageClass = operand;
break;
case "H" :
result.hostName = operand;
break;
case "I" :
if ( !/^\d+$/.test( operand ) ) {
throw new Error( "indent printing control value may consist of numbers, only" );
}
result.indentPrinting = parseInt( operand );
break;
case "J" :
result.bannerPageJobName = operand;
break;
case "L" :
result.bannerPage = operand.trim() || true;
break;
case "M" :
result.mailToWhenPrinted = operand;
break;
case "N" :
result.fileName = operand;
break;
case "P" :
result.userId = operand;
break;
case "S" : {
const operands = operand.split( /\s+/ ).filter( i => i.length > 0 );
result.symbolicLinkData = {
device: operands[0],
inode: operands[1],
};
break;
}
case "T" :
result.jobTitle = operand;
break;
case "U" :
result.unlinkFile = operand;
break;
case "W" :
if ( !/^\d+$/.test( operand ) ) {
throw new Error( "width control value may consist of numbers, only" );
}
result.width = parseInt( operand );
break;
case "1" :
result.troffRFile = operand;
break;
case "2" :
result.troffIFile = operand;
break;
case "3" :
result.troffBFile = operand;
break;
case "4" :
result.troffSFile = operand;
break;
case "c" :
case "d" :
case "f" :
case "g" :
case "k" :
case "l" :
case "n" :
case "o" :
case "p" :
case "r" :
case "t" :
case "v" :
case "z" : {
const map = {
c: "CIF",
d: "DVI",
f: "formatted",
g: "plot",
k: "kerberized",
l: "formattedUrl",
n: "ditroff",
o: "postscript",
p: "pr",
r: "fortran",
t: "troff",
v: "raster",
z: "palladium",
};
const type = map[lineType];
result.print.push( type );
result.print[type] = operand;
break;
}
}
}
if ( !result.hasOwnProperty( "hostName" ) ) {
throw new Error( "missing required host name in control file" );
}
if ( !result.hasOwnProperty( "userId" ) ) {
throw new Error( "missing required user ID in control file" );
}
return result;
}
/**
* Processes tokens generated by RequestTokenizer stream.
*
* @param {RequestTokenizerOutput} token token generated by RequestTokenizer
* @param {?string} encoding not used
* @param {function} callback callback to invoke when object has been processed
* @returns {void}
*/
_transform( token, encoding, callback ) {
Debug( `got token of type "${token.type}" in request ${token.context.requestId}` );
const { context } = token;
const { requestId } = context;
const statics = this.constructor;
switch ( token.type ) {
case "command" : {
const { command } = token;
const operands = statics._mapToStrings( token.operands );
if ( operands < 1 ) {
Debug( `ERROR: missing name of printer in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
const queueName = operands[0];
if ( !queueName ) {
Debug( `ERROR: invalid name of printer in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
context.queueName = queueName;
switch ( command ) {
case 1 :
// print any waiting job
this.backend.releaseJobs( queueName )
.then( success => {
callback( null, success ? statics.SUCCESS : statics.FAILURE );
} )
.catch( error => {
Debug( `ERROR: backend failed on releasing jobs in request #${requestId}: ${error.message}` );
callback( null, statics.FAILURE );
} );
break;
case 2 :
// receive a printer job
context.job = {};
callback( null, statics.SUCCESS );
break;
case 3 :
case 4 :
// send queue state
this.backend.report( queueName, command === 3, operands.slice( 1 ) )
.then( report => {
callback( null, Buffer.from( String( report ), "ascii" ) );
} )
.catch( error => {
Debug( `ERROR: backend failed on generating ${command === 3 ? "brief" : "full"} report in request #${requestId}: ${error.message}` );
callback( null, statics.FAILURE );
} );
break;
case 5 :
// remove jobs
this.backend.removeJob( queueName, operands[1], operands.slice( 2 ) )
.then( success => {
callback( null, success ? statics.SUCCESS : statics.FAILURE );
} )
.catch( error => {
Debug( `ERROR: backend failed on removing jobs in request #${requestId}: ${error.message}` );
callback( null, statics.FAILURE );
} );
break;
default :
Debug( `command 0x${( "0" + command.toString( 16 ) ).slice( -2 )} is not supported` );
callback( null, statics.FAILURE );
}
break;
}
case "subcommand" : {
const { command } = token;
// const operands = statics._mapToStrings( token.operands );
switch ( command ) {
case 1 :
// abort job
context.job = null;
callback( null, statics.SUCCESS );
break;
case 2 :
// receive control file
if ( context.job == null ) {
Debug( `ERROR: got sub-command after aborting job in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
context.job.expect = "control";
callback( null, statics.SUCCESS );
break;
case 3 :
// receive data file
if ( context.job == null ) {
Debug( `ERROR: got sub-command after aborting job in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
context.job.expect = "data";
callback( null, statics.SUCCESS );
break;
}
break;
}
case "data" : {
const { job } = context;
if ( job == null ) {
Debug( `ERROR: got data after aborting job in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
const { expect } = job;
if ( expect == null ) {
Debug( `ERROR: not expecting any data in request ${requestId}` );
callback( null, statics.FAILURE );
break;
}
job.expect = null;
switch ( expect ) {
case "data" :
// receiving data file now -> actually start printing job in backend
this.backend.printJob( context.queueName, job.control || {}, token.stream )
.then( jobId => {
Debug( `backend has printed job and assigned job ID ${jobId} in request #${requestId}` );
callback( null, statics.SUCCESS );
} )
.catch( error => {
Debug( `ERROR: backend failed on printing job in request #${requestId}: ${error.message}` );
callback( null, statics.FAILURE );
} );
context.job = null;
break;
case "control" : {
const chunks = [];
const { stream } = token;
stream.on( "data", chunk => {
chunks.push( chunk );
} );
stream.once( "end", () => {
if ( !job.control ) {
job.control = {};
}
Object.assign( job.control, statics._parseControlFile( Buffer.concat( chunks ) ) );
callback( null, statics.SUCCESS );
} );
stream.once( "error", error => {
Debug( `ERROR: aborting job due to failed submission of control file in request ${requestId}: ${error.message}` );
callback( null, statics.FAILURE );
} );
break;
}
}
break;
}
case "eof" :
// ignore end-of-request tokens
callback();
break;
default :
Debug( "token type not supported" );
callback( new Error( `invalid token of type ${token.type}` ) );
}
}
}
module.exports = RequestProcessor;