From d7717568e73a7ccea8f4ad329f438bc827522741 Mon Sep 17 00:00:00 2001 From: moshix Date: Fri, 10 Sep 2021 11:59:32 -0500 Subject: [PATCH] Create mvssplitter.py --- mvssplitter.py | 385 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 mvssplitter.py diff --git a/mvssplitter.py b/mvssplitter.py new file mode 100644 index 0000000..48de39e --- /dev/null +++ b/mvssplitter.py @@ -0,0 +1,385 @@ +#!/usr/bin/python3 + +""" +Split MVS 3.8j TK4- Spool files + +DESCRIPTION: + 1) Takes print outputs generated by MVS 3.8j TK4-. + a) either from an already ASCII file + b) or by listening directly to a sockdev + 2) Splits it into one file per job. + 3) Converts each job print out to PDF. + +DEPENDENCIES: + - MVS 3.8j TK4- + - Phyton 3 + - enscript and ps2pdf + need to installed and in the path of your computer running this. + +BUGS: + * Some error when processing CLASS Z printouts + UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd7 in position 0: invalid continuation byte + +CHANGELOG: + v0.4.2: Beta release - Solved bug because of typo on the months' list + v0.4.1: Beta release - Solved bug on conversion to 24h format. + v0.4.0: Beta release - Separated Programmer's Name from Job Name. + - Reverted v0.3.0, as it doesn't solve the issue. + v0.3.0: Beta release - Solved bug that created a blank page at the beginning. + v0.2.0: Beta release - If jobDateYear is the same as current date, add century to the filename (e.g. 20 => 2020). + v0.1.1: Beta release - Solved bugs with PDF filename: jobType missing, no leading zeros when hour < 10, job name truncated. + v0.1.0: Beta release - First release. + +---------------------------LICENSE NOTICE-------------------------------- +MIT License + +Copyright (c) 2020 David Asta + +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. +""" + +import sys +import argparse +import subprocess +import os +import socket +import datetime + +__pgmname__ = "mvssplitspl" +__version__ = "v0.4.2 Beta" + +################################################################ +class SplitSpool: + ############################################################ + # class init + def __init__(self, outdir, seproom, sepmsgclass, debug): + self.outputdir = outdir + # Job information + self.jobType = '' + self.jobNumber = '' + self.jobName = '' + self.jobProgrammerName = '' + self.jobRoom = '' + self.jobTimeHour = '' + self.jobTimeMinutes = '' + self.jobTimeSeconds = '' + self.jobTimeAMPM = '' + self.jobDateDay = '' + self.jobDateMonth = '' + self.jobDateYear = '' + self.jobPrinter = '' + self.jobMSGCLASS = '' + + self.sockdev = '' + self.jobLines = list() + self.countEndJobLine = 0 + self.endJob = False + self.months = ["", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] + + self.seproom = seproom + self.sepmsgclass = sepmsgclass + self.debug = debug + self.outputfilename = '' + + ############################################################ + # Create a socket and connect to specified Address and Port + def connectToSocket(self, hostaddr, hostport): + # Create socket + try: + self.sockdev = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error as err: + print('Error creating socket %s', err) + sys.exit(1) + + # Connect to socket + try: + self.sockdev.connect((hostaddr, int(hostport))) + print('Connected to: ' + hostaddr + ":" + hostport) + except socket.error as err: + print('Error connecting to ' + hostaddr + ":" + hostport) + print(err) + sys.exit(1) + + ############################################################ + # Listen to whatever is coming out of the MVS sockdev + # and for each line call processLine + # Lines are detected by Line Feed (\n) + def listenToSocket(self): + data = '' + buffer = '' + #isFirstChar = True + + while True: + data = self.sockdev.recv(1).decode('utf-8') + + # v0.3.0 - When a 0x0C character is found + # as the first character, it makes + # a Form Feed (FF), which creates + # a blank page at the beginning. + #if isFirstChar: + # isFirstChar = False + # if hex(ord(data[0:1])) == '0xc': + # data = ' ' + + buffer = buffer + data + if data == '\n': # Line Feed + self.processLine(buffer) + buffer = '' + + ############################################################ + # Read all lines of the input file, + # and for each line call processLine + def readFile(self, inputfile): + isFirstChar = True + self.prtfile = open(inputfile, "r") + + while True: + line = self.prtfile.readline() + + if not line: + break + + # v0.3.0 - When a 0x0C character is found + # as the first character, it makes + # a Form Feed (FF), which creates + # a blank page at the beginning. + #if isFirstChar: + # isFirstChar = False + # if hex(ord(line[0:1])) == '0xc': + # print("Initial FF removed") + # print(hex(ord(line[0:1]))) + # line = ' ' + line[1:] + + self.processLine(line) + + self.prtfile.close() + + ############################################################ + # For a line, + # extract job's information if is START/CONT + # call createJobFile if is END + def processLine(self, line): + # Line with START, identifies first page of the printout + if (line.find("START", 7, 12) > 0) \ + or (line.find("CONT", 7, 12) > 0): + self.extractJobInfo(line) + # Line with END, identifies last page of the printout + elif line.find("END", 7, 12) > 0: + self.countEndJobLine = self.countEndJobLine + 1 + if self.countEndJobLine == 4: + self.endJob = True + # Normal line + self.jobLines.append(line) + + if self.endJob: + # All lines of all pages of this job were read + self.createJobFile(self.jobLines) + self.jobLines.clear() + self.endJob = False + self.countEndJobLine = 0 + + ############################################################ + # Get Job's information details, + # used to compose the final filename of the PDF + def extractJobInfo(self, line): + self.jobMSGCLASS = line[4:5] + self.jobNumber = line[18:22].strip() + self.jobName = line[24:32].strip() + self.jobProgrammerName = line[34:55].strip() + self.jobRoom = line[61:65].strip() + self.jobTimeHour = (str(line[67:69]).strip()).zfill(2) + self.jobTimeMinutes = line[70:72] + self.jobTimeSeconds = line[73:75] + self.jobTimeAMPM = line[76:78] + self.jobDateDay = line[79:81] + self.jobDateMonth = str(self.months.index(line[82:85])).zfill(2) + self.jobDateYear = line[86:88] + self.jobPrinter = line[90:98] + + # If necessary, convert hours to 24h format + self.convertTo24h(int(self.jobTimeHour)) + + # Detect type of job + if line.find("JOB", 14,17) > 0: + self.jobType = 'J' + elif line.find("STC", 14,17) > 0: + self.jobType = 'S' + elif line.find("TSU", 14,17) > 0: + self.jobType = 'T' + + ############################################################ + # Create file with Job's print out in TXT format + # Call enscript to convert from TXT to PS + # Call ps2pdf to convert from PS to PDF + # The PDF is created at the outdir specified in the + # parameters when this script was called + # Remove TXT and PS files after the conversion to PDF is done + def createJobFile(self, jobLines): + filename = self.composeOutputFilename() + dirname = self.composeOutputDirectory() + filename_prt = dirname + filename + ".prt" + filename_ps = dirname + filename + ".ps" + filename_pdf = dirname + "/" + filename + ".pdf" + + print(filename_pdf) + + outputfile = open(filename_prt, "w") + + for line in jobLines: + outputfile.write(line) + + outputfile.close() + + # Call external programs enscript and ps2pdf to get the final PDF file + #subprocess.run(["enscript", "--quiet", "--font=Courier-Bold@8", "-l", "-H1", "-r", \ + subprocess.run(["enscript", "--quiet", "--font=1403VintageMonoLimited-Regular@8.5", "-l", "-L 60","-H3", "-r", \ + "--margins=25:25:40:40", "-p", "- ", filename_prt, "-o", filename_ps]) + if not self.debug: + os.remove(filename_prt) + subprocess.run(["ps2pdf", filename_ps, filename_pdf]) + os.remove(filename_ps) + + ############################################################ + # Compose the final PDF filename, + # based on the job information + def composeOutputFilename(self): + # example: 200609_113744_A_PRINTER1-ROOM_J942_PRINTSR + + # v0.2.0 - If jobDateYear is the same as current date, + # add century to the filename (e.g. 20 => 2020) + yearnow = str(datetime.datetime.now().year)[2:4] + centurynow = str(datetime.datetime.now().year)[0:2] + if(int(yearnow) == int(self.jobDateYear)): + self.jobDateYear = str(datetime.datetime.now().year) + + filename = \ + self.jobDateYear + self.jobDateMonth + self.jobDateDay \ + + "_" + self.jobTimeHour + self.jobTimeMinutes + self.jobTimeSeconds \ + + "_" + self.jobMSGCLASS \ + + "_" + self.jobPrinter \ + + # If there is a ROOM name (/*JOBPARM ROOM=xxxx), added it. + if self.jobRoom: + filename = filename \ + + '-' + self.jobRoom + + filename = filename \ + + "_" + self.jobType + self.jobNumber \ + + "_" + self.jobName + + if self.jobProgrammerName: + filename = filename \ + + "_" + self.jobProgrammerName + + return filename + + ############################################################ + # Compose the output dir, + # based in parameters outdir, seproom and sepmsgclass + # If not existing, create directory + def composeOutputDirectory(self): + dirname = '' + + if self.seproom or self.sepmsgclass: + if self.seproom and self.sepmsgclass: + # Separate both by ROOM and MSGCLASS + if self.jobRoom: # Is there actually a ROOM specified in the printout? + self.createDir(self.outputdir + "/ROOM-" + self.jobRoom) + self.createDir(self.outputdir + "/ROOM-" + self.jobRoom + "/CLASS" + self.jobMSGCLASS) + dirname = self.outputdir + "/ROOM-" + self.jobRoom + "/CLASS" + self.jobMSGCLASS + else: + self.createDir(self.outputdir + "/CLASS" + self.jobMSGCLASS) + dirname = self.outputdir + "/CLASS" + self.jobMSGCLASS + elif self.seproom: + # Separate by ROOM only + if self.jobRoom: # Is there actually a ROOM specified in the printout? + self.createDir(self.outputdir + "/ROOM-" + self.jobRoom) + dirname = self.outputdir + "/ROOM-" + self.jobRoom + else: + self.createDir(self.outputdir) + dirname = self.outputdir + elif self.sepmsgclass: + # Separate by MSGCLASS only + self.createDir(self.outputdir + "/CLASS" + self.jobMSGCLASS) + dirname = self.outputdir + "/CLASS" + self.jobMSGCLASS + else: + self.createDir(self.outputdir) + + return dirname + + ############################################################ + # Try to create directory + def createDir(self, dirname): + if not os.path.exists(dirname): + try: + os.mkdir(dirname) + print("Directory ", dirname, " created.") + except OSError as err: + print("Directory ", dirname, " CANNOT be created.") + print(err) + sys.exit(1) + + ############################################################ + # Convert hour to 24h format + # convert 12AM to 00 + # convert from 1PM to 11Pm, to 13 to 23 + def convertTo24h(self, hour): + hour24 = hour + + if self.jobTimeAMPM == "PM": + if hour < 12: # convert from 1PM to 11PM, to 13 to 23 + hour24 = hour + 12 + elif hour == 12: # convert 12AM to 00 + hour24 = 0 + + self.jobTimeHour = str(hour24).zfill(2) + +################################################################ +if __name__ == "__main__": + # Check that parameter have been received + parser = argparse.ArgumentParser( prog=__pgmname__, \ + description="Split MVS 3.8j TK4- Spool files", \ + epilog=__pgmname__ + " " + __version__) + parser.add_argument("-v", "--version", action="version", version="%(prog)s {version}".format(version=__version__)) + parser.add_argument("-in", "--infile", help="input file (file generated by MVS)") + parser.add_argument("-a", "--address", help="IP address of host's sockdev") + parser.add_argument("-p", "--port", help="Port of host's sockdev") + parser.add_argument("outdir", help="destination directory for PDFs") + parser.add_argument("-r", "--seproom", action="store_true", default=False, help="Separate outputs per ROOM. Directory created if needed.") + parser.add_argument("-c", "--sepmsgclass", action="store_true", default=False, help="Separate outputs per MSGCLASS. Directory created if needed.") + parser.add_argument("-d", "--debug", action="store_true", default=False, help="Do not delete .prt files") + + args = parser.parse_args() + + # remove trailing / from outdir, if exists + if args.outdir.endswith('/'): + args.outdir = args.outdir[0:len(args.outdir) - 1] + + splitter = SplitSpool(args.outdir, args.seproom, args.sepmsgclass, args.debug) + + if args.infile: + # Process an already existing file + splitter.readFile(args.infile) + elif args.address and args.port: + # Connect and process from a scokdev + splitter.connectToSocket(args.address, args.port) + splitter.listenToSocket() + + sys.exit(0) +