#!/usr/bin/python
# -*- coding: utf-8 -*-
########################################################################
# Generalized facility to manipulate os8 device images from the POSIX
# (host) side using OS/8 system programs under SIMH.
#
# See USAGE message below for details.
#
# Copyright © 2018 by Bill Cattey and Warren Young
#
# 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 LISTED ABOVE 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.
#
# Except as contained in this notice, the names of the authors above
# shall not be used in advertising or otherwise to promote the sale,
# use or other dealings in this Software without prior written
# authorization from those authors.
########################################################################
# Bring in just the basics so we can bring in our local modules
import os
import glob
import sys
import string
import re
import argparse
sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')
# Our local modules
from pidp8i import *
from simh import *
# Other global Python modules
import glob
import subprocess
#### GLOBALS AND CONSTANTS #############################################
progmsg = True
DEBUG = False
# regex for parsing an argument string into a sys device
_dev_arg_regex_str = "-(rk|td|dt|rx)(\d?)(s)?"
_dev_arg_re = re.compile(_dev_arg_regex_str)
# regex for parsing an action file att string into a sys device
# reuse _dev_arg_regex_str
_dev_actfile_regex_str = _dev_arg_regex_str[1:] # strip -
_dev_actfile_re = re.compile (_dev_actfile_regex_str)
_os8_from_simh_dev = {"rk" : "RK", "td" : "DTA", "dt" : "DTA", "rx" : "RX"}
_os8_partitions = {"RK": ["A", "B"]}
_os8_file_regex_str = "(\S+):(\S+)?"
_os8_file_re = re.compile(_os8_file_regex_str)
_valid_options = "abiyz"
_arg_to_option = {"-a" : "a", "-b" : "b", "-i" : "i", "-y" : "y", "-z" : "z" }
_option_to_info = {"a" : "ASCII", "b" : "binary", "i" : "image",
"y" : "yank system head", "z" : "zero device" }
#### UTILITY ROUTINES ##################################################
def fetch_one (s, os8dev, oname):
if debug: print "\t" + oname
fname, option = extract_option(oname)
if debug: print "Option: " + option + ", fname: " + fname
s.os8_fetch_pip(os8dev, fname, option)
def send_one (s, os8dev, iname):
if debug: print "\t" + iname
fname, option = extract_option(iname)
if debug: print "Option: " + option + ", fname: " + fname
s.os8_send_pip(os8dev, fname, option)
#### abort_prog ########################################################
#
# Print err_string and exit with -1 return status.
#
def abort_prog (err_str):
print "Abort: " + err_str
sys.exit(-1)
#### parse_attach #####################################################
#
# Parser for OS/8 attach spec.
#
def parse_attach (action_plan, match, imagename):
if match.group(2) == None or match.group(2) == "":
abort_prog ("Need unit number for: " + match.group(1) + ".")
image_spec = [match.group(1), match.group(2), imagename]
if match.group(3) == 's':
if action_plan ["sys"] != None:
print ("Already specified system device. Ignoring sys mount of: " + imagename)
else:
action_plan["sys"] = image_spec
else:
action_plan["mount"].append(image_spec)
#### parse_action_file #################################################
#
# This option allows creation of the action_plan from a file
# instead of by indivitual arguments. Think of it as a batch mode.
#
# Figuring out how to mix command lines args and the action_file
# would be tricky since the args are globbed, analyzed and then
# turned into an action plan.
#
# For now, the action file REPLACES the action plan of any other
# command line arguments before --action-file, and no other
# command line arguments are bothered with.
#
# The action file is 3 columns separated by one or more whitepace chars.
#
# If the first column is "att" it's an attach command
# and is parsed the same way as the attach args are parsed
# (Except we don't have a dash.)
#
# Otherwise the 3 columns are:
#
# option source destination
#
# option is one of the valid OS/8 file options:
# a, b, i, y, z
#
# Example:
#
# att rk0s bootdisk.rk05
# att td0 boing.tu56
# a pidp8i.in DTA0:
# b ac-mq-blinker.pal.pt DTA0:
def parse_action_file(fname):
try:
manifest = open(fname, "r")
except IOError:
print fname + " not found. Skipping."
return None
ioline_re = re.compile("(\S+)\s+(\S+)\s+(\S+)")
action_plan = {}
action_plan["sys"] = None
action_plan["mount"] = []
action_plan["copy"] = []
for line in manifest:
ioline = line.strip()
if DEBUG: print "parse_action_file: ioline: " + ioline
if ioline == "": continue
if ioline[0] == '#': continue # Allow comments
m=re.match(ioline_re, ioline)
if m== None:
print "Ignoring line: " + ioline
continue
option = m.group(1)
source = m.group(2)
destination = m.group(3)
if option == "att":
m = re.match(_dev_actfile_re, source)
if m== None:
prog_abort ("Could not parse attach spec: " + source)
parse_attach (action_plan, m, destination)
else:
if len(option) != 1:
print "Format options are only 1 letter in size. Ignoring line: " + ioline
elif option[0] not in _valid_options:
print "Unrecognize option in line: " + ioline
elif source == None:
print "Null value of source. Ignoring line: " + ioline
elif destination == None:
print "Null value of destination. Ignoring line: " + ioline
else:
action_plan["copy"].append([option, source, destination])
return action_plan
def is_directory(path):
if DEBUG: "is_directory (" + path + ")"
m = re.match(_os8_file_re, path)
if m != None:
if DEBUG: print "OS/8 Match: DEV: " + m.group(1) + ", File: " + str(m.group(2))
if m.group(2) == None or m.group(2) == "":
return True # Just a device so yes it's a directory.
else: return False
if has_os8_wildcards(path):
if DEBUG: print "Has wildcards."
return False
return os.path.isdir(path)
def has_os8_wildcards (filename):
os8_wild_cards = "*?"
for char in os8_wild_cards:
if char in filename: return True
return False
def append_copy(action_plan, mode, source, destination):
if ":" in source:
if "/" in source:
print "Illegal OS/8 file spec containing a slash:" + source
sys.exit(-1)
source = source.upper()
if ": in destination":
if "/" in destination:
print "Illegal OS/8 file spec containing a slash:" + destination
sys.exit(-1)
destination = destination.upper()
copyspec = [mode, source, destination]
action_plan["copy"].append (copyspec)
#### parse_args ##############################################################
#
# Builds the action plan from the command line arguments and executes it
#
# Note that if we specify the --action-file, the
# contents of that file REPLACE any command line arguments.
#
# An element of the action_plan["copy"] array is itself an array
# that names a file format, a source, and a destination:
#
# [<format>, <source>, <destination>]
#
# The source and destination file specifications are interpreted as in
# the USAGE message below. (Look for "colon".)
#
USAGE = 'usage: ' + os.path.basename (__file__) + \
""" [-dh] [--rk,--rx[tag] rotating-disk] [--dt,--td[tag] tape]
[[-abiyz] src-files]... [src-files] <dest>
This program boots an OS/8 environment underneath the SIMH PDP-8
simulator then tries to behave like the POSIX program we are named
after, either copying files from the POSIX (host) side into the
simulated OS/8 environment, copying files out of OS/8 to the POSIX
side, or copying filoes within the OS/8 world from one volume to
another.
The copying direction is determined by which file name arguments
have a colon in them:
* copy-within: The source and destination file arguments have
colons, so copy within the OS/8 environment from one volume to
another.
* copy-into: Only the dest argument has a colon, so assume the
source file names are POSIX-side and copy those files into the
simulated OS/8 environment.
* copy-out: The dest argument has no colon but the source file
names do, so copy the named OS/8 files out of the simulation.
If none of the file arguments has a colon in it and you give exactly
two such arguments, we operate in a special case of copy-within
mode: the source and destination volumes are assumed to be DSK:, so
the file is simply copied within the OS/8 DSK: volume from one name
to the other. If you give greater than two file name arguments
without a colon in any of them, it is not possible to make sense of
the command since we do not intend to try and replace your perfectly
good POSIX cp implementation, so it errors out.
If you give only one file name argument, the program always errors
out: it requires at least one source and one destination.
The -a, -b, -i, -y, and -z flags correspond to the OS/8 PIP options
/A, /B, /I, /Y, and /Z, which are passed to PIP as-is. (This
priogram currently uses PIP as its primary handler for the OS/8 side
of the work.) They must be followed by at least one source file
name, and they affect all subsequent source file names until another
such option is found. For example:
$ os8-cp -a foo bar -b qux sys:
Files foo and bar are copied to SYS: in ASCII mode, overriding the
default binary mode, then binary mode is restored for the copy of
file qux to the SYS: volume.
Beware that -i means something very different to this program than
it means to POSIX cp: destination files will be unceremoniously
overwritten!
If --rk or --rx is given, the named rotating disk image is attached
to the simulator on the first free disk controller if no digit
follows the letters, or to the specified controller otherwise.
If an "s" is at the end of any of the above media image file option
names, that marks the "system disk", which overrides our default
boot media, $prefix/shared/media/os8/os8v3d-patched.rk05.
Therefore, the following:
$ os8-cp --rk my.rk05 foo RKA1:
...will boot from the default RK05 disk image attached to RK0 with
my.rk05 attached to RK1, then copy POSIX-side file foo to RKA1:FOO.
$ os8-cp --tds my.tu56 --rk my.rk05 foo DSK:
...will boot from my.tu56, which is presumed to be a bootable OS/8
DECtape attached to SIMH device TD0. The RK05 disk image my.rk05
will be attached to RK0, since the default boot disk is not attached
there in this example. It will copy POSIX-side file foo to DSK:FOO
which will probably be interpreted as DTA0:FOO by the typical BUILD
options for a bootable OS/8 TU56 DECtape. Beware therefore of using
the generic SYS: and DSK: device names! You would be better advised
to use DTA0:, RKA0: or RKB0: as the destination in this example.
$ os8-cp --tds my.tu56 --rx1 my.rx01 foo RXA1:
This not only fixes the almost-certainly incorrect use of DSK: in
the prior example, it shows how a disk may be mounted on a device
other than the default by giving a digit after the device type in
the option name. (That is, --rx1 instead of just --rx, which would
be the same as --rx0 in this example.)
The --dt and --td options are handled similarly to the --r* options,
differing only in whether we use the SIMH DT or TD PDP-8 devices,
which correspond to the TC08 or TD8E DECtape controllers. Which one
you give depends on the device support built into the OS/8 media
you've booted from.
Destination OS/8 volumes are modified in place, rather than
recreated. The volume must therefore exist before this program
runs.
When only a destination device, directory, or volume name is given,
file names are normalized when coping between POSIX and OS/8
systems. File names are uppercased and truncated to 6.2 limits when
copying from POSIX to OS/8. File names are lowercased on copying
from OS/8 to the POSIX side unless you give the *source* file name
in all-uppercase; then, file name case is preserved. This behavior
is overridden if you give a complete file name for the destination:
$ os8-cp my-long-file-name.txt DSK:MLF.FD
If you gave "DSK:" as the destination instead, you would have gotten
"MY-LON.TX" as the desintation file name instead.
Give -d to run in debug mode.
Give -h to print this message.
"""
def parse_args ():
global DEBUG
action_plan = {}
action_plan["sys"] = None
action_plan["mount"] = []
action_plan["copy"] = []
idx = 1
numargs = len(sys.argv)
filespec_seen = 0
mode_opt = "b" # start of with default of binary.
first_mode = mode_opt
source = ""
destination = ""
# Keep file_list and mode_list in sync.
file_and_mode_list = []
while idx < numargs:
if DEBUG: print "idx: " + str(idx)
arg = sys.argv[idx]
# First the simple bit set options
if arg == "-d":
DEBUG = True
elif arg == "-h":
print USAGE
sys.exit(0)
# look for option args.
elif arg in _arg_to_option:
new_opt = _arg_to_option[arg]
if mode_opt == new_opt:
print "Warning redundant reset of mode option to " + \
_option_to_info[new_opt]
mode_opt = new_opt
# Not a simple bit set option.
elif arg == "--action-file":
if idx + 1 == numargs: # Need filename, but no args left.
abort_prog ("No action file name.")
argfilename = sys.argv[idx + 1]
retval = parse_action_file(argfilename)
if retval == None:
abort_prog ("No action plan could be made from " + argfilename + ".")
else: return retval
else:
# Parser for OS/8 attach spec.
m = re.match(_dev_arg_re, arg)
if m != None:
if idx + 1 == numargs: # Need filename, but no args left.
abort_prog ("No image file name.")
idx +=1
parse_attach (action_plan, m, sys.argv[idx])
# Do file parser if we didn't get an OS/8 attach spec.
else:
if DEBUG: print "File parsing of: " + arg
# Need to know if arg is Linux. If so, we need to do globbing.
# If you want OS/8 globbing, specify a device to prevent globbing
# from being run.
m = re.match(_os8_file_re, arg)
if m == None: # Yup, it's linux. Glob it.
more_files = glob.glob(arg)
if more_files == []:
more_files.append(arg) # If file not found may be an OS/8 internal xfer.
for new_file in more_files:
if filespec_seen == 0:
source = new_file
first_mode = mode_opt
elif filespec_seen == 1:
destination = new_file
else:
file_and_mode_list.append([mode_opt,destination])
destination = new_file
filespec_seen += 1
else:
if filespec_seen == 0:
source = arg
first_mode = mode_opt
elif filespec_seen == 1:
destination = arg
else:
file_and_mode_list.append([mode_opt, destination])
destination = arg
filespec_seen += 1
if DEBUG: print "filespec_seen: " + str(filespec_seen)
idx +=1 # Bottom of the while loop. Increment.
if filespec_seen == 0:
abort_prog ("No file specs seen. Nothing to do.")
elif filespec_seen == 1:
abort_prog ("Only 1 file spec found. Nothing to do.")
# Now it gets a little complicated...
# If neither source nor destination is OS/8, pretend they both were OS/8 "DSK:"
# If source is OS/8, and has OS/8 wild cards, the destination must be a directory.
else:
# If more than 2 files, the destination must be either an OS/8 device or a Linux directory.
if DEBUG: print "Destination: " + destination
if filespec_seen > 2 and is_directory(destination) == False:
abort_prog ("Destination must be a Linux directory or OS/8 device for multiple source files.")
m1 = re.match(_os8_file_re, source)
m2 = re.match(_os8_file_re, destination)
# If source is OS/8 and it has wild cards, but destination is a file, not a device,
# it's a fatal error.
if m1 != None and has_os8_wildcards(source) and is_directory (destination) == False:
abort_prog ("Not going to concatinate multiple OS/8 files into: " + destination + ".")
if m1 == None and m2 == None and filespec_seen == 2: # No OS/8 dev on two names, local DSK: copy.
source = "DSK:" + source
destination = "DSK:" + destination
append_copy(action_plan, first_mode, source, destination)
for mode_and_file in file_and_mode_list:
filename = mode_and_file[1]
m3 = re.match(_os8_file_re, filename)
if m3 != None and has_os8_wildcards(filename) and is_directory (destination) == False:
abort_prog ("Not going to concatinate multiple OS/8 files into: " + destination + ".")
append_copy(action_plan, mode_and_file[0], filename, destination)
return action_plan
#### main ##############################################################
def main ():
action_plan = parse_args()
if action_plan == None:
prog_abort ("No action plan was parsed.")
if DEBUG: print str(action_plan)
# Create the SIMH child instance and tell it where to send log output
try:
s = simh (dirs.build, True)
except (RuntimeError) as e:
print "Could not start simulator: " + e.message + '!'
exit (1)
s.set_logfile (os.fdopen (sys.stdout.fileno (), 'w', 0))
# Perform sys attach
att_spec = action_plan["sys"]
simh_boot_dev = att_spec[0] + att_spec[1] # Compose simh dev from name and unit.
imagename = att_spec[2]
if not os.path.exists (imagename):
abort_prog ("Requested boot image file: " + imagename + " not found.")
s.send_cmd ("att " + simh_boot_dev + " " + imagename)
images_to_zero = []
# Attach other mounts
for att_spec in action_plan["mount"]:
simh_dev = att_spec[0] + att_spec[1] # Compose simh dev from name and unit.
imagename = att_spec[2]
if os.path.exists (imagename):
print "Modifying existing " + simh_dev + " image " + imagename
else:
print "Will create a new image file named: " + imagename
# Save this att_spec so we can zero it later.
images_to_zero.append (att_spec)
s.send_cmd ("att " + simh_dev + " " + imagename)
print "Booting " + simh_boot_dev + "..."
s.send_cmd ("boot " + simh_boot_dev)
for att_spec in images_to_zero:
os8dev = _os8_from_simh_dev[att_spec[0]]
if os8dev in _os8_partitions:
for partition in _os8_subunits[os8dev]:
os8name = os8dev + partition + att_spec[1] + ":"
print "Initializing directory of " + os8name + " in " + imagename
s.os8_send_cmd ('\\.', "ZERO " + os8name)
else:
os8name = os8dev + att_spec[1] + ":"
print "Initializing directory of " + os8name + " in " + imagename
s.os8_send_cmd ('\\.', "ZERO " + os8name)
# Perform copy operations
for do_copy in action_plan["copy"]:
mode_opt = do_copy[0]
source = do_copy[1]
destination = do_copy[2]
if DEBUG: print "Source: " + source + ", Destination: " + destination + ", Mode: " + mode_opt + "."
# Is this "from" OS/8 to POSIX, "to" OS/8 from POSIX or "on" OS/8?
# "to" -- Attach source to simh ptr
# If we are operating "from" and source has wild cards,
# Use DIRECT to create list of files.
# "from" -- Attach destination to ptp
# "on" -- Use FOTP.
# Detach all mounts and then sys.
s.back_to_cmd ('\\.')
for att_spec in action_plan["mount"]:
simh_dev = att_spec[0] + att_spec[1] # Compose simh dev from name and unit.
s.send_cmd ("det " + simh_dev)
s.send_cmd ("det " + simh_boot_dev)
# And shut down the simulator.
s.send_cmd ('quit')
if __name__ == "__main__": main()