#!/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)