PiDP-8/I Software

Changes On Branch os8-run-python3
Log In

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch os8-run-python3 Excluding Merge-Ins

This is equivalent to a diff from b4b89f2f1e to 9b105fe0f5

2020-04-19
22:03
The build system now detects the availability of Python 3 and prefers it if available. check-in: d07b6b46f9 user: tangent tags: trunk
2020-03-24
23:50
Add ".decode()" to all uses of self_child.{before,after}. Now everything builds under python3. Closed-Leaf check-in: 9b105fe0f5 user: poetnerd tags: os8-run-python3
2019-05-11
18:35
Added all of the files renamed in [1860880163] to *.in to the Makefile's INFILES variable, so we autoreconf when any of them get touched. check-in: c67bd86630 user: tangent tags: os8-run-python3
15:33
URL update check-in: 89e22f5975 user: tangent tags: trunk
15:33
Merged in trunk changes check-in: 19490adbd4 user: tangent tags: os8-run-python3
15:17
Assorted small updates to doc/class-simh.md check-in: b4b89f2f1e user: tangent tags: trunk
13:03
Added TECO Pi macro creation credit to bin/teco-pi-demo. check-in: 57ccbf54aa user: tangent tags: trunk

Changes to Makefile.in.
247
248
249
250
251
252
253



254
255
256
257
258
259
260
261
262
263
264
265
266


267
268

269
270
271
272
273
274
275
    @srcdir@/examples/Makefile.in \
    @srcdir@/src/Makefile.in \
    @srcdir@/src/cc8/Makefile.in \
    @srcdir@/src/SIMH/Makefile.in \
    @srcdir@/src/SIMH/PDP8/Makefile.in
INFILES = \
	@srcdir@/bin/os8-cp.in \



	@srcdir@/bin/pidp8i.in \
	@srcdir@/boot/0.script.in \
	@srcdir@/boot/2.script.in \
	@srcdir@/boot/3.script.in \
	@srcdir@/boot/4.script.in \
	@srcdir@/boot/6.script.in \
	@srcdir@/boot/7.script.in \
	@srcdir@/boot/run.script.in \
	@srcdir@/boot/run-v3f.script.in \
	@srcdir@/boot/tss8.script.in \
	@srcdir@/etc/pidp8i.service.in \
	@srcdir@/etc/sudoers.in \
	@srcdir@/etc/usb-mount@.service.in \


	@srcdir@/src/pidp8i/gpio-common.c.in \
	@srcdir@/tools/simh-update.in \

	$(OS8RUN_INFILES)
OS8RUN_OUTFILES := $(subst @srcdir@/,,$(OS8RUN_INFILES))
OS8RUN_OUTFILES := $(subst .in,,$(OS8RUN_OUTFILES))
PRECIOUS_OUTFILES := $(subst @srcdir@/,,$(PRECIOUS_INFILES))
PRECIOUS_OUTFILES := $(subst .in,,$(PRECIOUS_OUTFILES))
OUTFILES := $(subst @srcdir@/,,$(INFILES))
OUTFILES := $(subst .in,,$(OUTFILES))







>
>
>













>
>


>







247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
    @srcdir@/examples/Makefile.in \
    @srcdir@/src/Makefile.in \
    @srcdir@/src/cc8/Makefile.in \
    @srcdir@/src/SIMH/Makefile.in \
    @srcdir@/src/SIMH/PDP8/Makefile.in
INFILES = \
	@srcdir@/bin/os8-cp.in \
	@srcdir@/bin/os8-run.in \
	@srcdir@/bin/teco-pi-demo.in \
	@srcdir@/bin/txt2os8.in \
	@srcdir@/bin/pidp8i.in \
	@srcdir@/boot/0.script.in \
	@srcdir@/boot/2.script.in \
	@srcdir@/boot/3.script.in \
	@srcdir@/boot/4.script.in \
	@srcdir@/boot/6.script.in \
	@srcdir@/boot/7.script.in \
	@srcdir@/boot/run.script.in \
	@srcdir@/boot/run-v3f.script.in \
	@srcdir@/boot/tss8.script.in \
	@srcdir@/etc/pidp8i.service.in \
	@srcdir@/etc/sudoers.in \
	@srcdir@/etc/usb-mount@.service.in \
	@srcdir@/lib/os8script.py.in \
	@srcdir@/lib/simh.py.in \
	@srcdir@/src/pidp8i/gpio-common.c.in \
	@srcdir@/tools/simh-update.in \
	@srcdir@/tools/test-os8-send-file.in \
	$(OS8RUN_INFILES)
OS8RUN_OUTFILES := $(subst @srcdir@/,,$(OS8RUN_INFILES))
OS8RUN_OUTFILES := $(subst .in,,$(OS8RUN_OUTFILES))
PRECIOUS_OUTFILES := $(subst @srcdir@/,,$(PRECIOUS_INFILES))
PRECIOUS_OUTFILES := $(subst .in,,$(PRECIOUS_OUTFILES))
OUTFILES := $(subst @srcdir@/,,$(INFILES))
OUTFILES := $(subst .in,,$(OUTFILES))
Changes to auto.def.
443
444
445
446
447
448
449
450










451
452







453
454


455
456
457
458
459

460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482

# Get host, user, and date info for use by media/os8/init.tx.
catch {exec hostname} host
set user $::env(USER)
define BUILDUSER "$user@$host"
define BUILDTS [clock format [clock seconds] -format "%Y.%m.%d at %T %Z"]

# The os8-run script requires Python and some non-core modules.










set status [catch {exec python -c exit} result]
if {$status != 0} {







    user-error "Python 2 does not appear to be installed here.  It is required."
}


msg-result "Python 2 is installed here."
set status [catch {exec python -c "import pexpect" 2> /dev/null} result]
if {$status != 0} {
    set msg "The Python pexpect module is not installed here.  Fix with\n\n"
    append msg "    sudo apt install python-pip\n"

    append msg "    sudo pip install pexpect\n"
    append msg "\nOR:\n"
    append msg "\n    sudo easy_install pexpect\n"
    append msg "\nOR:\n"
    append msg "\n    sudo apt install python-pexpect\n"
    user-error $msg
}
msg-result "Python module pexpect is installed here."
set status [catch {exec python -c "import pkg_resources" 2> /dev/null} result]
if {$status != 0} {
    set msg "The Python pkg_resources module is not installed here.  Fix with\n"
    append msg "\n    sudo pip install pkg_resources\n"
    append msg "\nOR:\n"
    append msg "\n    sudo easy_install pkg_resources\n"
    append msg "\nOR:\n"
    append msg "\n    sudo apt install python-pkg-resources\n"
    user-error $msg
}
msg-result "Python module pkg_resources is installed here."

# Check for Perl and that it can run the test corpus builder.  Not fatal
# if it can't, since only developers and deep testers need it.
set status [catch {exec perl -e exit} result]







|
>
>
>
>
>
>
>
>
>
>
|
|
>
>
>
>
>
>
>
|

>
>
|
|


|
>
|



|



|


|



|







443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502

# Get host, user, and date info for use by media/os8/init.tx.
catch {exec hostname} host
set user $::env(USER)
define BUILDUSER "$user@$host"
define BUILDTS [clock format [clock seconds] -format "%Y.%m.%d at %T %Z"]

# The os8-run script requires Python 2 or 3 and some non-core modules.
set status [catch {exec python3 -c exit} result]
if {$status == 0} {
    set pyver 3
    set pycmd "python3"
} else {
    set status [catch {exec python2 -c exit} result]
    if {$status == 0} {
        set pyver 2
        set pycmd "python2"
    } else {
        set status [catch {exec python -c exit} result]
        if {$status == 0} {
            set status [catch {exec python --version | grep -q 'Version 2'} result]
            set pyver [expr $status == 0 ? 2 : 3]
            set pycmd "python"
        }
    }
}
if {$pyver == ""} {
    user-error "Python does not appear to be installed here.  It is required."
}
define PYCMD $pycmd
define PYVER $pyver
msg-result "Python $pyver is installed here as '$pycmd'."
set status [catch {exec $pycmd -c "import pexpect" 2> /dev/null} result]
if {$status != 0} {
    set msg "The Python pexpect module is not installed here.  Fix with\n\n"
    append msg "    sudo apt install $pycmd-pip\n"
    append msg "\nTHEN:\n"
    append msg "    sudo pip$pyver install pexpect\n"
    append msg "\nOR:\n"
    append msg "\n    sudo easy_install pexpect\n"
    append msg "\nOR:\n"
    append msg "\n    sudo apt install $pycmd-pexpect\n"
    user-error $msg
}
msg-result "Python module pexpect is installed here."
set status [catch {exec $pycmd -c "import pkg_resources" 2> /dev/null} result]
if {$status != 0} {
    set msg "The Python pkg_resources module is not installed here.  Fix with\n"
    append msg "\n    sudo pip$pyver install pkg_resources\n"
    append msg "\nOR:\n"
    append msg "\n    sudo easy_install pkg_resources\n"
    append msg "\nOR:\n"
    append msg "\n    sudo apt install $pycmd-pkg-resources\n"
    user-error $msg
}
msg-result "Python module pkg_resources is installed here."

# Check for Perl and that it can run the test corpus builder.  Not fatal
# if it can't, since only developers and deep testers need it.
set status [catch {exec perl -e exit} result]
541
542
543
544
545
546
547



548
549
550
551
552
553
554
555
556
557
558
559
560
561

562
563
564

565
566
567
568
569
570
571
572
573
574

575
576


# The boot/common.script must precede the rest of boot/*, because they
# @include the output version.
make-config-header src/config.h \
    -auto {ENABLE_* HAVE_* PACKAGE_* SIZEOF_*} \
    -bare {ILS_MODE PCB_*}
make-template bin/pidp8i.in
make-template bin/os8-cp.in



make-template boot/common.script.in
make-template boot/0.script.in
make-template boot/2.script.in
make-template boot/3.script.in
make-template boot/4.script.in
make-template boot/6.script.in
make-template boot/7.script.in
make-template boot/run.script.in
make-template boot/run-v3f.script.in
make-template boot/tss8.script.in
make-template etc/pidp8i.service.in
make-template etc/sudoers.in
make-template etc/usb-mount@.service.in
make-template examples/Makefile.in

make-template lib/pidp8i/__init__.py.in
make-template lib/pidp8i/dirs.py.in
make-template lib/pidp8i/ips.py.in

make-template media/os8/init.tx.in
make-template media/os8/3finit.tx.in
make-template src/Makefile.in
make-template src/cc8/Makefile.in
make-template src/cc8/os8/Makefile.in
make-template src/pidp8i/gpio-common.c.in
make-template src/pidp8i/main.c.in
make-template src/SIMH/Makefile.in
make-template src/SIMH/PDP8/Makefile.in
make-template tools/simh-update.in

make-template Makefile.in
exec chmod +x "$builddir/bin/pidp8i" "$builddir/tools/simh-update" "$builddir/bin/os8-cp"









>
>
>














>



>










>

|
>
>
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
# The boot/common.script must precede the rest of boot/*, because they
# @include the output version.
make-config-header src/config.h \
    -auto {ENABLE_* HAVE_* PACKAGE_* SIZEOF_*} \
    -bare {ILS_MODE PCB_*}
make-template bin/pidp8i.in
make-template bin/os8-cp.in
make-template bin/os8-run.in
make-template bin/teco-pi-demo.in
make-template bin/txt2os8.in
make-template boot/common.script.in
make-template boot/0.script.in
make-template boot/2.script.in
make-template boot/3.script.in
make-template boot/4.script.in
make-template boot/6.script.in
make-template boot/7.script.in
make-template boot/run.script.in
make-template boot/run-v3f.script.in
make-template boot/tss8.script.in
make-template etc/pidp8i.service.in
make-template etc/sudoers.in
make-template etc/usb-mount@.service.in
make-template examples/Makefile.in
make-template lib/os8script.py.in
make-template lib/pidp8i/__init__.py.in
make-template lib/pidp8i/dirs.py.in
make-template lib/pidp8i/ips.py.in
make-template lib/simh.py.in
make-template media/os8/init.tx.in
make-template media/os8/3finit.tx.in
make-template src/Makefile.in
make-template src/cc8/Makefile.in
make-template src/cc8/os8/Makefile.in
make-template src/pidp8i/gpio-common.c.in
make-template src/pidp8i/main.c.in
make-template src/SIMH/Makefile.in
make-template src/SIMH/PDP8/Makefile.in
make-template tools/simh-update.in
make-template tools/test-os8-send-file.in
make-template Makefile.in
foreach f [concat [glob "$builddir/bin/*"] [glob "$builddir/tools/*"]] {
    file attributes $f -permissions +x
}
Changes to bin/os8-cp.in.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env 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
|







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env @PYCMD@
# -*- 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-2019 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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
_expandable_re = re.compile ("^\$([^/\s]+)/(\S*)$")


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


#### path_expand #######################################################







|













|







108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
_expandable_re = re.compile ("^\$([^/\s]+)/(\S*)$")


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


#### path_expand #######################################################
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231

232
233
234
235
236
237
238
239
240
241
242
243
244
# A dollar sign appearing in a POSIX pathname is expanded with substitutions
# from dirs.py.

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:
        abort_prog ("Could not parse attach spec: " + source)
      parse_attach (action_plan, m, path_expand(destination))
    else:
      if len(option) != 1:
        print "Format options are only 1 letter in size. Ignoring line: " + ioline

      elif option[0] not in _valid_pip_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:
        append_copy(action_plan, option, source, destination)

  return action_plan


#### is_directory ######################################################







|











|





|








|




|
>

|

|

|







192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# A dollar sign appearing in a POSIX pathname is expanded with substitutions
# from dirs.py.

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:
        abort_prog ("Could not parse attach spec: " + source)
      parse_attach (action_plan, m, path_expand(destination))
    else:
      if len(option) != 1:
        print("Format options are only 1 letter in size. " + \
                "Ignoring line: " + ioline)
      elif option[0] not in _valid_pip_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:
        append_copy(action_plan, option, source, destination)

  return action_plan


#### is_directory ######################################################
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# device name, lacking a file name part after it.  Otherwise, we use the
# local OS's "is a directory" path check.

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)


#### has_os8_wildcards #################################################
# Returns True if the passed file name has OS/8 style wildcards.








|




|







253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# device name, lacking a file name part after it.  Otherwise, we use the
# local OS's "is a directory" path check.

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)


#### has_os8_wildcards #################################################
# Returns True if the passed file name has OS/8 style wildcards.

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
  file_list = []
  lines = before.split("\r")
  for line in lines[1:]:    # First line is our command. Skip it.
    line = line.strip()
    if line == "": continue
    m = re.match("(\S+)\s*\.(\S+)", line)
    if m == None: continue
    # if DEBUG: print "file_list_from_expect: group 1: " + m.group(1) + ", group 2: " + m.group(2)
    fname = m.group(1) + "." + m.group(2)
    file_list.append(fname)
  return file_list


#### append_copy #######################################################
# Append a copy control array to the action_plan







|







285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
  file_list = []
  lines = before.split("\r")
  for line in lines[1:]:    # First line is our command. Skip it.
    line = line.strip()
    if line == "": continue
    m = re.match("(\S+)\s*\.(\S+)", line)
    if m == None: continue
    # if DEBUG: print("file_list_from_expect: group 1: " + m.group(1) + ", group 2: " + m.group(2))
    fname = m.group(1) + "." + m.group(2)
    file_list.append(fname)
  return file_list


#### append_copy #######################################################
# Append a copy control array to the action_plan
308
309
310
311
312
313
314
315

316
317
318
319
320
321
322

  if ":" in destination:
    if copy_type == "from":
      copy_type = "within"
    else:
      copy_type = "into"
    if "/" in destination:
      print "append_copy, into: Illegal OS/8 file spec containing a slash:" + destination

      sys.exit(-1)
    destination = destination.upper()
    
    if copy_type == "":
      abort_prog ("append_copy: No OS/8 file spec found with source: " + source + ", destination: " + destination)
  else: destination = path_expand(destination)








|
>







309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324

  if ":" in destination:
    if copy_type == "from":
      copy_type = "within"
    else:
      copy_type = "into"
    if "/" in destination:
      print("append_copy, into: Illegal OS/8 file spec containing " + \
            "a slash:" + destination)
      sys.exit(-1)
    destination = destination.upper()
    
    if copy_type == "":
      abort_prog ("append_copy: No OS/8 file spec found with source: " + source + ", destination: " + destination)
  else: destination = path_expand(destination)

511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
    arg = sys.argv[idx]

    # First the simple bit set options
    if arg == "-d":
      DEBUG = True
    elif arg == "-h":
      if VERBOSE:
        print VERBOSE_USAGE
      else:
        print USAGE
      sys.exit(0)
    elif arg == "-q":
      QUIET = True
    elif arg == "-v":
      VERBOSE = 1

    # 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 " + \
          _pip_option_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.")







|

|










|
|







513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
    arg = sys.argv[idx]

    # First the simple bit set options
    if arg == "-d":
      DEBUG = True
    elif arg == "-h":
      if VERBOSE:
        print(VERBOSE_USAGE)
      else:
        print(USAGE)
      sys.exit(0)
    elif arg == "-q":
      QUIET = True
    elif arg == "-v":
      VERBOSE = 1

    # 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 " + \
          _pip_option_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.")
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567

568
569
570
571
572
573

574

575

576

577
578
579
580
581
582
583
584
585
586

587

588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
          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 POSIX.  Glob it.
          if DEBUG: print arg + " is POSIX."
          more_files = glob.glob(arg)
          if more_files == []:

            if DEBUG: print "No more files in POSIX Glob. Our file is: " + arg
            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

              if DEBUG: print "Globber: Setting initial source: " + source + " and mode: " + first_mode

            elif filespec_seen == 1:

              if DEBUG: print "Globber: Setting initial destination: " + destination

              destination = new_file  
            else:
              if DEBUG: "Globber: Appending destination to list. New file is: " + new_file
              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

            if DEBUG: print "Setting initial source: " + source + " and mode: " + first_mode

          elif filespec_seen == 1:
            destination = arg
            if DEBUG: print "Setting initial destination: " + destination
          else:
            file_and_mode_list.append([mode_opt, destination])
            destination = arg
            if DEBUG: "Appending destination to list. New file is: " + new_file
          filespec_seen += 1

    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,







|






|


>
|





>
|
>

>
|
>










>
|
>


|

















|







553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
          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 POSIX.  Glob it.
          if DEBUG: print(arg + " is POSIX.")
          more_files = glob.glob(arg)
          if more_files == []:
            if DEBUG:
              print("No more files in POSIX Glob. Our file is: " + arg)
            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
              if DEBUG:
                print("Globber: Setting initial source: " + source + \
                    " and mode: " + first_mode)
            elif filespec_seen == 1:
              if DEBUG:
                print("Globber: Setting initial destination: " + \
                    destination)
              destination = new_file  
            else:
              if DEBUG: "Globber: Appending destination to list. New file is: " + new_file
              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
            if DEBUG:
              print("Setting initial source: " + source + \
                  " and mode: " + first_mode)
          elif filespec_seen == 1:
            destination = arg
            if DEBUG: print("Setting initial destination: " + destination)
          else:
            file_and_mode_list.append([mode_opt, destination])
            destination = arg
            if DEBUG: "Appending destination to list. New file is: " + new_file
          filespec_seen += 1

    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,
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672

673
674

675
676
677
678
679
680
681
682
683
684
685
686
687
688
689

690

691
692
693

694

695
696
697
698
699
700
701
702
703
704
705
706
707

708

709
710
711
712
713
714
715
716
717
718
719
720
721
722
723

724
725
726
727
728
729
730
731
732

733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
#### main ##############################################################

def main ():

  action_plan = parse_args()
  if action_plan == None:
    abort_prog ("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))
  s.set_logfile (open ("logfile.txt", 'w'))
  if VERBOSE: s.verbose = True
  
  # Perform sys attach
  att_spec = action_plan["sys"]
  if att_spec == None: att_spec = _default_att_spec
  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.")
  if VERBOSE or DEBUG:
    print "Attaching " + simh_boot_dev + " to " + imagename
  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):

      if VERBOSE or DEBUG: print "Modifying existing " + simh_dev + " image " + imagename
    else:

      if VERBOSE or DEBUG: 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)
    if VERBOSE or DEBUG:
      print "Attaching " + simh_dev + " to " + imagename      
    s.send_cmd ("att " + simh_dev + " " + imagename)

  if VERBOSE or DEBUG: 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_partitions[os8dev]:
        os8name = os8dev + partition + att_spec[1] + ":"

        if VERBOSE or DEBUG: print "Initializing directory of " + os8name + " in " + imagename

        s.os8_send_cmd ('\\.', "ZERO " + os8name)
    else:
      os8name = os8dev + att_spec[1] + ":"

      if VERBOSE or DEBUG: 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]
    copy_type = do_copy[3]
    if mode_opt in _option_to_pip:
      pip_option = _option_to_pip[mode_opt]
    else:
      abort_prog ("Unrecognized mode option: " + mode_opt)


    if DEBUG: print "Source: " + source + ", Destination: " + destination + ", Mode: " + mode_opt + "."

    # Is this "from" OS/8 to POSIX, "into" OS/8 from POSIX or "within" OS/8?
    # "into" -- 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. We've already done POSIX globing.
    # "within" -- Use COPY.
    
    if copy_type == "into":
      s.os8_pip_to(source, destination, pip_option)
    elif copy_type == "from":
      if has_os8_wildcards(source):
        # Split off device from source:
        os8dev = source[0:source.index(":")+1]
        if DEBUG: print "Wild card dev: " + os8dev
        # Use OS/8 Direct to enumerate our input files.

        if DEBUG: print "Calling OS/8 DIRECT on wild card filespec: " + source
        s.os8_send_cmd ('\\.', "DIR " + source + "/F=1")
        # Now harvest direct output.  One file per line.  Ignore blank lines.
        # Maybe parse the FREE BLOCKS Output.
        # Done when we see a dot.
        s._child.expect("\d+\s+FREE BLOCKS")
        files = file_list_from_expect(s._child.before)
        for filename in files:
          if VERBOSE or DEBUG:

            print "Wildcard call os8_pip_from: copy from: {" + os8dev + "}{" + filename + "}" + \
              " to: " + destination + ", mode: " + pip_option 
          s.os8_pip_from(os8dev + filename, destination, pip_option)
      else:
        if VERBOSE or DEBUG:
          print "Call os8_pip_from: copy from: " + source + " to " + destination + \
            ", mode: " + pip_option 
        s.os8_pip_from(source, destination, pip_option)
    elif copy_type == "within":
      if VERBOSE or DEBUG:
        print "Call COPY of: " + source + " to " + destination
      s.os8_send_cmd ('\\.', "COPY " + destination + "< " + source)
    else:
      abort_prog ("Unrecognized copy type: " + copy_type)  # Should never happen.
      
  # 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.
    if VERBOSE or DEBUG:
      print "Detaching " + simh_dev
    s.send_cmd ("det " + simh_dev)
  if VERBOSE or DEBUG:
    print "Detaching " + simh_boot_dev
  s.send_cmd ("det " + simh_boot_dev)

  # And shut down the simulator.
  if VERBOSE or DEBUG:
    print "Quitting simh."
  s.send_cmd ('quit')

if __name__ == "__main__": main()







|





|

|
|










|









>
|

>
|



|


|







>
|
>



>
|
>













>
|
>













|

>
|








>
|
|



|
|



|









|


|




|



645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
#### main ##############################################################

def main ():

  action_plan = parse_args()
  if action_plan == None:
    abort_prog ("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 (), 'wb', 0))
  s.set_logfile (open ("logfile.txt", 'wb'))
  if VERBOSE: s.verbose = True
  
  # Perform sys attach
  att_spec = action_plan["sys"]
  if att_spec == None: att_spec = _default_att_spec
  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.")
  if VERBOSE or DEBUG:
    print("Attaching " + simh_boot_dev + " to " + imagename)
  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):
      if VERBOSE or DEBUG:
        print("Modifying existing " + simh_dev + " image " + imagename)
    else:
      if VERBOSE or DEBUG:
        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)
    if VERBOSE or DEBUG:
      print("Attaching " + simh_dev + " to " + imagename)
    s.send_cmd ("att " + simh_dev + " " + imagename)

  if VERBOSE or DEBUG: 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_partitions[os8dev]:
        os8name = os8dev + partition + att_spec[1] + ":"
        if VERBOSE or DEBUG:
          print("Initializing directory of " + os8name + " in " + \
              imagename)
        s.os8_send_cmd ('\\.', "ZERO " + os8name)
    else:
      os8name = os8dev + att_spec[1] + ":"
      if VERBOSE or DEBUG:
        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]
    copy_type = do_copy[3]
    if mode_opt in _option_to_pip:
      pip_option = _option_to_pip[mode_opt]
    else:
      abort_prog ("Unrecognized mode option: " + mode_opt)

    if DEBUG:
      print("Source: " + source + ", Destination: " + destination + \
          ", Mode: " + mode_opt + ".")
    # Is this "from" OS/8 to POSIX, "into" OS/8 from POSIX or "within" OS/8?
    # "into" -- 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. We've already done POSIX globing.
    # "within" -- Use COPY.
    
    if copy_type == "into":
      s.os8_pip_to(source, destination, pip_option)
    elif copy_type == "from":
      if has_os8_wildcards(source):
        # Split off device from source:
        os8dev = source[0:source.index(":")+1]
        if DEBUG: print("Wild card dev: " + os8dev)
        # Use OS/8 Direct to enumerate our input files.
        if DEBUG:
          print("Calling OS/8 DIRECT on wild card filespec: " + source)
        s.os8_send_cmd ('\\.', "DIR " + source + "/F=1")
        # Now harvest direct output.  One file per line.  Ignore blank lines.
        # Maybe parse the FREE BLOCKS Output.
        # Done when we see a dot.
        s._child.expect("\d+\s+FREE BLOCKS")
        files = file_list_from_expect(s._child.before)
        for filename in files:
          if VERBOSE or DEBUG:
            print("Wildcard call os8_pip_from: copy from: " + \
                "{" + os8dev + "}{" + filename + "}" + \
                " to: " + destination + ", mode: " + pip_option)
          s.os8_pip_from(os8dev + filename, destination, pip_option)
      else:
        if VERBOSE or DEBUG:
          print("Call os8_pip_from: copy from: " + source + " to " + \
              destination + ", mode: " + pip_option)
        s.os8_pip_from(source, destination, pip_option)
    elif copy_type == "within":
      if VERBOSE or DEBUG:
        print("Call COPY of: " + source + " to " + destination)
      s.os8_send_cmd ('\\.', "COPY " + destination + "< " + source)
    else:
      abort_prog ("Unrecognized copy type: " + copy_type)  # Should never happen.
      
  # 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.
    if VERBOSE or DEBUG:
      print("Detaching " + simh_dev)
    s.send_cmd ("det " + simh_dev)
  if VERBOSE or DEBUG:
    print("Detaching " + simh_boot_dev)
  s.send_cmd ("det " + simh_boot_dev)

  # And shut down the simulator.
  if VERBOSE or DEBUG:
    print("Quitting simh.")
  s.send_cmd ('quit')

if __name__ == "__main__": main()
Deleted bin/os8-run.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# Script runner for OS/8 under SIMH.
# The library module os8script.py does the heavy lifting.
#
# 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 sys

sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Remaining Python core modules
import subprocess
import string
import re
import shutil
import argparse
from itertools import chain

# Our local modules
from pidp8i    import *
from simh      import *
from os8script import *


#### GLOBALS AND CONSTANTS #############################################

DEBUG = False
VERBOSE = False
VERY_VERBOSE = False
QUIET = False
SCRIPT_FILE = ""

USAGE = "usage: "  + os.path.basename (__file__) + \
       " [-h] [-d] [-v] [-vv] [--enable enable_option] ... \n\t[--disable disable_option] ... " + \
       "\n \tscript [script] ... "

_en_dis_arg_re = re.compile("^(enable|disable)_(\S+)$")


def opt_set(en_dis, option, options_enabled, options_disabled):
  if option == None or en_dis == None: return
  # print en_dis + " " + option
  if en_dis == "enable":
    if option not in options_enabled:
      options_enabled.append(option)
    if option in options_disabled:
        options_disabled.remove(option)
  elif en_dis == "disable":
    if option not in options_disabled:
      options_disabled.append(option)
    if option in options_enabled:
        options_enabled.remove(option)
  else: return


def add_bool (self, *args, **kwargs):
  kwargs['action']  = 'store_true'
  kwargs['default'] = False
  self.add_argument (*args, **kwargs)
  

#### parse_args ########################################################
    
def parse_args (script_files, options_enabled, options_disabled):
  global DEBUG
  global VERBOSE
  global VERY_VERBOSE
  global USAGE

  enable_usage = ""
  disable_usage = ""

  idx = 1
  numargs = len(sys.argv)

  if numargs < 2:
    print USAGE
    sys.exit(-1)

  # Add arguments corresponding to --*-os8-* configure script options
  max_obn_len = 0
  for obn, vals in os8opts.opts.iteritems():
    max_obn_len = max(max_obn_len, len(obn))
  for obn, vals in os8opts.opts.iteritems():
    if vals[0]:
      # Enable option
      pad_str = (max_obn_len - len (obn)) * " "
      new_usage = "    " + obn + ": " + pad_str + vals[1] + "\n"
      disable_usage += new_usage
    else:
      # Disable option
      pad_str = (max_obn_len - len (obn)) * " "
      new_usage = "    " + obn + ": " + pad_str + vals[1] + "\n"
      enable_usage += new_usage

  if enable_usage != "":
    USAGE += "\n  Known enable options: \n"
    USAGE += enable_usage

  if disable_usage != "":
    USAGE += "\n  Known disable options: \n"
    USAGE += disable_usage
    
  while idx < numargs:
    arg = sys.argv[idx]
    # print "idx: " + str(idx) + ", arg: " + arg
    # print "Files: " + str(script_files)
    # print "Options: " + str(options_enabled)
    if arg == "-d" or arg == "--debug":
      DEBUG = True
    elif arg == "-h" or arg == "--help":
      print USAGE
      sys.exit(0)
    elif arg == "-v" or arg == "--verbose":
      VERBOSE = 1
    elif arg == "-vv" or arg == "--very-verbose":
      VERY_VERBOSE = 1
    elif arg == "--enable":
      idx +=1
      if idx == numargs:
        print "expecting an option but got none."
      else:
        option = sys.argv[idx]
        # Only add the option once
        if option not in options_enabled: options_enabled.append(option)
    elif arg == "--disable":
      idx +=1
      if idx == numargs:
        print "expecting an option but got none."
      else:
        option = sys.argv[idx]
        # Only add the option once
        if option not in options_disabled: options_disabled.append(option)
    else:
      script_files.append(arg)
    idx += 1
      


#### main ##############################################################
# Program entry point.  Parses the command line and drives the above.

def main ():
  script_files = []
  options_enabled = []
  options_disabled = []

  parse_args (script_files, options_enabled, options_disabled)
  if len(script_files) == 0:
    print "Need a script file to run."
    sys.exit(-1)

  if VERBOSE:
    print "script_files: " + str(script_files)
  if DEBUG:
    print "options_enabled" + str(options_enabled)
    print "options_disabled" + str(options_disabled)
  
  # Append SIMH and OS/8 output to a file by default.
  #
  # We append because we're run twice in each test directory via the
  # os8-sys Makefile target called by test-os8-run, once for the "dist"
  # media and once for the actual RK05 bootable media.  So, the second
  # run must append its logs to the first run's log file.
  #
  # Send the log info to the console instead of the progress messages if
  # -v was given.  See https://stackoverflow.com/questions/21239338
  s = simh (dirs.build, True)
  if VERBOSE: s.verbose = True
  s.set_logfile (open (dirs.log + 'os8-run' + '.log', 'a') \
      if not VERY_VERBOSE else os.fdopen (sys.stdout.fileno (), 'w', 0))

  os8 = os8script (s, options_enabled, options_disabled, verbose=VERBOSE, debug=DEBUG)

  for script_file in script_files:
    if VERBOSE:
      print os.path.basename (__file__) + " -- Language Version: " + os8.lang_version

    print "Running script file: " + script_file
    os8.run_script_file (script_file)

  # After all scripts are done, we remove any scratch files,
  # detach any mounted devices, and shut down simh gracefully.

  for filename in os8.scratch_list:
    if os8.verbose: print "Deleting scratch_copy: " + filename
    os.remove(filename)
    
  s.send_cmd ("detach all")

  s.quit ()
  if VERBOSE: print "Done!"

  
if __name__ == "__main__":
    main()
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































































































































































































































































































































Added bin/os8-run.in.








































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# Script runner for OS/8 under SIMH.
# The library module os8script.py does the heavy lifting.
#
# See USAGE message below for details.
#
# Copyright © 2018-2019 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 sys

sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Remaining Python core modules
import subprocess
import string
import re
import shutil
import argparse
from itertools import chain

# Our local modules
from pidp8i    import *
from simh      import *
from os8script import *


#### GLOBALS AND CONSTANTS #############################################

DEBUG = False
VERBOSE = False
VERY_VERBOSE = False
QUIET = False
SCRIPT_FILE = ""

USAGE = "usage: "  + os.path.basename (__file__) + \
       " [-h] [-d] [-v] [-vv] [--enable enable_option] ... \n\t[--disable disable_option] ... " + \
       "\n \tscript [script] ... "

_en_dis_arg_re = re.compile("^(enable|disable)_(\S+)$")


def opt_set(en_dis, option, options_enabled, options_disabled):
  if option == None or en_dis == None: return
  # print(en_dis + " " + option)
  if en_dis == "enable":
    if option not in options_enabled:
      options_enabled.append(option)
    if option in options_disabled:
        options_disabled.remove(option)
  elif en_dis == "disable":
    if option not in options_disabled:
      options_disabled.append(option)
    if option in options_enabled:
        options_enabled.remove(option)
  else: return


def add_bool (self, *args, **kwargs):
  kwargs['action']  = 'store_true'
  kwargs['default'] = False
  self.add_argument (*args, **kwargs)
  

#### parse_args ########################################################
    
def parse_args (script_files, options_enabled, options_disabled):
  global DEBUG
  global VERBOSE
  global VERY_VERBOSE
  global USAGE

  enable_usage = ""
  disable_usage = ""

  idx = 1
  numargs = len(sys.argv)

  if numargs < 2:
    print(USAGE)
    sys.exit(-1)

  # Add arguments corresponding to --*-os8-* configure script options
  max_obn_len = 0
  for obn, vals in os8opts.opts.items():
    max_obn_len = max(max_obn_len, len(obn))
  for obn, vals in os8opts.opts.items():
    if vals[0]:
      # Enable option
      pad_str = (max_obn_len - len (obn)) * " "
      new_usage = "    " + obn + ": " + pad_str + vals[1] + "\n"
      disable_usage += new_usage
    else:
      # Disable option
      pad_str = (max_obn_len - len (obn)) * " "
      new_usage = "    " + obn + ": " + pad_str + vals[1] + "\n"
      enable_usage += new_usage

  if enable_usage != "":
    USAGE += "\n  Known enable options: \n"
    USAGE += enable_usage

  if disable_usage != "":
    USAGE += "\n  Known disable options: \n"
    USAGE += disable_usage
    
  while idx < numargs:
    arg = sys.argv[idx]
    # print("idx: " + str(idx) + ", arg: " + arg)
    # print("Files: " + str(script_files))
    # print("Options: " + str(options_enabled))
    if arg == "-d" or arg == "--debug":
      DEBUG = True
    elif arg == "-h" or arg == "--help":
      print(USAGE)
      sys.exit(0)
    elif arg == "-v" or arg == "--verbose":
      VERBOSE = 1
    elif arg == "-vv" or arg == "--very-verbose":
      VERY_VERBOSE = 1
    elif arg == "--enable":
      idx +=1
      if idx == numargs:
        print("expecting an option but got none.")
      else:
        option = sys.argv[idx]
        # Only add the option once
        if option not in options_enabled: options_enabled.append(option)
    elif arg == "--disable":
      idx +=1
      if idx == numargs:
        print("expecting an option but got none.")
      else:
        option = sys.argv[idx]
        # Only add the option once
        if option not in options_disabled: options_disabled.append(option)
    else:
      script_files.append(arg)
    idx += 1
      


#### main ##############################################################
# Program entry point.  Parses the command line and drives the above.

def main ():
  script_files = []
  options_enabled = []
  options_disabled = []

  parse_args (script_files, options_enabled, options_disabled)
  if len(script_files) == 0:
    print("Need a script file to run.")
    sys.exit(-1)

  if VERBOSE:
    print("script_files: " + str(script_files))
  if DEBUG:
    print("options_enabled" + str(options_enabled))
    print("options_disabled" + str(options_disabled))
  
  # Append SIMH and OS/8 output to a file by default.
  #
  # We append because we're run twice in each test directory via the
  # os8-sys Makefile target called by test-os8-run, once for the "dist"
  # media and once for the actual RK05 bootable media.  So, the second
  # run must append its logs to the first run's log file.
  #
  # Send the log info to the console instead of the progress messages if
  # -v was given.  See https://stackoverflow.com/questions/21239338
  s = simh (dirs.build, True)
  if VERBOSE: s.verbose = True
  s.set_logfile (open (dirs.log + 'os8-run' + '.log', 'ab') \
      if not VERY_VERBOSE else os.fdopen (sys.stdout.fileno (), 'wb', 0))

  os8 = os8script (s, options_enabled, options_disabled, verbose=VERBOSE, debug=DEBUG)

  for script_file in script_files:
    if VERBOSE:
      print(os.path.basename (__file__) + " -- Language Version: " + os8.lang_version)

    print("Running script file: " + script_file)
    os8.run_script_file (script_file)

  # After all scripts are done, we remove any scratch files,
  # detach any mounted devices, and shut down simh gracefully.

  for filename in os8.scratch_list:
    if os8.verbose: print("Deleting scratch_copy: " + filename)
    os.remove(filename)
    
  s.send_cmd ("detach all")

  s.quit ()
  if VERBOSE: print("Done!")

  
if __name__ == "__main__":
    main()
Deleted bin/teco-pi-demo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# teco-pi-demo - Starts the simulator with the OS/8, sends one of the
#   famous TECO "calculate pi" program to it, and starts it running at
#   a very slow rate of speed to act as a blinkenlights demo.
#
# Copyright © 2017-2019 by 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 sys
sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Other core modules we need
from datetime import datetime
import time

# Our local modules
from pidp8i import *
from simh   import *


#### main ##############################################################

def main ():
  # Check for command line flags
  benchmark = len (sys.argv) > 1 and sys.argv[1] == '-b'

  # Create the SIMH child instance and tell it where to send log output
  try:
    s = simh (dirs.build)
  except (RuntimeError) as e:
    print "Could not start simulator: " + e.message + '!'
    exit (1)
  s.set_logfile (os.fdopen (sys.stdout.fileno (), 'w', 0))

  # Find and boot the built OS/8 bin disk
  rk = os.path.join (dirs.os8mo, 'v3d.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  # Start TECO8 in the simulator under OS/8
  s.os8_send_cmd ('\\.', "R TECO")

  # The macro comes from http://www.iwriteiam.nl/HaPi_TECO_macro.html
  # and it was created by Stanley Rabinowitz.
  #
  # The 248 preceding "UN" in the first line of the macro is the number
  # of digits of pi to calculate.  That value was reached by experiment
  # as the largest value that runs without crashing TECO with a
  #
  #    ?MEM  STORAGE CAPACITY EXCEEDED
  #
  # error.  You can see that by increasing the value below, commenting
  # out the throttle setting below, and running the demo.  On a Pi 3, it
  # should take a bit over an hour to complete, if it doesn't error out.
  #
  # With the simulator throttled, generating 248 digits takes 17 years!
  #
  # That is based on generating 1 digit every ~16 seconds on a Pi 3 when
  # running unthrottled, roughly 8 MIPS.  When throttled to 59 IPS — or
  # 17ms per instruction, as below — you multiply the seconds by the
  # factor 8 MIPS / 59 IPS = ~136000, giving about 2.2 million seconds
  # per digit.  Multiplying that by 248 gives ~17 years.
  macro = [
    'GZ0J\UNQN"E 248UN \' BUH BUV HK',
    'QN< J BUQ QN*10/3UI',
    'QI< \+2*10+(QQ*QI)UA B L K QI*2-1UJ QA/QJUQ',
    'QA-(QQ*QJ)-2\ 10@I// -1%I >',
    'QQ/10UT QH+QT+48UW QW-58"E 48UW %V \' QV"N QV^T \' QWUV QQ-(QT*10)UH >',
    'QV^T @^A/',
    '/HKEX',
  ]

  # First and last lines are handled specially, so slice them off.
  first = macro.pop (0)
  last  = macro.pop ()

  # Send the first line of the macro; implicitly awaits 1st TECO prompt
  s.os8_send_cmd ('\\*', first)

  # Blindly send core lines of the macro; TECO gives no prompts for 'em.
  for line in macro: 
    s.os8_send_line (line)

  # Send last line of macro sans CR, followed by two Esc characters to
  # start it running.
  s.os8_send_str (last)     # not os8_send_line!
  s.os8_send_ctrl ('[')
  s.os8_send_ctrl ('[')

  if benchmark:
    # Run demo long enough to get a good sense of the simulator's
    # execution rate while unthrottled on this host hardware.  If
    # you don't run it long enough, the IPS value is untrustworthy.
    try:
      s.spin (10)
    except pexpect.TIMEOUT:
      # Explicitly shift back from OS/8 context to SIMH command context.
      # We cannot rely on class simh to do this automatically because it
      # expects to see a . prompt from the prior command, but we're
      # still in TECO here, so we must be explicit.
      s.os8_send_ctrl ('e')

      # Ask the simulator what IPS rate we ran that benchmark at.
      s.send_cmd ('show clocks')
      line = s.read_tail ('Execution Rate:')
      curr_ips = int (line.strip().replace(',', '').split(' ')[0])
      pf = open ('lib/pidp8i/ips.py', 'a')
      pf.write ('current = ' + str (curr_ips) + '    # ' + \
            str (datetime.today ()) + '\n')
      pf.close ()
      s.send_cmd ('quit')
      pdp_ratio = float (curr_ips) / ips.pdp8i
      rpi_ratio = float (curr_ips) / ips.raspberry_pi_b_plus
      print "\nYour system is " + format (rpi_ratio, '.1f') + \
          " times faster than a Raspberry Pi Model B+"
      print "or " + format (pdp_ratio, '.1f') + \
          " times faster than a PDP-8/I.\n"
  else:
    # Normal mode.  Tell SIMH and throttle down to a rate suitable for a
    # blinkenlights demo.  1/17 means SIMH runs one instruction then
    # waits for 17ms, yielding ~59 IPS.
    time.sleep (0.02)       # FIXME: simulator chokes on 'cont' without this
    s.os8_send_ctrl ('e')   # same justification as above
    s.send_cmd ('set throttle 1/17')

    # You can't hit Ctrl-E while running this script in the foreground
    # since pexpect takes over stdio.  Therefore, if you want to be able
    # to send commands to the simulator while the demo is running,
    # uncomment the line below, which will let you send commands to the
    # simulator via telnet.  From another terminal or SSH session:
    #
    #     $ telnet localhost 3141
    #
    # or from a remote machine:
    #
    #     $ telnet 192.168.1.2 3141
    #
    # It's disabled by default because SIMH can't be made to listen only
    # on localhost, so doing this may be a security risk.  SIMH disables
    # obviously-unsafe commands like ! on the remote console, but it is
    # possible some mischief may be possible via this path anyway.  It
    # could be used to exfiltrate a sensitive file via ATTACH, for one
    # thing.  For another, it's a potential DoS vector.
    #s.send_cmd ('set remote telnet=3141')

    # Let it run.  Never exits.
    s.send_cmd ('cont')
    s.spin ()


if __name__ == "__main__":
    main()
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































































































































Added bin/teco-pi-demo.in.


















































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# teco-pi-demo - Starts the simulator with the OS/8, sends one of the
#   famous TECO "calculate pi" program to it, and starts it running at
#   a very slow rate of speed to act as a blinkenlights demo.
#
# Copyright © 2017-2019 by 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 sys
sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Other core modules we need
from datetime import datetime
import time

# Our local modules
from pidp8i import *
from simh   import *


#### main ##############################################################

def main ():
  # Check for command line flags
  benchmark = len (sys.argv) > 1 and sys.argv[1] == '-b'

  # Create the SIMH child instance and tell it where to send log output
  try:
    s = simh (dirs.build)
  except (RuntimeError) as e:
    print "Could not start simulator: " + e.message + '!'
    exit (1)
  s.set_logfile (os.fdopen (sys.stdout.fileno (), 'wb', 0))

  # Find and boot the built OS/8 bin disk
  rk = os.path.join (dirs.os8mo, 'v3d.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  # Start TECO8 in the simulator under OS/8
  s.os8_send_cmd ('\\.', "R TECO")

  # The macro comes from http://www.iwriteiam.nl/HaPi_TECO_macro.html
  # and it was created by Stanley Rabinowitz.
  #
  # The 248 preceding "UN" in the first line of the macro is the number
  # of digits of pi to calculate.  That value was reached by experiment
  # as the largest value that runs without crashing TECO with a
  #
  #    ?MEM  STORAGE CAPACITY EXCEEDED
  #
  # error.  You can see that by increasing the value below, commenting
  # out the throttle setting below, and running the demo.  On a Pi 3, it
  # should take a bit over an hour to complete, if it doesn't error out.
  #
  # With the simulator throttled, generating 248 digits takes 17 years!
  #
  # That is based on generating 1 digit every ~16 seconds on a Pi 3 when
  # running unthrottled, roughly 8 MIPS.  When throttled to 59 IPS — or
  # 17ms per instruction, as below — you multiply the seconds by the
  # factor 8 MIPS / 59 IPS = ~136000, giving about 2.2 million seconds
  # per digit.  Multiplying that by 248 gives ~17 years.
  macro = [
    'GZ0J\UNQN"E 248UN \' BUH BUV HK',
    'QN< J BUQ QN*10/3UI',
    'QI< \+2*10+(QQ*QI)UA B L K QI*2-1UJ QA/QJUQ',
    'QA-(QQ*QJ)-2\ 10@I// -1%I >',
    'QQ/10UT QH+QT+48UW QW-58"E 48UW %V \' QV"N QV^T \' QWUV QQ-(QT*10)UH >',
    'QV^T @^A/',
    '/HKEX',
  ]

  # First and last lines are handled specially, so slice them off.
  first = macro.pop (0)
  last  = macro.pop ()

  # Send the first line of the macro; implicitly awaits 1st TECO prompt
  s.os8_send_cmd ('\\*', first)

  # Blindly send core lines of the macro; TECO gives no prompts for 'em.
  for line in macro: 
    s.os8_send_line (line)

  # Send last line of macro sans CR, followed by two Esc characters to
  # start it running.
  s.os8_send_str (last)     # not os8_send_line!
  s.os8_send_ctrl ('[')
  s.os8_send_ctrl ('[')

  if benchmark:
    # Run demo long enough to get a good sense of the simulator's
    # execution rate while unthrottled on this host hardware.  If
    # you don't run it long enough, the IPS value is untrustworthy.
    try:
      s.spin (10)
    except pexpect.TIMEOUT:
      # Explicitly shift back from OS/8 context to SIMH command context.
      # We cannot rely on class simh to do this automatically because it
      # expects to see a . prompt from the prior command, but we're
      # still in TECO here, so we must be explicit.
      s.os8_send_ctrl ('e')

      # Ask the simulator what IPS rate we ran that benchmark at.
      s.send_cmd ('show clocks')
      line = s.read_tail ('Execution Rate:')
      curr_ips = int (line.strip().replace(',', '').split(' ')[0])
      pf = open ('lib/pidp8i/ips.py', 'a')
      pf.write ('current = ' + str (curr_ips) + '    # ' + \
            str (datetime.today ()) + '\n')
      pf.close ()
      s.send_cmd ('quit')
      pdp_ratio = float (curr_ips) / ips.pdp8i
      rpi_ratio = float (curr_ips) / ips.raspberry_pi_b_plus
      print "\nYour system is " + format (rpi_ratio, '.1f') + \
          " times faster than a Raspberry Pi Model B+"
      print "or " + format (pdp_ratio, '.1f') + \
          " times faster than a PDP-8/I.\n"
  else:
    # Normal mode.  Tell SIMH and throttle down to a rate suitable for a
    # blinkenlights demo.  1/17 means SIMH runs one instruction then
    # waits for 17ms, yielding ~59 IPS.
    time.sleep (0.02)       # FIXME: simulator chokes on 'cont' without this
    s.os8_send_ctrl ('e')   # same justification as above
    s.send_cmd ('set throttle 1/17')

    # You can't hit Ctrl-E while running this script in the foreground
    # since pexpect takes over stdio.  Therefore, if you want to be able
    # to send commands to the simulator while the demo is running,
    # uncomment the line below, which will let you send commands to the
    # simulator via telnet.  From another terminal or SSH session:
    #
    #     $ telnet localhost 3141
    #
    # or from a remote machine:
    #
    #     $ telnet 192.168.1.2 3141
    #
    # It's disabled by default because SIMH can't be made to listen only
    # on localhost, so doing this may be a security risk.  SIMH disables
    # obviously-unsafe commands like ! on the remote console, but it is
    # possible some mischief may be possible via this path anyway.  It
    # could be used to exfiltrate a sensitive file via ATTACH, for one
    # thing.  For another, it's a potential DoS vector.
    #s.send_cmd ('set remote telnet=3141')

    # Let it run.  Never exits.
    s.send_cmd ('cont')
    s.spin ()


if __name__ == "__main__":
    main()
Deleted bin/txt2os8.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# Create a tu56 or rk05 image and fill it with ASCII files, i.e.  source code.
#
# It is intended to be be called manually when we have a POSIX
# directory full of ASCII files we want to bulk-copy into SIMH.
#
# The argument is taken both as the name of the image to create
# and the list of files to copy in.
#
# For now, it takes all input and produces all output in the
# current working directory.
#
# IMPORTANT:  Currently all input files are mindlessly passed through
# txt2ptp which transforms POSIX ASCII files to OS/8 ASCII files.
# It WILL mutilate non-ASCII files.
#
# This program is based on the old cc8-tu56-update program, last
# shipped by this project in release v20171222.
#
# Copyright © 2017 by Warren Young and Bill Cattey
#
# 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 sys
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


#### main ##############################################################

def main ():
  global progmsg

  # Set up the arg parser and use it to parse the command line.
  parser = argparse.ArgumentParser()
  parser.add_argument("name",
                      help="Create an OS/8 image from a list of ASCII files.")
  group = parser.add_mutually_exclusive_group(required=True)
  group.add_argument("--tu56", "-t", action="store_true")
  group.add_argument("--rk05a", "-ra", action="store_true")
  group.add_argument("--rk05b", "-rb", action="store_true")

  args = parser.parse_args()
  
  print "Filename: " + args.name
  if args.tu56:
    sdev = "dt0"
    os8dev = "DTA0:"
    imagename = args.name + ".tu56"
    stat_str = "DECtape"

  if args.rk05a:
    sdev = "rk1"
    os8dev = "RKA1:"
    imagename = args.name + ".rk05"
    stat_str = "partition A of"
    
  if args.rk05b:
    sdev = "rk1"
    os8dev = "RKB1:"
    imagename = args.name + ".rk05"
    stat_str = "partition B of"
  
  listname = args.name + ".list"

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

  # Attach a clean version of the image to the simulator
  if os.path.exists (imagename):
    print "Overwriting old " + stat_str + " image " + imagename

  s.send_cmd ("att " + sdev + " " + imagename)

  # Find and boot the bootable OS/8 disk.  Use the "patched" version
  # because that is what "make run" uses; we use that command to
  # inspect this script's work.
  rk = os.path.join (dirs.os8mo, 'os8v3d-patched.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  s.os8_send_cmd ('\\.', "ZERO " + os8dev)

  manifest = open (listname, "r")

  for line in manifest:
    src = line.strip()
    if src == "": continue
    if src[0] == '#': continue      # Allow commenting out files

    dest = src.upper ()
    s.os8_send_file (src, os8dev + dest)

  # Exit simulator nicely so that image detaches cleanly
  s.back_to_cmd ('\\.')
  s.send_cmd ("det " + sdev)
  s.send_cmd ('quit')


if __name__ == "__main__": main()
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































































































Added bin/txt2os8.in.














































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# Create a tu56 or rk05 image and fill it with ASCII files, i.e.  source code.
#
# It is intended to be be called manually when we have a POSIX
# directory full of ASCII files we want to bulk-copy into SIMH.
#
# The argument is taken both as the name of the image to create
# and the list of files to copy in.
#
# For now, it takes all input and produces all output in the
# current working directory.
#
# IMPORTANT:  Currently all input files are mindlessly passed through
# txt2ptp which transforms POSIX ASCII files to OS/8 ASCII files.
# It WILL mutilate non-ASCII files.
#
# This program is based on the old cc8-tu56-update program, last
# shipped by this project in release v20171222.
#
# Copyright © 2017-2019 by Warren Young and Bill Cattey
#
# 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 sys
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


#### main ##############################################################

def main ():
  global progmsg

  # Set up the arg parser and use it to parse the command line.
  parser = argparse.ArgumentParser()
  parser.add_argument("name",
                      help="Create an OS/8 image from a list of ASCII files.")
  group = parser.add_mutually_exclusive_group(required=True)
  group.add_argument("--tu56", "-t", action="store_true")
  group.add_argument("--rk05a", "-ra", action="store_true")
  group.add_argument("--rk05b", "-rb", action="store_true")

  args = parser.parse_args()
  
  print "Filename: " + args.name
  if args.tu56:
    sdev = "dt0"
    os8dev = "DTA0:"
    imagename = args.name + ".tu56"
    stat_str = "DECtape"

  if args.rk05a:
    sdev = "rk1"
    os8dev = "RKA1:"
    imagename = args.name + ".rk05"
    stat_str = "partition A of"
    
  if args.rk05b:
    sdev = "rk1"
    os8dev = "RKB1:"
    imagename = args.name + ".rk05"
    stat_str = "partition B of"
  
  listname = args.name + ".list"

  # 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 (), 'wb', 0))

  # Attach a clean version of the image to the simulator
  if os.path.exists (imagename):
    print "Overwriting old " + stat_str + " image " + imagename

  s.send_cmd ("att " + sdev + " " + imagename)

  # Find and boot the bootable OS/8 disk.  Use the "patched" version
  # because that is what "make run" uses; we use that command to
  # inspect this script's work.
  rk = os.path.join (dirs.os8mo, 'os8v3d-patched.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  s.os8_send_cmd ('\\.', "ZERO " + os8dev)

  manifest = open (listname, "r")

  for line in manifest:
    src = line.strip()
    if src == "": continue
    if src[0] == '#': continue      # Allow commenting out files

    dest = src.upper ()
    s.os8_send_file (src, os8dev + dest)

  # Exit simulator nicely so that image detaches cleanly
  s.back_to_cmd ('\\.')
  s.send_cmd ("det " + sdev)
  s.send_cmd ('quit')


if __name__ == "__main__": main()
Changes to doc/class-simh.md.
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102


## Logging

The next step is to tell the `s` object where to send its logging
output:

    s.set_logfile (os.fdopen (sys.stdout.fileno (), 'w', 0))

Contrast the corresponding line in `os8-run` which chooses whether to send
logging output to the console or to a log file:

    s.set_logfile (open (dirs.log + 'os8-run' + '.log', 'a') \
        if not VERY_VERBOSE else os.fdopen (sys.stdout.fileno (), 'w', 0))

Note that this more complicated scheme appends to the log file instead
of overwriting it because there are cases where `os8-run` gets run
more than once with different script inputs, so we want to preserve
the prior script outputs, not keep only the latest.









|




|
|







82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102


## Logging

The next step is to tell the `s` object where to send its logging
output:

    s.set_logfile (os.fdopen (sys.stdout.fileno (), 'wb', 0))

Contrast the corresponding line in `os8-run` which chooses whether to send
logging output to the console or to a log file:

    s.set_logfile (open (dirs.log + 'os8-run' + '.log', 'ab') \
        if not VERY_VERBOSE else os.fdopen (sys.stdout.fileno (), 'wb', 0))

Note that this more complicated scheme appends to the log file instead
of overwriting it because there are cases where `os8-run` gets run
more than once with different script inputs, so we want to preserve
the prior script outputs, not keep only the latest.


Deleted lib/os8script.py.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# simh-os8-script.py Library for scripting OS/8 under SIMH
# Contains validators and callers for os8 and simh commands to make
# it easier to create scripts.
#
# Copyright © 2017 by Jonathan Trites, William 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 sys
import tempfile
sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Python core modules we use
import re
from string import Template
import shutil
import subprocess

# Our local modules
from pidp8i import *
from simh import *

# Script Language Version
# Update this version number as the language evolves.
# Version 1.0 is the first public version.
LANG_VERSION = "1.0"

# Error Class Definitions ##############################################
# Enables us to use exceptions from within this module.

class Error(Exception):
  """Base Class for exceptions in this module."""
  pass

class InputError(Error):
  """Exception raised for errors in the input.

  Attributes:
  expr -- input expression in which the error occurred
  msg  -- explanation of the error
  """

  def __init__(self, msg):
    self.msg = msg

  def __str__(self):
    return self.msg

      
# Private globals ######################################################
# Visible within this file, but not to the outside.

# Identify a begin enabled/not_disabled command. group(1) contains either the enabled or
# disabled flag. Put the rest of the line in group(2)
_begin_en_dis_comm_re = re.compile ("^begin\s+(enabled|default|version)\s+(.+)$")
  
# Identify an end enabled/not_disabled command. group(1) contains either the enabled or
# disabled flag. Put the rest of the line in group(2)
_end_en_dis_comm_re = re.compile ("^end\s+(enabled|default|version)\s+(.+)$")
  
# Identify an end comm and put the rest of the line in group(1)
_end_comm_re = re.compile ("^end\s+(.+)?$")
  
# Identify an end option command and put the rest of the line in group(1)
_end_option_comm_re = re.compile ("^end\s+option\s+(.+)$")

# A valid version spec
_version_parse_re = re.compile ("^((\d+\.)*)?(\d+)?$")
  
# Name of the DECtape image file we create
_new_sys_tape_prefix = "system"

# Parser regexps used in patcher
_com_os8_parse_str = "^\.([a-zA-Z]+)\s*(.*)$"
_com_os8_parse = re.compile(_com_os8_parse_str)
_com_split_str = "^([a-zA-Z]+)\s*(.*)$"
_com_split_parse = re.compile(_com_split_str)
_odt_parse_str = "^([0-7]+)\s*/\s*(\S+)\s+([0-7;]+)"
_odt_parse = re.compile(_odt_parse_str)

# Put command keyword in group(1) and the rest is in group(3)
_comm_re_str = "^(\S+)(\s+(.+))?$"
_comm_re = re.compile(_comm_re_str)

# Identify an end comm and put the rest of the line in group(1)
_end_comm_re = re.compile ("^end\s+(.+)?$")

# Identify an end option command and put the rest of the line in group(1)
_end_option_comm_re = re.compile ("^end\s+option\s+(.+)$")

# Identify a begin command and put the rest of the line in group(1)
_begin_option_comm_re = re.compile ("^begin\s+option\s+(.+)$")

# Parse an argument string into a sys device with
# device name in group(1), unit number in group(2)
# We put all bootable devices into this string so that when
# we add more devices, for example rl for RL01, we change one
# string not many.
_simh_boot_dev_str = "(rk|td|dt|rx)(\d*)"
_simh_boot_re = re.compile("^" + _simh_boot_dev_str + "$")

# Parse an argument string for mount into SIMH device
# device name in group(1), unit number in group(2)
# And the rest in group (3)
_mount_regex_str = "^" + _simh_boot_dev_str + "\s+(.+)$"
_mount_re = re.compile(_mount_regex_str)

# Map of SIMH device names to OS/8 device name prefixes.
_os8_from_simh_dev = {"rk" : "RK", "td" : "DTA", "dt" : "DTA", "rx" : "RX"}

_os8_partitions = {"RK": ["A", "B"]}

# OS/8 file name matching regex
_os8_file_re = re.compile("(\S+):(\S+)?")

# Regular expression for syntax checking inside FOTP
# Destination is in group(1), Source is in group(3)
_fotp_re = re.compile ("^((\S+:)?\S+)<((\S+:)?\S+)$")

# Regular expression for detecting the 2 arg and 3 arg forms
# of the "pal8" script command.

# OS/8 name regex template:
# Optional device spec, i.e. DTA0:
# File spec with a specific extension or no extension.

_os8_fspec = Template ("((\S+:)?([A-Z0-9]{1,6}|[A-Z0-9]{1,6}\.$ext))")
_os8_BN_fspec = _os8_fspec.substitute(ext="BN")
_os8_PA_fspec = _os8_fspec.substitute(ext="PA")
_os8_LS_fspec = _os8_fspec.substitute(ext="LS")

# For the two arg form:
# The full destination spec is in group(1), The full source spec is in group(4).
# The device components, if any, are in group(2) for destination, and
# group(5) for source.
# The file components are in group(3) for destination, and group (6) for source.
# The destination file must either end in ".BN" or have no extension.
# The source must file either end in ".PA" or have no extension.
_two_arg_pal_re = re.compile ("^" + _os8_BN_fspec + "\s*<\s*" + _os8_PA_fspec + "$")

# For the 3 arg form:
# The full destination spec is in group(1), The full source spec is in group(7).
# The full listing spec is in group(4)
# The device components, if any, are in group(2) for destination, group(5)
# for listing, and group(8) for source.
# The file components are in group(3) for destination, and group(9) for source,
# and group(6) for listing.
# The destination file must either end in ".BN" or have no extension.
# The source must file either end in ".PA" or have no extension.
# The listing must either end in "LS" or have no extension.

_three_arg_pal_re = re.compile ("^" + _os8_BN_fspec + "\s*,\s*" + _os8_LS_fspec + "\s*<\s*" + _os8_PA_fspec + "$")

# Regular expression for syntax checking inside ABSLDR
# One or more OS/8 binary files and optional args beginning with a slash.

_absldr_re = re.compile ("^" + _os8_BN_fspec + "(," + _os8_BN_fspec + ")*(/\S)*$")

# Regular expressions for syntax checking for cpto and cpfrom.
# May be <source> where destination and default option /A is implied.
# Or <source> <option> where destination is implied and option is set.
# Or <source> <destination> where option /A is implied.
# Or <source> <destination> <option> are explicit.
# Valid options are "/A", "/I", and "/B"
# Use two regex's in order:
# <source> in group 1, <dest> in group 2.
# Option is one of /I /B /A in group 4.

# source in group 1, option in group 3.
_from_to_re_1 = re.compile ("^(\S+)(\s+(/[AIB]))?$")
# source in group 1, destination in group 2, option in group 4.
_from_to_re_2 = re.compile ("^(\S+)\s+(\S+)(\s+(/[AIB]))?$")

# Array of regular expressions for syntax checking inside BUILD
_build_comm_regs = {"LOAD"  : re.compile("^(\S+:)?\S+(.BN)?$"),
                    "UNLOAD": re.compile("^\S+(,\S+)?$"),
                    "INSERT": re.compile("^\S+,\S+(,\S+)?$"),
                    "DELETE": re.compile("^\S+(,\S+)?$"),
                    "SYSTEM": re.compile("^\S+$"),
                    "DSK"   : re.compile("^(\S+:)?\S+$"),
                    "BUILD" : re.compile("^(\S+(.BN)?)\s+(\S+(.BN)?)$"),
                    "PRINT" : None,
                    "BOOT"  : None,
                    "end"   : None}

_build_replies = ["\\$", "SYS BUILT", "WRITE ZERO DIRECT\\?", "\\?BAD ARG",
                  "\\?BAD INPUT", "\\?BAD LOAD",
                  "\\?BAD ORIGIN", "\\?CORE", "\\?DSK", "\\?HANDLERS",
                  "I/O ERR", "\\?NAME", "NO ROOM", "SYS NOT FOUND",
                  "\\?PLAT", "\\?SYNTAX", "\\?SYS", "SYS ERR",
                  "\S+ NOT FOUND"]

# Parse two whitspace separated arguments into group(1) and group(2)
_two_args_re = re.compile("^(\S+)\s+(\S+)$")

_rx_settings = ["rx01", "rx02", "RX8E", "RX28"]
_tape_settings = ["td", "dt"]
_tti_settings = ["KSR", "7b"]
_configurables = {"rx": _rx_settings, "tape": _tape_settings,
                  "tti": _tti_settings}

# Matches if the string begins with a dollar sign, and has at least
# one slash, returning the string between the dollar sign and the
# first slash in group 1 and the rest in group 2.
# No whitespace in the string.
_expandable_re = re.compile ("^\$([^/\s]+)/(\S*)$")

# Parse an exit arg for an integer or an integer in parentheses
_exit_arg_re = re.compile ("^(\s*[+-]?\s*\d+)|\s*\(\s*([+-]?\s*\d+)\s*\)\s*$")

# Options enabled/not_disabled for conditional execution in scripts.
#
# Earlier code allowed --enable and --disable. We interface to it.
# We maintain two arrays: options_enable and options_disabled for those
# two argument constructs.
#
# Argument parsing of repeated enable and disable arguments is as follows:
# --enable looks on options_disabled and if present removes it, then adds
# to options_enabled.
# --disable looks on options_enabled and if present removes it, then adds
# to options_enabled.
#
# Last seen enable/disable command line or executed command for a
# particular option wins.
#
# Scripts have enable/disable commands that are run-time
# changers of the contents of options_enabled and options_disabled.
#
# When we run a script we have begin/end blocks for enabled/not_disabled options:
# "begin enabled <option name>" ... "end enabled <option name>
# "begin not_disabled <option name>" ... "end not_disabled <option name>
#
# The enabled block looks for an explicit enablement on the options_enabled
# list. If none is found we default to ignoring the contents of the block.
#
# The not_disabled block looks for an explicit disablement on the
# options_disabled lis.  If found, the block is ignored. Otherwise
# the block defaults to being executed.
#
# begin/end blocks can be nested.  We track the nesting with options_stack.
# Testing for options happens when the begin command is evaluated.
# So changing an enable/disable option inside a begin/end block
# takes effect at the next begin statement. 
# You can write a script as follows:
# enable foo
# begin enabled foo
# # Commands to executed
# disable foo
# # Commands still being executed.
# begin enabled foo
# # Commands to ignore
# end enabled foo
# end enabled foo

# Local routine to perform a save of a pre-existing file
# because we do this in a couple places

def save_if_needed(path):
  if os.path.isfile(path):
    save_path = path + ".save"
    print "Pre-existing " + path + " found.  Saving as " + path + ".save"
    if os.path.isfile(save_path):
      print "Overwriting old " + path + ".save"
      os.remove(save_path)
    os.rename(path, save_path)
  
def version_to_array (version):
  vers_array = []
  this_str = ""

  for c in version:
    if c != ".":
      this_str += c
    else:
      vers_array.append(this_str)
      this_str = ""
  if this_str != "": vers_array.append(this_str)
  return vers_array


class os8script:
  # Contains a simh object, other global state and methods
  # for running OS/8 scripts under simh.
  #### globals and constants ###########################################
  
  
  def __init__ (self, simh, enabled_options, disabled_options, verbose=False, debug=True):
    self.lang_version = LANG_VERSION
    self.verbose = verbose
    self.debug = debug
    self.simh = simh
    self.options_enabled = enabled_options
    self.options_disabled = disabled_options
    # Do we need separate stacks for enabled/disabled options?
    self.options_stack = []
    # List of scratch files to delete when we are done with all script runs.
    self.scratch_list = []
    self.booted = False
    self.line_ct_stack = []


  #### path_expand #######################################################
  # Simple minded variable substitution in a path.
  # A path beginning with a dollar sign parses the characters between
  # the dollar sign and the first slash seen becomes a name to
  # expand with a couple local names: $home and the anchor directories
  # defined in lib/pidp8i/dirs.py.
  # Returns None if the expansion fails.  That signals the caller to fail.

  def path_expand (self, path):
    m = re.match(_expandable_re, path)
    if m == None: return path
    var = m.group(1)

    val = getattr (dirs, var, None)
    if val != None:
      return os.path.join(val,m.group(2))
    else:
      print "At line " + str(self.line_ct_stack[0]) + \
        ": {$" + var + "} is not a valid path expansion in " + path
      return None
    

  #### print_expand ######################################################
  # Close kin to path_expand.  Takes a string that may name a path
  # substitution or the magic $version value and performs the appropriate
  # value substitution.

  def print_expand (self,str):
    end = str.find("$")
    if end == -1: return str

    m = re.findall("\$\S+",str)
    if m == None: return str

    outstr = ""
    start = 0

    for name in m:
      end = str.index(name, start)
      outstr += str[start:end]

      sub = getattr (dirs, name[1:], None)
      if sub == None:
        if name == "$version": sub = self.lang_version
        else: sub = name

      outstr += sub
      start = end + len(name)

    return outstr
      

  #### version_test ######################################################
  # Compare each component of the version test agains the actual version
  # Return true if actual version is greater than or equal to the test
  # version.
  # Caller validates test with _version_parse_re so we only
  # need to return True or False, not error.
  
  def version_test (self, test):
    test_array = version_to_array(test)
    version_array = version_to_array(self.lang_version)
    
    idx = 0
    endpoint = len(test_array)

    while idx < endpoint:
      # If version has more digits than test, the greater than test succeeds.
      if idx >= len(version_array):
           vers_item = "0"
      else:
         vers_item = version_array[idx]
      test_item = test_array[idx]
      if self.debug: print  "version_test: vers_item: " + vers_item + \
         ", test_item: " + test_item

      vers_num = int(vers_item)
      test_num = int(test_item)

      # First time version componet greater than test -> success.
      if vers_num > test_num:
        if self.debug: print "version_test: Success: version greater than test."
        return True
      # First time version component less than test -> failure.
      elif test_num > vers_num:
        if self.debug: print "version_test: Fails on sub compare."
        return False
      #Otherwise is equal. Keep going.

      idx += 1
    # Made it all the way through. Test succeeds.
    if self.debug: print "version_test: Success. Made it thru test string."
    return True

          
  #### basic_line_parse ################################################
  # Returns stripped line and any other cleanup we want.
  # Returns None if we should just 'continue' on to the next line.
  # Filters out comments.
  # Processes the option begin/end blocks.
  
  def basic_line_parse (self, line, script_file):
    self.line_ct_stack[0] += 1
    retval = line.strip()
    if retval == "": return None
    elif retval[0] == "#": return None
    # First test if we are in a begin option block
    m = re.match (_begin_en_dis_comm_re, retval)
    if m != None:
      en_dis = m.group(1)
      rest = m.group(2)
      if self.verbose: print "Line " + str(self.line_ct_stack[0]) + \
         ": doing_begin_option: " + en_dis + " " + rest
      if self.debug: print "options_enabled: " + str (self.options_enabled)
      if self.debug: print "options_disabled: " + str (self.options_disabled)
      if self.debug: print "options_stack: " + str(self.options_stack)

      vers_match = False
      if en_dis == "version":
        # Check for mal-formed version match first
        if re.match (_version_parse_re, rest) == None:
          print "Mal-formed version match string {" + rest + "} at line " + \
            str(self.line_ct_stack[0]) + ". Ignoring this block."
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
          return None
        vers_match = self.version_test (rest)
        if vers_match:
          # Block is active. We push it onto the stack
          if self.debug: print "Pushing version enabled block " + rest + " onto options_stack"
          self.options_stack.insert(0, rest)
          if self.debug: print " new options_stack: " + str(self.options_stack)
        else:
          # Option is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
        return None
        
      elif en_dis == "enabled":
        if rest in self.options_enabled:
          # Block is active. We push it onto the stack
          if self.debug: print "Pushing enabled block " + rest + " onto options_stack"
          self.options_stack.insert(0, rest)
          if self.debug: print "new options_stack: " + str(self.options_stack)
        else:
          # Option is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)

        return None
      # only other choice is disabled because of our regex.
      else:
        if rest not in self.options_disabled:
          # Block defaults to active. We push it onto the stack
          if self.debug: print "Pushing not_disabled block " + rest + " onto options_stack"
          self.options_stack.insert(0, rest)
          if self.debug: print "new options_stack: " + str(self.options_stack)
        else:
          # Block is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
        return None
  
    m = re.match(_end_en_dis_comm_re, retval)
    if m != None:
      rest = m.group(2)
      if self.verbose: print "Line " + str(self.line_ct_stack[0]) + ": end rest = " + rest
      if (rest == None or rest == ""):
        print "Warning! option end statement at line " + str(self.line_ct_stack[0]) + \
          " encountered with no argument."
        return None
      if len(self.options_stack) == 0:
        print "Warning! option end statement at line " + str(self.line_ct_stack[0]) + \
          " found with no matching begin for option: " + rest
        return None
      if rest != self.options_stack[0]:
        print "Warning! Mismatched option begin/end group at line " + \
          str(self.line_ct_stack[0]) + ". Currently inside option: " + \
          self.options_stack[0] + " not " + rest
        return None
      else:
        if self.debug: print "Popping " + self.options_stack[0]
        self.options_stack.pop(0)
        if self.debug: print "new options_stack: " + str(self.options_stack)
        return None
  
    return retval
  
  
  #### ignore_to_subcomm_end ###########################################
  
  def ignore_to_subcomm_end (self, old_line, script_file, end_str):
    if self.debug: print "ignore to: " + end_str
    for line in script_file:
      self.line_ct_stack[0] += 1
      line = line.strip()
      if self.verbose: print "Ignore line " + str(self.line_ct_stack[0]) + ": " + line
      
      m = re.match(_end_comm_re, line)
      if m == None: continue
  
      rest = m.group(1)
      if rest == None: rest = ""
      
      if rest == end_str: return
  
  
  #### include_command #################################################
  # Call run_system_script recursively on the file path provided.
  
  def include_command (self, line, script_file):
    path = self.path_expand(line)
    if path == None:
      print "Ignoring: \n\tinclude " + line
      return "fail"

    if not os.path.isfile(path):
      print "Line " + str(self.line_ct_stack[0]) + \
        ": Could not find include file: " + path
      return "fail"
    if self.verbose: print "line: " + str(self.line_ct_stack[0]) + \
       ": include " + line
    return self.run_script_file (path)
      
  
  #### enable_option_command ###########################################
  # Deletes an option from the list of active options.
  # Parses the first argument after "enable" as the key to enable.
  # The end of the key is the end of the line or the first whitespace
  # character.
  
  def enable_option_command (self, line, script_file):
    if line == "":
      print "Empty option to enable at line: " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    m = re.match(_comm_re, line)
    if m == None:
      print "Could not parse enable command at line " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    option = m.group(1)
    if option == None:
      print "Empty option to enable command at line: " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    if self.verbose: print "Line " + str(self.line_ct_stack[0]) + \
       ": enable option: " + option
    # Remove it from other set if present
    if option in self.options_disabled:
      self.options_disabled.remove(option)
    # Add it if not already present.
    if option not in self.options_enabled:
      self.options_enabled.append(option)
    return "success"


  #### disable_option_command ###########################################
  # Deletes an option from the list of active options.
  # Parses the first argument after "disable" as the key to enable.
  # The end of the key is the end of the line or the first whitespace
  # character.
 
  def disable_option_command (self, line, script_file):
    if line == "":
      print "Empty option to disable at line: " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    m = re.match(_comm_re, line)
    if m == None:
      print "Could not parse disable option command at line " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    option = m.group(1)
    if option == None:
      print "Empty option to disable command at line " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
    if self.verbose: print "line: " + \
       str(self.line_ct_stack[0]) + ": disable option: " + option
    # Remove it from other set if present
    if option in self.options_enabled:
      self.options_enabled.remove(option)
    # Add it if not already present.
    if option not in self.options_disabled:
      self.options_disabled.append(option)
    return "success"


  #### configure_command ###############################################
  # First arg is the item to configure.
  # Second arg is the setting.
  # This enables adding option setting inside a script file.
  
  def configure_command (self, line, script_file):
    m = re.match(_two_args_re, line)
    if m == None or m.group(1) == None or m.group(2) == None: 
      print "Could not parse configure command arguments at line " + \
        str(self.line_ct_stack[0]) + ": {" + line + "}"
      return "fail"
    item = m.group(1)
    setting = m.group(2)
    if item not in _configurables:
      print "Ignoring invalid configuration item at line " + \
        str(self.line_ct_stack[0]) + ": " + item
      return "fail"
    if setting not in _configurables[item]:
      print "At line " + str(self.line_ct_stack[0]) + \
        ": Cannot set " + item + " to " + setting + "."
      return "fail"
    if item == "tape":
      self.simh.set_tape_config(setting)
    elif item == "rx":
      self.simh.set_rx_config (setting)
    elif item == "tti":
      self.simh.set_tti_config (setting)
    return "success"


  #### cpto_command ###########################################
  # Calls os8_pip_to with the command line arguments.
  
  def cpto_command (self, line, script_file):
    if not self.booted:
      print "Cannot run cpto command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"

    # Is 2nd and final arg the option?
    m = re.match(_from_to_re_1, line)
    if m != None:
      # Yes.  Expand Source first.
      path = self.path_expand(m.group(1))
      if path == None:
        print "Ignoring: \n\tcpto " + line
        return "fail"
      self.simh.os8_pip_to (path, "DSK:", m.group(2))
    else:
      # Is this normal case of source, dest, with possibly empty option?
      m = re.match(_from_to_re_2, line)
      if m == None:
        print "Could not parse cpto command at line " + \
          str(self.line_ct_stack[0]) + "."
        return "fail"
      path = self.path_expand(m.group(1))
      if path == None:
        print "Ignoring: \n\tcpto " + line
        return "fail"
      self.simh.os8_pip_to (path, m.group(2), m.group(4))
    return "success"


  #### cpto_command ###########################################
  # Calls os8_pip_from with the command line arguments.
  
  def cpfrom_command (self, line, script_file):
    if not self.booted:
      print "Cannot run cpfrom command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"
    m = re.match(_from_to_re_2, line)
    if m == None:
      print "Could not parse cpfrom command at line " + \
        str(self.line_ct_stack[0]) + "."
      return "fail"
  
    path = self.path_expand(m.group(2))
    if path == None:
      print "Ignoring: \n\tcpfrom " + line
      return "fail"
    self.simh.os8_pip_from (m.group(1), path, m.group(4))
    return "success"


  #### copy_command ###############################################
  # Simple script interface to create a copy of a file.

  def copy_command (self, line, script_file):
    m = re.match(_two_args_re, line)
    if m == None or m.group(1) == None or m.group(2) == None: 
      print "Could not copy command: " + line
      return "fail"
    from_path = self.path_expand(m.group(1))      
    to_path = self.path_expand(m.group(2))

    if from_path == None or to_path == None:
      print "Ignoring: \n\t copy " + line
      return "fail"

    print "copy command: \n\tfrom: " + from_path + ", \n\tto: " + to_path
    
    if (not os.path.isfile(from_path)):
        print "At line " + str(self.line_ct_stack[0]) + \
          ": Required copy input file: " + from_path + " not found."
        return "fail"

    save_if_needed(to_path)
    
    try:
      shutil.copyfile(from_path, to_path)
    except shutil.Error as e:
      print "copy command failed with error: " + str(e)
      return "fail"
    except IOError as e:
      print "copy command failed with IOError: " + str(e)
      return "fail"
    return "success"


  #### resume_command #############################################
  # Call the os8_resume in simh to resume OS/8.

  def resume_command (self, line, script_file):
    if self.verbose: print "Resuming OS/8 at line " + \
       str(self.line_ct_stack[0]) + "."

    self.simh.os8_restart()
    return "success"


  #### restart_command #############################################
  # Call the os8_restart in simh to resume OS/8.

  def restart_command (self, line, script_file):
    if self.verbose: print "Restarting OS/8 at line " + \
       str(self.line_ct_stack[0]) + "."

    self.simh.os8_restart()
    return "success"


  #### patch_command ##############################################
  # Read the named patch file and perform its actions.

  def patch_command (self, line, script_file):
    if not self.booted:
      print "Cannot run patch command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"
    path = self.path_expand(line)
    if path == None:
      print "Ignoring: \n\t" + "patch " + line
      return "fail"
    if not os.path.isfile(path):
      print "At line " + str(self.line_ct_stack[0]) + \
        ": Patch file: " + path + " not found."
      return "fail"

    self.run_patch_file (path)
    return "success"


  #### exit_command ###############################################
  # Call POSIX exit to exit the running program,
  # returning a numerical status value.

  def exit_command (self, line, script_file):
    m = re.match (_exit_arg_re, line)
    intfound = False
    if m != None:
      if m.group(1) != None:
        intfound = True
        status = int(m.group(1))
      elif m.group(2) != None:
        intfound = True
        status = int(m.group(2))
      else:
        intfound = False
    if self.verbose:
      if intfound: stat_str = str(status)
      else: stat_str = line
      print "Calling sys.exit (" +  stat_str + ") at line: " + \
        str(self.line_ct_stack[0]) + "."
    if intfound: sys.exit (status)
    else: sys.exit(line)


  #### print_command ###############################################
  # Print text from the running script
  # If verbose is set, say what line in the script containing
  # the print command.

  def print_command (self, line, script_file):
    if self.verbose:
      print "Line: " + str(self.line_ct_stack[0]) + ": " + line
    else:
      print self.print_expand(line)
    return "success"


  #### _command ###########################################
  # 

  def _command (self, line, script_file):
    return "success"


  #### run_script_file ############################################
  # Run os8 command script file
  # Call parsers as needed for supported sub commands.
  #
  # Commands:
  # mount <simh-dev> <image-file> [<option> ...]
  #       option: required | preserve | readonly | ro | scratch
  # umount <simh-dev>
  # boot <simh-dev>
  # os8 <command-line>
  #       the rest of the line is passed verbatim to OS/8
  # pal8 <pal-command line>
  # include <script-file>
  # configure <device> <parameter>
  #       device: tti | tape | rx
  #       tt parameter: KSR | 7b
  #       tape parameter: td | dt
  #       rx parameter: rx8e | rx28 | rx01 | rx02
  # enable <parameter>
  # disable <parameter>
  # cpto <posix-file> [<os8-file>] [<format>]
  # cpfrom <os8-file> <posix-file> [<format>]
  #       format: /A | /I | /B
  # copy <from-file> <to-file>
  # patch <patch-file>
  # resume
  # restart
  # begin <sub-command> <os8-path>
  # end <sub-command>
  # print <output text>
  # exit <status>

  # Sub-commands:
  # build, fotp, absldr
  #
  # Commands return, "success", "fail", or "die".
  
  def run_script_file (self, script_path):
  # Strings, regexps, and command arrays used by run_system
    commands = {"mount": self.mount_command,
                "boot": self.boot_command,
                "os8": self.os8_command,
                "pal8": self.pal8_command,
                "include": self.include_command,
                "begin": self.begin_command,
                "end": self.end_command,
                "exit": self.exit_command,
                "print": self.print_command,
                "umount": self.umount_command,
                "simh": self.simh_command,
                "configure": self.configure_command,
                "enable": self.enable_option_command,
                "disable": self.disable_option_command,
                "cpto": self.cpto_command,
                "cpfrom": self.cpfrom_command,
                "copy": self.copy_command,
                "resume": self.resume_command,
                "restart": self.restart_command,
                "patch": self.patch_command}
  
    try:
      script_file = open(script_path, "r")
    except IOError:
      print script_path + " not found."
      return "fail"

    # Every time we start a new script
    # We append a new line number count of 0
    # onto our line_ct_stack
    self.line_ct_stack.insert(0, 0)
    if self.debug: print "New line_ct_stack: " + str(self.line_ct_stack)
  
    for line in script_file:
      line = self.basic_line_parse (line, script_file)
      if line == None: continue
      
      m = re.match(_comm_re, line)
      if m == None:
        print "Ignoring command line at line " + \
          str(self.line_ct_stack[0]) + ": " + line
        continue
  
      if m.group(1) not in commands:
        print "Unrecognized script command at line " + \
          str(self.line_ct_stack[0]) + ": " + m.group(1)
        continue
  
      # print "arg: " + m.group(3)
      if m.group(3) == None: rest = ""
      else: rest = m.group(3)
      retval = commands[m.group(1)](rest, script_file)
      if retval == "die":
        print "\nFatal error encountered in " + script_path + \
          " at line " +  str(self.line_ct_stack[0]) + ":"
        print "\t" + line
        sys.exit(-1)

    # Done.  Pop the line count off our line_ct_stack
    self.line_ct_stack.pop(0)
    if self.debug: print "popped line_ct_stack: " + str(self.line_ct_stack)
    
    return "success"


  #### end_command #####################################################
  
  def end_command (self, line, script_file):
    print "Unexpectedly encountered end command at line " + \
      str(self.line_ct_stack[0]) + ": " + line
    return "fail"

  
  #### parse_odt #######################################################
  
  def parse_odt (self, com, line):
    if self.debug: print line
    
    if line == "\\c": return "break"
    match = _odt_parse.match(line)
    if match == None:
      print "Aborting because of bad ODT line: " + line
      self.simh.os8_send_ctrl('C')
      return "err"
    loc = match.group(1)
    old_val = match.group(2)
    new_val = match.group(3)
    expect_val_str = "\s*[0-7]{4} "
    
    if self.debug: print "Loc: " + loc + ", old_val: " + old_val + ", new_val: " + new_val
    self.simh.os8_send_str (loc + "/")
    self.simh._child.expect(expect_val_str)
  
    if old_val.isdigit():          # We need to check old value
      found_val = self.simh._child.after.strip()
      if found_val != old_val:
        print "\tOld value: " + found_val + " does not match " + old_val + ". Aborting patch."
        # Abort out of ODT back to the OS/8 Monitor
        self.simh.os8_send_ctrl('C')
        return "err"
  
    self.simh.os8_send_line (new_val)
    return "cont"


  #### futil_exit ########################################################
  
  def futil_exit (self, com, line):
    self.simh.os8_send_line(line)
    return "break"


  #### futil_file ########################################################
  
  def futil_file (self, com, line):
    # Redundant re-parse of line but nobody else wants args right now.
    match = _com_split_parse.match(line)
    if match == None:
      print "Aborting because of mal-formed FUTIL FILE command: " + line
      self.simh.os8_send_ctrl('C')
      return "err"
    fname = match.group(2)
    expect_futil_file_str = "\n" + fname + "\s+(.*)$"
    self.simh.os8_send_line (line)
    self.simh._child.expect(expect_futil_file_str)
    result = self.simh._child.after.strip()
    match = _com_split_parse.match(result)
    if match == None:
      print "Aborting because unexpected return status: " + result + " from: " + line
      self.simh.os8_send_ctrl('C')
      return "err"
    if match.group(2).strip() == "LOOKUP FAILED":
      print "Aborting because of FUTIL lookup failure on: " + fname
      self.simh.os8_send_ctrl('C')
      return "err"
    else:
      return "cont"
  
  
  #### parse_futil #####################################################
  #
  # Very simple minded:
  # If first char on line is an alpha, run the command.
  # If the first char on line is number, do the substitute command.
  #
  # Substitute command acts like ODT.
  # Future version should support the IF construct.
  #
  # When we encounter the EXIT command, we return success.
  
  def parse_futil (self, com, line):
    futil_specials = {
      "EXIT": self.futil_exit,
      "FILE": self.futil_file
    }
  
    if line[0].isdigit():
      # Treat the line as ODT
      return self.parse_odt(com, line)
    else:
      match = _com_split_parse.match(line)
      if match == None:
        print "Ignoring failed FUTIL command parse of: " + line
        return "cont"
      fcom = match.group(1)
      rest = match.group(2)
  
      if fcom not in futil_specials:
        # Blind faith and no error checking.
        self.simh.os8_send_line(line)
        return "cont"
      else:
        return futil_specials[fcom](fcom, line)


  #### run_patch_file ##################################################
  
  def run_patch_file (self, pathname):
    sys.stdout.write ("Applying patch " + os.path.basename (pathname) + "...")
    sys.stdout.flush ()

    try:
      patch_file = open(pathname, "r")
    except IOError:
      print pathname + " not found. Skipping."
      return "fail"
  
    special_commands = {
      "ODT": self.parse_odt,
      "R": None,               # Get next parser.
      "FUTIL": self.parse_futil
    }
  
    inside_a_command = False
    the_command = ""
    the_command_parser = None
    
    # Resume OS/8 if necessary.
    if self.simh._context == "simh":
      self.resume_command(line, script_file)

    for line in patch_file:
      line = line.rstrip()
      if line == "": continue
      elif line[0] == '#': continue     # Ignore Comments
      elif inside_a_command:
        retval = the_command_parser (the_command, line)
        if retval == "break":
          inside_a_command = False
          self.simh.os8_send_ctrl('C')
        elif retval == "err":
          patch_file.close()
          return "fail"
      elif line[0] == '.':        # New OS/8 Command
        match = _com_os8_parse.match(line)
        if match == None:
          print "Aborting patch on failed OS/8 command parse of: " + line
          return "fail"
        com = match.group(1)
        rest = match.group(2)
  
        if com in special_commands:
          if com == "R":
            # Run command is special.  Take arg as the command and run it.
            com = rest
          inside_a_command = True
          the_command = com
          the_command_parser = special_commands[com]
  
        # We carefully separate com and args
        # But don't make much use of that yet.
        if self.verbose and self.debug: print line
        self.simh.os8_send_cmd ("\\.", line[1:])  # Skip Prompt.
  
    patch_file.close()
  
    print "Success."
    return "success"


  #### skip_patch ######################################################
  # Returns true if the given filename matches one of the regex string
  # keys of the given skips dict and the flag value for that key is set.
  # See skips definition in make_patch, which calls this.
  
  def skip_patch (fn, skips):
      for p in skips:
          if re.search (p, fn) and skips[p]: return True
      return False
  

  #### call_pal8 #######################################################
  # Generic call out to PAL8 with error recovery.
  # We rely on the caller to have good specifications for source,
  # binary and optional listing files.
  
  def call_pal8 (self, source, binary):
    pal8_replies = ["ERRORS DETECTED: ", "BE\s+\S+", "CF\s+\S+", "DE\s+\S+", "DF", "IC\s+\S+", "ID\s+\S+",
                    "IE\s+\S+", "II\s+\S+", "IP\s+\S+", "IZ\s+\S+", "LD\s+\S+", "LG\s+\S+", "PE\s+\S+",
                    "PH\s+\S+", "RD\s+\S+", "SE\s+\S+", "UO\s+\S+", "US\s+\S+", "ZE\s+\S+", "\S+ NOT FOUND"]
  
    if self.verbose: print "Assembling " + source
    com_line = binary + "<" + source
    self.simh.os8_send_cmd ("\\.", "R PAL8")
    # Did the command successfully run and enter the command decoder?
    reply = self.simh._child.expect (self.simh._cd_replies)
    if reply != 0:
      print "PAL8 failed to start at line " + \
        str(self.line_ct_stack[0])
      return "fail"

    self.simh.os8_send_line (com_line)
    err_count = 0
    reply = self.simh._child.expect (pal8_replies)
    executed_line = self.simh._child.before.strip()
    reply_str = self.simh._child.after.strip()
    if reply == 0:
      self.simh._child.expect("\d+")
      err_count = int(self.simh._child.after.strip())
      reply_str += " " + self.simh._child.after.strip()
    if reply > 0 or err_count > 0:
      print "PAL8 Error: "
      print "\t*" + executed_line
      print "\t" + reply_str
      self.simh.os8_send_ctrl ('c')      # exit PAL8 Just in case.
      # We could do something better than just dying, I expect.
      return "fail"
    # self.simh.os8_send_ctrl ('[')      # exit PAL8
    return "success"


  #### simh_command ####################################################
  # I tried to avoid including this command but sometimes you just
  # have to reconfigure subtle bits of device drivers.
  # We assume we can call a simh command at any time, but
  # doing so puts us in the simh context that persists until we
  # issue a boot or go command.
  
  def simh_command (self, line, script_file):
    print "simh command is disabled. Line " + \
      str(self.line_ct_stack[0]) + " ignored."
    return "fail"
    if self.verbose: print line
    self.simh.send_cmd(line)
    return "success"


  #### umount_command ##################################################
  def umount_command (self, line, script_file):
    detach_comm = "det " + line
    if self.verbose: print "line: " + \
        str(self.line_ct_stack[0]) + ": " + detach_comm
    self.simh.send_cmd(detach_comm)
    return "success"


  #### make_scratch ####################################################
  # Create a copy of the contents of image file using the python
  # method to create a secure, named temp filename.
  # The caller has split the image file name into a base path,
  # and an extension which we use.
  # After copying the source image, the file is closed and the name
  # of the scratch file is returned.
  def make_scratch (self, base_imagename, extension):
    try:
      src_image = open(base_imagename+extension, "rb")
    except IOError:
      print "Open of imagefile " + base_imagename + extension + " failed."
      return None
      
    dest_image = tempfile.NamedTemporaryFile(mode='w+b', \
                                             prefix=base_imagename+'-temp-', \
                                             suffix=extension, \
                                             delete=False)
    destname = dest_image.name
    if self.debug: print "Temp file name is: " + destname

    try:
      shutil.copyfileobj(src_image, dest_image)
    except shutil.Error as e:
      print "Copy of imagefile " + base_imagename + extension + \
        " to scratch failed with error: " + str(e)
      return None
    except IOError as e:
      print "Copy of imagefile " + base_imagename + extension + \
        ", failed with IOError: " + \
        str(e)
      return None
    src_image.close()
    dest_image.close()
    self.scratch_list.append(destname)
    return destname
    

  #### mount_command ###################################################
  # mount <simh-device> <image-file> [option ...]
  #
  # Interface to SIMH attach command with options that do a bit more.
  #
  # Options:
  # required: <image-file> is required to exist, otherwise abort the script.
  # preserve if <image-file> already exists, create a copy with a
  #    version number suffix. This is useful when you want to prevent
  #    overwrites of a good image file with changes that might not work.
  #    os8-run preserves all versions seen and creates a new
  #    version that doesn't overwrite any of the previous ones.
  # readonly:  Passes the `-r` option to SIMH attach to mount the
  #    device in read only mode.
  # ro: abbreviation for  readonly.
  # scratch: Create a writeable scratch copy of the named image
  #    file and mount it.  This is helpful when you are booting a
  #    distribution DECtape.  Booted DECtape images must be writeable.
  #    To protect a distribution DECtape, use this option.
  #    When all script runs are done, the scratch version is deleted.
  # new: If there is an existing file, move it aside as a .save because
  #    we want to create a new empty image file.
  #
  # If the mount command fails for any reason, we presume
  # it is a fatal error and abort the script.
  
  def mount_command (self, line, script_file):
    m = re.match(_mount_re, line)
    if m == None:
      print "At line " + str(self.line_ct_stack[0]) + \
        ", could not parse mount.  Ignoring: {" + line + "}."
      return "die"
    simh_dev = m.group(1)
    unit = m.group(2)
    rest = m.group(3)
    parts = rest.split()
    if len(parts) == 0:
      print "At line " + str(self.line_ct_stack[0]) + \
        ": No image name specified in: {" + line + "}"
      return "die"
    ro_arg = ""
    imagename = self.path_expand(parts[0])
    if imagename == None: return "die"
    
    dot = imagename.rindex(".")
    base_imagename = imagename[:dot]
    extension = imagename[dot:]
    copy_imagename = ""
    # Case of additional arguments.
    if len (parts) > 1:
      # Perform must_exist before scratch
      if "new" in parts[1:]:
        save_if_needed(imagename)
      if "must_exist" in parts[1:] or "required" in parts[1:]:
          if not os.path.exists(imagename):
            print "At line " + str(self.line_ct_stack[0]) + \
              ", " + imagename + " must exist but was not found. Not mounting."
            return "die"
      if "scratch" in parts[1:]:
        copy_imagename = self.make_scratch(base_imagename, extension)
        if copy_imagename == None: return "die"
        imagename = copy_imagename
        
      if "readonly" in parts[1:] or "ro" in parts[1:]:
        if copy_imagename != "":
          print "At line " + str(self.line_ct_stack[0]) + \
            ": You don't really need to set readonly on a scratch copy."
        ro_arg = "-r "
      if "no_overwrite" in parts[1:] or "preserve" in parts[1:]:
        if copy_imagename != "":
          print "Ignoring preserve option at line " + \
            str(self.line_ct_stack[0]) + " because scratch option is present."
        else:
          next_tape = 0
          while os.path.isfile(imagename):
            print "Found: " + imagename
            next_tape += 1
            imagename =  base_imagename + "-" + str(next_tape) + extension
    if unit == None or unit == "":
      print "Need unit number for: " + line
      return "die"
  
    if simh_dev not in _os8_from_simh_dev:
      print "At line " + str(self.line_ct_stack[0]) + \
        ": Unrecognized simh dev: " + simh_dev
      return "die"
    os8dev = _os8_from_simh_dev[simh_dev]
  
    attach_comm = "att " + ro_arg + simh_dev + unit + " " + imagename
  
    if self.verbose: print "Line " + str(self.line_ct_stack[0]) + \
       ": mount: " + attach_comm
    self.simh.send_cmd(attach_comm)
    return "success"


  #### boot_command ####################################################
  #
  # Check to see if the device to be booted has something attached.
  # If not die.
  # If so, boot it, and set our booted state to True.
  
  def boot_command (self, line, script_file):
    # First confirm something is attached to boot from.
    ucname = line.upper()
    boot_replies = [ucname + "\s+(.+)\r", "Non-existent device"]
    self.simh.send_cmd("show " + line)
    retval = self.simh._child.expect(boot_replies)
    if retval == 1:
      print "Attempt to boot non-existent device: " + line
      return "die"
    m = re.match("^(\S+)\s(\S+),\s+(attached to |not attached)(\S+)?,\s+(.+)\r",
                 self.simh._child.after)
    if m == None:
      print "Could not determine if device " + line + " is attached."
      return "die"

    # Caution match group we want ends with a space.
    if m.group(3) != "attached to ":
      print "Attempt to boot on non-attached device: " + line
      return "die"
    
    boot_comm = "boot " + line
    if self.verbose: print "Line " + str(self.line_ct_stack[0]) + ": " + \
       boot_comm
    self.simh.send_cmd(boot_comm)
    self.booted = True
    return "success"


  #### os8_command #####################################################
  
  def os8_command (self, line, script_file):
    if not self.booted:
      print "Cannot run os8 command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"

    os8_comm = line
    if self.verbose: print "Line: " + \
       str(self.line_ct_stack[0]) + ": os8_command: " + os8_comm

    # Resume OS/8 if necessary.
    if self.simh._context == "simh":
      self.resume_command(line, script_file)

    self.simh.os8_send_cmd ("\\.", os8_comm, self.debug)
    return "success"


  #### pal8_command ####################################################
  # The "pal8" script command comes in two forms:
  # The two argument form where the PAL8 status is printed on the fly
  # and the 3 argument form where all status goes into the listing file.
  # We do the 3 argument form with a simple "os8" script command.
  
  def pal8_command (self, line, script_file):
    if not self.booted:
      print "Cannot run pal8 command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"

    m_2form = re.match (_two_arg_pal_re, line)
    if m_2form != None:

      # Resume OS/8 if necessary.
      if self.simh._context == "simh":
        self.resume_command(line, script_file)

      # Call the 2arg pal8 code that works hard at error analysis.
      return self.call_pal8 (m_2form.group(4), m_2form.group(1))
    else:
      m_3form = re.match (_three_arg_pal_re, line)
      if m_3form != None:
        # Just run the OS/8 command.
        os8_comm = "pal8 " + line
        if self.verbose: print "Line: " + \
           str(self.line_ct_stack[0]) + ": Calling 3-arg pal8 command: " + os8_comm
        self.simh.os8_send_cmd ("\\.", os8_comm)
      else:
        print "At line " + str(self.line_ct_stack[0]) + \
          ": Unrecognized pal8 form: {" + line + "}."
        return "fail"
    return "success"


  #### begin_command ###################################################
  
  def begin_command (self, line, script_file):
    if not self.booted:
      print "Cannot execute begin subcommand block at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted."
      return "die"

    sub_commands = {"fotp": self.fotp_subcomm, "build": self.build_subcomm,
                    "absldr": self.absldr_subcomm,
                    "cdprog": self.cdprog_subcomm}
  
    m = re.match(_comm_re, line)
    if m == None:
      print "Could not parse sub-command at line " + \
        str(self.line_ct_stack[0]) + ": " + line
    if m.group(1) not in sub_commands:
      print "Ignoring unrecognized sub-command at line " + \
        str(self.line_ct_stack[0]) + ": " + m.group(1)
      print "Ignoring everything to next end."
      self.ignore_to_subcomm_end(line, script_file, "")
      return "fail"
    else:
      # Resume OS/8 if necessary.
      if self.simh._context == "simh":
        self.resume_command(line, script_file)

      return sub_commands[m.group(1)](m.group(3), script_file)
  
  
  #### run_build_build #################################################
  # ***CAUTION***
  # When you do this you are instructing BUILD to
  # OVERWRITE the system area.  If you do this to your
  # running RK05 pack by mistake, you WILL make a mess
  # and need to re-run mkos8 to re-make it.
  
  def run_build_build (self, os8_spec, cd_spec):
    self.simh.os8_send_cmd ("\\$", "BUILD", debug=True)
    self.simh.os8_send_cmd ("LOAD OS/8: ", os8_spec, debug=True)
    self.simh.os8_send_cmd ("LOAD CD: ", cd_spec, debug=True)
    return "success"


  #### build_subcomm ###################################################
  
  def build_subcomm (self, old_line, script_file):
    # A race condition results if we send ^C when we are already at
    # Monitor level.  So need_exit gets set to False, when we know
    # we have already exited build, and are at the monitor prompt.
    need_exit = True
    os8_comm = "RU " + old_line
    if self.verbose: print "Line " + str(self.line_ct_stack[0]) + ": " + \
       os8_comm
    prompt_str = "\n\\$$"
    if self.debug:
      print "sending to simh: " + os8_comm
      print " and expecting prompt: '\\n\\\\$$'"
    self.simh.os8_send_cmd ("\\.", os8_comm)
    self.simh._child.expect(prompt_str)
    
    for line in script_file:
      # if self.debug:
      #  print "line: " + line
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      m = re.match(_comm_re, line)
      if m == None:
        print "Ignoring mal-formed build sub-command at line " + \
          str(self.line_ct_stack[0]) + ": " + line
        continue
  
      build_sub = m.group(1)
      rest = m.group(3)
      if rest == None: rest = ""
      
      if build_sub not in _build_comm_regs:
        print "Unrecognized BUILD command at line " + \
          str(self.line_ct_stack[0]) + ": " + build_sub
        continue
  
      if build_sub == "end":
        if rest == "":
          print "Warning! end statement encountered inside build with no argument at line " + \
            str(self.line_ct_stack[0]) + ".\nExiting build."
          return "fail"
        elif rest != "build": 
          print "Warning! Mismatched begin/end blocks in BUILD at line " + \
            str(self.line_ct_stack[0]) + ".\nEncountered end: {" + rest + "}. Exiting BUILD."
          return "fail"
        
        if self.verbose:
          print "Line " + str(self.line_ct_stack[0]) + ": end BUILD"
        if self.debug:
          print "before: " + self.simh._child.before.strip()
          print "after: " + self.simh._child.after.strip()
        # Return to monitor level unless need_exit == False.
        if need_exit:
          self.simh.os8_send_ctrl ('c')

        return "success"
        
      build_re = _build_comm_regs[build_sub]
  
      if build_re != None:
        m2 = re.match(build_re, rest)
        if m2 == None:
          print "Ignoring mal-formed BUILD at line " + \
            str(self.line_ct_stack[0]) + ": " + build_sub + " command: " + rest
          continue
      
        if build_sub == "BUILD":
          if m2.group(1) == None or m2.group(1) == "":
            print "Missing source of OS8.BN. Ignoring BUILD command at line " + \
              str(self.line_ct_stack[0]) + "."
            continue
          else: kbm_arg = m2.group(1)

          if m2.group(3) == None or m2.group(3) == "":
            print "Missing sorce of CD.BN. Ignoring BUILD command at line " + \
              str(self.line_ct_stack[0]) + "."
            continue
          else: cd_arg = m2.group(3)
          
          if self.verbose: print "Line " + str(self.line_ct_stack[0]) + \
             ": BUILD KBM: " + kbm_arg + ", CD: " + cd_arg
          if self.debug:
            print "sending to simh: BUILD"
          self.simh.os8_send_line ("BUILD")

          build_build_replies = ["LOAD OS/8: "]
          build_build_replies.extend(_build_replies)
          
          if self.debug:
            print "expecting: " + str(build_build_replies)
          reply = self.simh._child.expect(build_build_replies)
          if self.debug:
            print "reply: " + str(reply)
            print "before: " + self.simh._child.before.strip()
            print "after: " + self.simh._child.after.strip()
          if reply != 0:
            print "No prompt for LOAD OS/8 in BUILD command within BUILD at line " + \
              str(self.line_ct_stack[0]) + "."
            print "Instead got: {" + self.simh._child.after + "}."
            print "Exiting BUILD."
            return "die"
          if self.debug:
            print "sending to simh: " + kbm_arg
          self.simh.os8_send_line (kbm_arg)

          build_build_replies = ["LOAD CD: "]
          build_build_replies.extend(_build_replies)
          
          if self.debug:
            print "expecting: " + str(build_build_replies)
          reply = self.simh._child.expect(build_build_replies)
          if self.debug:
            print "reply: " + str(reply)
            print "before: " + self.simh._child.before.strip()
            print "after: " + self.simh._child.after.strip()
          if reply != 0:
            print "No prompt for LOAD CD in BUILD command within BUILD at line " + \
              str(self.line_ct_stack[0]) + "."
            print "Instead got: {" + self.simh._child.after + "}."
            print "Exiting BUILD."
            return "die"
          if self.debug:
            print "sending to simh: " + cd_arg
          self.simh.os8_send_line (cd_arg)

          # Done with BUILD command dialog within BUILD.SV
          # Get that BUILD.SV prompt.
          if self.debug:
            print "Expecting prompt: '\\n\\\\$$'"
          self.simh._child.expect(prompt_str)
          if self.debug:
            print "Resume BUILD.SV command loop."
          continue

      comm = build_sub + " " + rest
      if self.verbose: print "Line " + str(self.line_ct_stack[0]) + \
         ": BUILD-> " + comm

      if self.debug:
        print "sending to simh: " + comm
      self.simh.os8_send_line (comm)
      if self.debug:
        print "expecting: " + str(_build_replies)
      reply = self.simh._child.expect(_build_replies)
      if self.debug:
        print "reply: " + str(reply)
        print "before: " + self.simh._child.before.strip()
        print "after: " + self.simh._child.after.strip()
      if reply > 3:
        print "BUILD error at line " + str(self.line_ct_stack[0]) + \
          " with command " + self.simh._child.before.strip()
        print "\t" + self.simh._child.after.strip()
        self.simh.os8_send_ctrl ('c')
      # Special case "BOOT" sub-command: May ask, "WRITE ZERO DIRECT?"
      if build_sub == "BOOT":
        if reply == 2:
          if self.debug:
            print "Boot received \"WRITE ZERO DIRECT?\""
            print "sending to simh: Y"
          self.simh.os8_send_line("Y")
          if self.debug:
            print "Expecting \"SYS BUILT\""
          reply = self.simh._child.expect("SYS BUILT")
          if self.debug:
            print "ZeroDir: reply: " + str(reply)
            print "before: " + self.simh._child.before.strip()
            print "after: " + self.simh._child.after.strip()
          need_exit = False
        elif reply == 0:
          reply = self.simh._child.expect("SYS BUILT")
          if self.debug:
            print "$: reply: " + str(reply)
            print "before: " + self.simh._child.before.strip()
            print "after: " + self.simh._child.after.strip()
          need_exit = False
        elif reply == 1:
          reply = self.simh._child.expect("\\.")
          if self.debug:
            print "SysBuilt: reply: " + str(reply)
            print "before: " + self.simh._child.before.strip()
            print "after: " + self.simh._child.after.strip()
    print "Warning end of file encountered with no end of BUILD command block at line " + \
      str(self.line_ct_stack[0]) + "."
    return "fail"
  
  #### cdprog_subcomm ##################################################
  # Cycle through OS/8 command decoder with the command specified
  # in the argument.
  
  def cdprog_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    end_str = "cdprog " + old_line
    if self.verbose: print "Line: " + \
       str(self.line_ct_stack[0]) + ": " + os8_comm
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        retval = "fail"  # Return fail unless proven successful.
        if rest == None or rest == "":
          print "Warning! end statement encountered inside cdprog with no argument at line " + \
            str(self.line_ct_stack[0]) + "."
          print "Expecting: {" + end_str + "}."
        elif rest != end_str:
          print "Warning! Mismatched begin/end blocks in cdprog at line " + \
            str(self.line_ct_stack[0]) + ".\n"
          print "Expecting: {" + end_str + "}. Got: {" + rest + "}.\n"
        else:
          retval = "success"
          if self.verbose: print "Line " + str(self.line_ct_stack[0]) + ": end " + end_str
        if retval == "fail":
          print "Exiting cdprog, possibly earlier than expected at line " + \
            str(self.line_ct_stack[0]) + "."
        self.simh.os8_send_ctrl ('[')
        return retval
  
      # We could do some basic OS/8 command decoder synax checking here.
      comm = line
      if self.verbose: print "Line: " + \
         str(self.line_ct_stack[0]) + ": * " + line
      self.simh.os8_send_cmd ("\\*", line)
    print "Warning end of file encountered at line " + \
      str(self.line_ct_stack[0]) + " with no end of cdprog command block."
    self.simh.os8_send_ctrl ('[')
    return "fail"

  
  #### fotp_subcomm ####################################################
  
  def fotp_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    if self.verbose: print "Line: " + \
       str(self.line_ct_stack[0]) + ": " + os8_comm
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        if rest == None or rest == "":
          print "Warning! end statement encountered inside fotp with no argument at line " + \
            str(self.line_ct_stack[0]) + "."
          return "fail"
        elif rest != "fotp":
          print "Warning! Mismatched begin/end blocks in FOTP at line " + \
            str(self.line_ct_stack[0]) + ".\nEncountered end: {" + rest + "}. Exiting FOTP."
          return "fail"

        if self.verbose: print "Line " + str(self.line_ct_stack[0]) + ": end FOTP"
        self.simh.os8_send_ctrl ('[')
        return "success"
  
      m = re.match(_fotp_re, line)
      if m == None:
        print "At line " + str(self.line_ct_stack[0]) + \
          ": ignoring mal-formed fotp file spec: {" + line + "}."
        continue
  
      comm = line
      if self.verbose: print "Line: " + \
         str(self.line_ct_stack[0]) + ": * " + line
      self.simh.os8_send_cmd ("\\*", line)
    print "Warning end of file encountered at line " + \
      str(self.line_ct_stack[0]) + " with no end of FOTP command block."
    return "fail"

  
  #### absldr_subcomm ##################################################
  # A clone of fotp_subcom.  Can we find a way to merge the common code?
  
  def absldr_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    if self.verbose: print " line: " + \
       str(self.line_ct_stack[0]) + ": " + os8_comm
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        if rest == None or rest == "":
          print "Warning! end statement encountered inside ABSLDR with no argument at line " + \
            str(self.line_ct_stack[0]) + "."
          return "fail"
        elif rest != "absldr":
          print "Warning! Mismatched begin/end blocks in ABSLDR at line " + \
            str(self.line_ct_stack[0]) + ".\nEncountered end: {" + rest + "}. Exiting ABSLDR."
          return "fail"
          
        if self.verbose: print "End ABSLDR at line " + str(self.line_ct_stack[0]) + "."
        self.simh.os8_send_ctrl ('[')
        return "success"
  
      m = re.match(_absldr_re, line)
      if m == None:
        print "Ignoring mal-formed absldr file spec at line " + str(self.line_ct_stack[0]) + \
          ": {" + line + "}."
        continue
  
      comm = line
      if self.verbose: print "Line : " + str(self.line_ct_stack[0]) + "* " + line
      self.simh.os8_send_cmd ("\\*", line)
    print "Warning end of file encountered at line " + \
      str(self.line_ct_stack[0]) + "with no end of ABSLDR command block."
    return "fail"


  #### check_exists ####################################################
  # Check existence of all files needed
  
  def check_exists (s, image_copyins):
    for copyin in image_copyins:
      image = copyin[1]
      image_path = dirs.os8mi + image
      if (not os.path.isfile(image_path)):
          print "Required file: " + image_path + " not found."
          mkos8_abort(s)
      # else: print "Found " + image_path
  
     
  #### Data Structures ################################################
  #
  # The make procedures use helper procedures
  # to confirm that the relevant input image file exists and
  # to perform the file copies.
  #
  # A data structure called "image copyin"
  #    describes the image file pathname relative to an implied root,
  #    provides a message string when the action is run,
  #    names a default destination device for whole image content copies,
  #    offers an optional array of specific file copy actions.
  #
  # FUTURE: Parse source path for ".tu56" vs. ".rk05" for more general use.
  # Currently all code assumes a copyin comes from a DECtape image.
  #
  # Example: We Install all files for ADVENT, the Adventure game:
  #
  # advent_copyin = ['RKB0:', 'subsys/advent.tu56',  "Installing ADVENT...", None]
  #
  # A DECtape device is chosen for attachment in SIMH and
  # a 'COPY *.*' command is filled in with the Destination device, and the chosen DECtape.
  #
  # A data structer called "file copyin"
  #     provides override destination to allow renames or varied destinations.
  #     names individual files within a copyin to use
  #
  # Example:  To copy the C compiler we want all .SV files on SYS
  #           but everything else to RKB0:
  #           (Note the useful /V option to invert the match.)
  #
  # cc8_sv_file_copyin   = ['SYS:', '*.SV']
  # cc8_rest_file_copyin = ['RKB0:', '*.SV/V']
  #
  # A 'COPY' command is filled in with the override destination and
  # The file spec is used with the chosen dectape instead of "*.*"
  #
  
  #### copyin_pair #####################################################
  # Copy two images into two destinations with two messages
  #
  # Assumes our context is "in simh".
  # Assumes dt0 and dt1 are free.
  # Assumes rk0 is the boot device
  # Detaches dt0 and dt1 after using them.
  # copyin0 mounts on dt0.  copyin1 mounts on dt1.
  # Either copyin or both can be None
  
  def copyin_pair (s, copyin0, copyin1, debug):
    if debug:
      if copyin0:
        print "Copying: " + copyin0[1] + " to: " + copyin0[0] + "from dt0"
      else: print "copyin0 is empty."
      if copyin1:
        print "Copying: " + copyin1[1] + " to: " + copyin1[0] + "from dt1"
      else: print "copyin1 is empty."
      
    if not copyin0 and not copyin1: return   # Nothing to do.
  
    # The order of events here is a bit funky because we want
    # to use both DECtape drives but also
    # switch between SIMH and OS/8 as infrequently as possible.
  
    if copyin0: self.simh.send_cmd ("attach -r dt0 " + dirs.os8mi + copyin0[1])
    if copyin1: self.simh.send_cmd ("attach -r dt1 " + dirs.os8mi + copyin1[1])
  
    self.simh.os8_restart()
  
    if copyin0:
      if self.verbose: print copyin0[2]
      if copyin0[3]:                    # We have specific files to do.
        for file_copyin in copyin0[3]:
          self.simh.os8_send_cmd ("\\.", "COPY " + file_copyin[0] + "<DTA0:" + file_copyin[1])
      else:
        self.simh.os8_send_cmd ("\\.", "COPY " + copyin0[0] + "<DTA0:*.*")
  
    if copyin1:
      if self.verbose: print copyin1[2]
      if copyin1[3]:                    # We have specific files to do.
        for file_copyin in copyin1[3]:
          self.simh.os8_send_cmd ("\\.", "COPY " + file_copyin[0] + "<DTA1:" + file_copyin[1])
      else:
        self.simh.os8_send_cmd ("\\.", "COPY " + copyin1[0] + "<DTA1:*.*")
  
    self.simh.back_to_cmd("\\.")
  
    if copyin0: self.simh.send_cmd ("detach dt0")
    if copyin1: self.simh.send_cmd ("detach dt1")
  
  
  #### do_all_copyins ##################################################
  
  def do_all_copyins (s, copyins, debug):
    pair_idx = 0
    pair_ct = int(len(copyins) / 2)
    while pair_idx < pair_ct:
      copyin_pair(s, copyins[pair_idx * 2], copyins[pair_idx * 2 + 1], debug)
      pair_idx += 1
    if pair_ct * 2 < len(copyins):
      copyin_pair(s, copyins[len(copyins) - 1], None, debug)
  
  
  
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Added lib/os8script.py.in.
























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# simh-os8-script.py Library for scripting OS/8 under SIMH
# Contains validators and callers for os8 and simh commands to make
# it easier to create scripts.
#
# Copyright © 2017-2019 by Jonathan Trites, William 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 sys
import tempfile
sys.path.insert (0, os.path.dirname (__file__) + '/../lib')
sys.path.insert (0, os.getcwd () + '/lib')

# Python core modules we use
import re
from string import Template
import shutil
import subprocess

# Our local modules
from pidp8i import *
from simh import *

# Script Language Version
# Update this version number as the language evolves.
# Version 1.0 is the first public version.
LANG_VERSION = "1.0"

# Error Class Definitions ##############################################
# Enables us to use exceptions from within this module.

class Error(Exception):
  """Base Class for exceptions in this module."""
  pass

class InputError(Error):
  """Exception raised for errors in the input.

  Attributes:
  expr -- input expression in which the error occurred
  msg  -- explanation of the error
  """

  def __init__(self, msg):
    self.msg = msg

  def __str__(self):
    return self.msg

      
# Private globals ######################################################
# Visible within this file, but not to the outside.

# Identify a begin enabled/not_disabled command. group(1) contains either the enabled or
# disabled flag. Put the rest of the line in group(2)
_begin_en_dis_comm_re = re.compile ("^begin\s+(enabled|default|version)\s+(.+)$")
  
# Identify an end enabled/not_disabled command. group(1) contains either the enabled or
# disabled flag. Put the rest of the line in group(2)
_end_en_dis_comm_re = re.compile ("^end\s+(enabled|default|version)\s+(.+)$")
  
# Identify an end comm and put the rest of the line in group(1)
_end_comm_re = re.compile ("^end\s+(.+)?$")
  
# Identify an end option command and put the rest of the line in group(1)
_end_option_comm_re = re.compile ("^end\s+option\s+(.+)$")

# A valid version spec
_version_parse_re = re.compile ("^((\d+\.)*)?(\d+)?$")
  
# Name of the DECtape image file we create
_new_sys_tape_prefix = "system"

# Parser regexps used in patcher
_com_os8_parse_str = "^\.([a-zA-Z]+)\s*(.*)$"
_com_os8_parse = re.compile(_com_os8_parse_str)
_com_split_str = "^([a-zA-Z]+)\s*(.*)$"
_com_split_parse = re.compile(_com_split_str)
_odt_parse_str = "^([0-7]+)\s*/\s*(\S+)\s+([0-7;]+)"
_odt_parse = re.compile(_odt_parse_str)

# Put command keyword in group(1) and the rest is in group(3)
_comm_re_str = "^(\S+)(\s+(.+))?$"
_comm_re = re.compile(_comm_re_str)

# Identify an end comm and put the rest of the line in group(1)
_end_comm_re = re.compile ("^end\s+(.+)?$")

# Identify an end option command and put the rest of the line in group(1)
_end_option_comm_re = re.compile ("^end\s+option\s+(.+)$")

# Identify a begin command and put the rest of the line in group(1)
_begin_option_comm_re = re.compile ("^begin\s+option\s+(.+)$")

# Parse an argument string into a sys device with
# device name in group(1), unit number in group(2)
# We put all bootable devices into this string so that when
# we add more devices, for example rl for RL01, we change one
# string not many.
_simh_boot_dev_str = "(rk|td|dt|rx)(\d*)"
_simh_boot_re = re.compile("^" + _simh_boot_dev_str + "$")

# Parse an argument string for mount into SIMH device
# device name in group(1), unit number in group(2)
# And the rest in group (3)
_mount_regex_str = "^" + _simh_boot_dev_str + "\s+(.+)$"
_mount_re = re.compile(_mount_regex_str)

# Map of SIMH device names to OS/8 device name prefixes.
_os8_from_simh_dev = {"rk" : "RK", "td" : "DTA", "dt" : "DTA", "rx" : "RX"}

_os8_partitions = {"RK": ["A", "B"]}

# OS/8 file name matching regex
_os8_file_re = re.compile("(\S+):(\S+)?")

# Regular expression for syntax checking inside FOTP
# Destination is in group(1), Source is in group(3)
_fotp_re = re.compile ("^((\S+:)?\S+)<((\S+:)?\S+)$")

# Regular expression for detecting the 2 arg and 3 arg forms
# of the "pal8" script command.

# OS/8 name regex template:
# Optional device spec, i.e. DTA0:
# File spec with a specific extension or no extension.

_os8_fspec = Template ("((\S+:)?([A-Z0-9]{1,6}|[A-Z0-9]{1,6}\.$ext))")
_os8_BN_fspec = _os8_fspec.substitute(ext="BN")
_os8_PA_fspec = _os8_fspec.substitute(ext="PA")
_os8_LS_fspec = _os8_fspec.substitute(ext="LS")

# For the two arg form:
# The full destination spec is in group(1), The full source spec is in group(4).
# The device components, if any, are in group(2) for destination, and
# group(5) for source.
# The file components are in group(3) for destination, and group (6) for source.
# The destination file must either end in ".BN" or have no extension.
# The source must file either end in ".PA" or have no extension.
_two_arg_pal_re = re.compile ("^" + _os8_BN_fspec + "\s*<\s*" + _os8_PA_fspec + "$")

# For the 3 arg form:
# The full destination spec is in group(1), The full source spec is in group(7).
# The full listing spec is in group(4)
# The device components, if any, are in group(2) for destination, group(5)
# for listing, and group(8) for source.
# The file components are in group(3) for destination, and group(9) for source,
# and group(6) for listing.
# The destination file must either end in ".BN" or have no extension.
# The source must file either end in ".PA" or have no extension.
# The listing must either end in "LS" or have no extension.

_three_arg_pal_re = re.compile ("^" + _os8_BN_fspec + "\s*,\s*" + _os8_LS_fspec + "\s*<\s*" + _os8_PA_fspec + "$")

# Regular expression for syntax checking inside ABSLDR
# One or more OS/8 binary files and optional args beginning with a slash.

_absldr_re = re.compile ("^" + _os8_BN_fspec + "(," + _os8_BN_fspec + ")*(/\S)*$")

# Regular expressions for syntax checking for cpto and cpfrom.
# May be <source> where destination and default option /A is implied.
# Or <source> <option> where destination is implied and option is set.
# Or <source> <destination> where option /A is implied.
# Or <source> <destination> <option> are explicit.
# Valid options are "/A", "/I", and "/B"
# Use two regex's in order:
# <source> in group 1, <dest> in group 2.
# Option is one of /I /B /A in group 4.

# source in group 1, option in group 3.
_from_to_re_1 = re.compile ("^(\S+)(\s+(/[AIB]))?$")
# source in group 1, destination in group 2, option in group 4.
_from_to_re_2 = re.compile ("^(\S+)\s+(\S+)(\s+(/[AIB]))?$")

# Array of regular expressions for syntax checking inside BUILD
_build_comm_regs = {"LOAD"  : re.compile("^(\S+:)?\S+(.BN)?$"),
                    "UNLOAD": re.compile("^\S+(,\S+)?$"),
                    "INSERT": re.compile("^\S+,\S+(,\S+)?$"),
                    "DELETE": re.compile("^\S+(,\S+)?$"),
                    "SYSTEM": re.compile("^\S+$"),
                    "DSK"   : re.compile("^(\S+:)?\S+$"),
                    "BUILD" : re.compile("^(\S+(.BN)?)\s+(\S+(.BN)?)$"),
                    "PRINT" : None,
                    "BOOT"  : None,
                    "end"   : None}

_build_replies = ["\\$", "SYS BUILT", "WRITE ZERO DIRECT\\?", "\\?BAD ARG",
                  "\\?BAD INPUT", "\\?BAD LOAD",
                  "\\?BAD ORIGIN", "\\?CORE", "\\?DSK", "\\?HANDLERS",
                  "I/O ERR", "\\?NAME", "NO ROOM", "SYS NOT FOUND",
                  "\\?PLAT", "\\?SYNTAX", "\\?SYS", "SYS ERR",
                  "\S+ NOT FOUND"]

# Parse two whitspace separated arguments into group(1) and group(2)
_two_args_re = re.compile("^(\S+)\s+(\S+)$")

_rx_settings = ["rx01", "rx02", "RX8E", "RX28"]
_tape_settings = ["td", "dt"]
_tti_settings = ["KSR", "7b"]
_configurables = {"rx": _rx_settings, "tape": _tape_settings,
                  "tti": _tti_settings}

# Matches if the string begins with a dollar sign, and has at least
# one slash, returning the string between the dollar sign and the
# first slash in group 1 and the rest in group 2.
# No whitespace in the string.
_expandable_re = re.compile ("^\$([^/\s]+)/(\S*)$")

# Parse an exit arg for an integer or an integer in parentheses
_exit_arg_re = re.compile ("^(\s*[+-]?\s*\d+)|\s*\(\s*([+-]?\s*\d+)\s*\)\s*$")

# Options enabled/not_disabled for conditional execution in scripts.
#
# Earlier code allowed --enable and --disable. We interface to it.
# We maintain two arrays: options_enable and options_disabled for those
# two argument constructs.
#
# Argument parsing of repeated enable and disable arguments is as follows:
# --enable looks on options_disabled and if present removes it, then adds
# to options_enabled.
# --disable looks on options_enabled and if present removes it, then adds
# to options_enabled.
#
# Last seen enable/disable command line or executed command for a
# particular option wins.
#
# Scripts have enable/disable commands that are run-time
# changers of the contents of options_enabled and options_disabled.
#
# When we run a script we have begin/end blocks for enabled/not_disabled options:
# "begin enabled <option name>" ... "end enabled <option name>
# "begin not_disabled <option name>" ... "end not_disabled <option name>
#
# The enabled block looks for an explicit enablement on the options_enabled
# list. If none is found we default to ignoring the contents of the block.
#
# The not_disabled block looks for an explicit disablement on the
# options_disabled lis.  If found, the block is ignored. Otherwise
# the block defaults to being executed.
#
# begin/end blocks can be nested.  We track the nesting with options_stack.
# Testing for options happens when the begin command is evaluated.
# So changing an enable/disable option inside a begin/end block
# takes effect at the next begin statement. 
# You can write a script as follows:
# enable foo
# begin enabled foo
# # Commands to executed
# disable foo
# # Commands still being executed.
# begin enabled foo
# # Commands to ignore
# end enabled foo
# end enabled foo

# Local routine to perform a save of a pre-existing file
# because we do this in a couple places

def save_if_needed(path):
  if os.path.isfile(path):
    save_path = path + ".save"
    print("Pre-existing " + path + " found.  Saving as " + path + ".save")
    if os.path.isfile(save_path):
      print("Overwriting old " + path + ".save")
      os.remove(save_path)
    os.rename(path, save_path)
  
def version_to_array (version):
  vers_array = []
  this_str = ""

  for c in version:
    if c != ".":
      this_str += c
    else:
      vers_array.append(this_str)
      this_str = ""
  if this_str != "": vers_array.append(this_str)
  return vers_array


class os8script:
  # Contains a simh object, other global state and methods
  # for running OS/8 scripts under simh.
  #### globals and constants ###########################################
  
  
  def __init__ (self, simh, enabled_options, disabled_options, verbose=False, debug=True):
    self.lang_version = LANG_VERSION
    self.verbose = verbose
    self.debug = debug
    self.simh = simh
    self.options_enabled = enabled_options
    self.options_disabled = disabled_options
    # Do we need separate stacks for enabled/disabled options?
    self.options_stack = []
    # List of scratch files to delete when we are done with all script runs.
    self.scratch_list = []
    self.booted = False
    self.line_ct_stack = []


  #### path_expand #######################################################
  # Simple minded variable substitution in a path.
  # A path beginning with a dollar sign parses the characters between
  # the dollar sign and the first slash seen becomes a name to
  # expand with a couple local names: $home and the anchor directories
  # defined in lib/pidp8i/dirs.py.
  # Returns None if the expansion fails.  That signals the caller to fail.

  def path_expand (self, path):
    m = re.match(_expandable_re, path)
    if m == None: return path
    var = m.group(1)

    val = getattr (dirs, var, None)
    if val != None:
      return os.path.join(val,m.group(2))
    else:
      print("At line " + str(self.line_ct_stack[0]) + \
        ": {$" + var + "} is not a valid path expansion in " + path)
      return None
    

  #### print_expand ######################################################
  # Close kin to path_expand.  Takes a string that may name a path
  # substitution or the magic $version value and performs the appropriate
  # value substitution.

  def print_expand (self,str):
    end = str.find("$")
    if end == -1: return str

    m = re.findall("\$\S+",str)
    if m == None: return str

    outstr = ""
    start = 0

    for name in m:
      end = str.index(name, start)
      outstr += str[start:end]

      sub = getattr (dirs, name[1:], None)
      if sub == None:
        if name == "$version": sub = self.lang_version
        else: sub = name

      outstr += sub
      start = end + len(name)

    return outstr
      

  #### version_test ######################################################
  # Compare each component of the version test agains the actual version
  # Return true if actual version is greater than or equal to the test
  # version.
  # Caller validates test with _version_parse_re so we only
  # need to return True or False, not error.
  
  def version_test (self, test):
    test_array = version_to_array(test)
    version_array = version_to_array(self.lang_version)
    
    idx = 0
    endpoint = len(test_array)

    while idx < endpoint:
      # If version has more digits than test, the greater than test succeeds.
      if idx >= len(version_array):
           vers_item = "0"
      else:
         vers_item = version_array[idx]
      test_item = test_array[idx]
      if self.debug:
        print("version_test: vers_item: " + vers_item + \
           ", test_item: " + test_item)

      vers_num = int(vers_item)
      test_num = int(test_item)

      # First time version componet greater than test -> success.
      if vers_num > test_num:
        if self.debug:
          print("version_test: Success: version greater than test.")
        return True
      # First time version component less than test -> failure.
      elif test_num > vers_num:
        if self.debug: print("version_test: Fails on sub compare.")
        return False
      #Otherwise is equal. Keep going.

      idx += 1
    # Made it all the way through. Test succeeds.
    if self.debug:
      print("version_test: Success. Made it thru test string.")
    return True

          
  #### basic_line_parse ################################################
  # Returns stripped line and any other cleanup we want.
  # Returns None if we should just 'continue' on to the next line.
  # Filters out comments.
  # Processes the option begin/end blocks.
  
  def basic_line_parse (self, line, script_file):
    self.line_ct_stack[0] += 1
    retval = line.strip()
    if retval == "": return None
    elif retval[0] == "#": return None
    # First test if we are in a begin option block
    m = re.match (_begin_en_dis_comm_re, retval)
    if m != None:
      en_dis = m.group(1)
      rest = m.group(2)
      if self.verbose: print("Line " + str(self.line_ct_stack[0]) + \
         ": doing_begin_option: " + en_dis + " " + rest)
      if self.debug:
        print("options_enabled: " + str (self.options_enabled))
        print("options_disabled: " + str (self.options_disabled))
        print("options_stack: " + str(self.options_stack))

      vers_match = False
      if en_dis == "version":
        # Check for mal-formed version match first
        if re.match (_version_parse_re, rest) == None:
          print("Mal-formed version match string {" + rest + "} at line " + \
            str(self.line_ct_stack[0]) + ". Ignoring this block.")
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
          return None
        vers_match = self.version_test (rest)
        if vers_match:
          # Block is active. We push it onto the stack
          if self.debug:
            print("Pushing version enabled block " + rest + \
                " onto options_stack")
          self.options_stack.insert(0, rest)
          if self.debug: print(" new options_stack: " + \
              str(self.options_stack))
        else:
          # Option is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
        return None
        
      elif en_dis == "enabled":
        if rest in self.options_enabled:
          # Block is active. We push it onto the stack
          if self.debug:
              print("Pushing enabled block " + rest + " onto options_stack")
          self.options_stack.insert(0, rest)
          if self.debug:
            print("new options_stack: " + str(self.options_stack))
        else:
          # Option is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)

        return None
      # only other choice is disabled because of our regex.
      else:
        if rest not in self.options_disabled:
          # Block defaults to active. We push it onto the stack
          if self.debug:
            print("Pushing not_disabled block " + rest + \
                " onto options_stack")
          self.options_stack.insert(0, rest)
          if self.debug:
            print("new options_stack: " + str(self.options_stack))
        else:
          # Block is inactive.  Ignore all subseqent lines
          # until we get to an end command that matches our option.
          self.ignore_to_subcomm_end (retval, script_file, en_dis + " " + rest)
        return None
  
    m = re.match(_end_en_dis_comm_re, retval)
    if m != None:
      rest = m.group(2)
      if self.verbose:
        print("Line " + str(self.line_ct_stack[0]) + ": end rest = " + rest)
      if (rest == None or rest == ""):
        print("Warning! option end statement at line " + \
          str(self.line_ct_stack[0]) + " encountered with no argument.")
        return None
      if len(self.options_stack) == 0:
        print("Warning! option end statement at line " + \
          str(self.line_ct_stack[0]) + \
          " found with no matching begin for option: " + rest)
        return None
      if rest != self.options_stack[0]:
        print("Warning! Mismatched option begin/end group at line " + \
          str(self.line_ct_stack[0]) + ". Currently inside option: " + \
          self.options_stack[0] + " not " + rest)
        return None
      else:
        if self.debug: print("Popping " + self.options_stack[0])
        self.options_stack.pop(0)
        if self.debug:
          print("new options_stack: " + str(self.options_stack))
        return None
  
    return retval
  
  
  #### ignore_to_subcomm_end ###########################################
  
  def ignore_to_subcomm_end (self, old_line, script_file, end_str):
    if self.debug: print("ignore to: " + end_str)
    for line in script_file:
      self.line_ct_stack[0] += 1
      line = line.strip()
      if self.verbose:
        print("Ignore line " + str(self.line_ct_stack[0]) + ": " + line)
      
      m = re.match(_end_comm_re, line)
      if m == None: continue
  
      rest = m.group(1)
      if rest == None: rest = ""
      
      if rest == end_str: return
  
  
  #### include_command #################################################
  # Call run_system_script recursively on the file path provided.
  
  def include_command (self, line, script_file):
    path = self.path_expand(line)
    if path == None:
      print("Ignoring: \n\tinclude " + line)
      return "fail"

    if not os.path.isfile(path):
      print("Line " + str(self.line_ct_stack[0]) + \
        ": Could not find include file: " + path)
      return "fail"
    if self.verbose:
      print("line: " + str(self.line_ct_stack[0]) + ": include " + line)
    return self.run_script_file (path)
      
  
  #### enable_option_command ###########################################
  # Deletes an option from the list of active options.
  # Parses the first argument after "enable" as the key to enable.
  # The end of the key is the end of the line or the first whitespace
  # character.
  
  def enable_option_command (self, line, script_file):
    if line == "":
      print("Empty option to enable at line: " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    m = re.match(_comm_re, line)
    if m == None:
      print("Could not parse enable command at line " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    option = m.group(1)
    if option == None:
      print("Empty option to enable command at line: " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    if self.verbose:
      print("Line " + str(self.line_ct_stack[0]) + \
         ": enable option: " + option)
    # Remove it from other set if present
    if option in self.options_disabled:
      self.options_disabled.remove(option)
    # Add it if not already present.
    if option not in self.options_enabled:
      self.options_enabled.append(option)
    return "success"


  #### disable_option_command ###########################################
  # Deletes an option from the list of active options.
  # Parses the first argument after "disable" as the key to enable.
  # The end of the key is the end of the line or the first whitespace
  # character.
 
  def disable_option_command (self, line, script_file):
    if line == "":
      print("Empty option to disable at line: " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    m = re.match(_comm_re, line)
    if m == None:
      print("Could not parse disable option command at line " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    option = m.group(1)
    if option == None:
      print("Empty option to disable command at line " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
    if self.verbose:
      print("line: " + str(self.line_ct_stack[0]) + \
          ": disable option: " + option)
    # Remove it from other set if present
    if option in self.options_enabled:
      self.options_enabled.remove(option)
    # Add it if not already present.
    if option not in self.options_disabled:
      self.options_disabled.append(option)
    return "success"


  #### configure_command ###############################################
  # First arg is the item to configure.
  # Second arg is the setting.
  # This enables adding option setting inside a script file.
  
  def configure_command (self, line, script_file):
    m = re.match(_two_args_re, line)
    if m == None or m.group(1) == None or m.group(2) == None: 
      print("Could not parse configure command arguments at line " + \
        str(self.line_ct_stack[0]) + ": {" + line + "}")
      return "fail"
    item = m.group(1)
    setting = m.group(2)
    if item not in _configurables:
      print("Ignoring invalid configuration item at line " + \
        str(self.line_ct_stack[0]) + ": " + item)
      return "fail"
    if setting not in _configurables[item]:
      print("At line " + str(self.line_ct_stack[0]) + \
        ": Cannot set " + item + " to " + setting + ".")
      return "fail"
    if item == "tape":
      self.simh.set_tape_config(setting)
    elif item == "rx":
      self.simh.set_rx_config (setting)
    elif item == "tti":
      self.simh.set_tti_config (setting)
    return "success"


  #### cpto_command ###########################################
  # Calls os8_pip_to with the command line arguments.
  
  def cpto_command (self, line, script_file):
    if not self.booted:
      print("Cannot run cpto command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"

    # Is 2nd and final arg the option?
    m = re.match(_from_to_re_1, line)
    if m != None:
      # Yes.  Expand Source first.
      path = self.path_expand(m.group(1))
      if path == None:
        print("Ignoring: \n\tcpto " + line)
        return "fail"
      self.simh.os8_pip_to (path, "DSK:", m.group(2))
    else:
      # Is this normal case of source, dest, with possibly empty option?
      m = re.match(_from_to_re_2, line)
      if m == None:
        print("Could not parse cpto command at line " + \
          str(self.line_ct_stack[0]) + ".")
        return "fail"
      path = self.path_expand(m.group(1))
      if path == None:
        print("Ignoring: \n\tcpto " + line)
        return "fail"
      self.simh.os8_pip_to (path, m.group(2), m.group(4))
    return "success"


  #### cpto_command ###########################################
  # Calls os8_pip_from with the command line arguments.
  
  def cpfrom_command (self, line, script_file):
    if not self.booted:
      print("Cannot run cpfrom command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"
    m = re.match(_from_to_re_2, line)
    if m == None:
      print("Could not parse cpfrom command at line " + \
        str(self.line_ct_stack[0]) + ".")
      return "fail"
  
    path = self.path_expand(m.group(2))
    if path == None:
      print("Ignoring: \n\tcpfrom " + line)
      return "fail"
    self.simh.os8_pip_from (m.group(1), path, m.group(4))
    return "success"


  #### copy_command ###############################################
  # Simple script interface to create a copy of a file.

  def copy_command (self, line, script_file):
    m = re.match(_two_args_re, line)
    if m == None or m.group(1) == None or m.group(2) == None: 
      print("Could not copy command: " + line)
      return "fail"
    from_path = self.path_expand(m.group(1))      
    to_path = self.path_expand(m.group(2))

    if from_path == None or to_path == None:
      print("Ignoring: \n\t copy " + line)
      return "fail"

    print("copy command: \n\tfrom: " + from_path + ", \n\tto: " + to_path)
    
    if (not os.path.isfile(from_path)):
        print("At line " + str(self.line_ct_stack[0]) + \
          ": Required copy input file: " + from_path + " not found.")
        return "fail"

    save_if_needed(to_path)
    
    try:
      shutil.copyfile(from_path, to_path)
    except shutil.Error as e:
      print("copy command failed with error: " + str(e))
      return "fail"
    except IOError as e:
      print("copy command failed with IOError: " + str(e))
      return "fail"
    return "success"


  #### resume_command #############################################
  # Call the os8_resume in simh to resume OS/8.

  def resume_command (self, line, script_file):
    if self.verbose:
      print("Resuming OS/8 at line " + str(self.line_ct_stack[0]) + ".")

    self.simh.os8_restart()
    return "success"


  #### restart_command #############################################
  # Call the os8_restart in simh to resume OS/8.

  def restart_command (self, line, script_file):
    if self.verbose:
      print("Restarting OS/8 at line " + str(self.line_ct_stack[0]) + ".")

    self.simh.os8_restart()
    return "success"


  #### patch_command ##############################################
  # Read the named patch file and perform its actions.

  def patch_command (self, line, script_file):
    if not self.booted:
      print("Cannot run patch command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"
    path = self.path_expand(line)
    if path == None:
      print("Ignoring: \n\t" + "patch " + line)
      return "fail"
    if not os.path.isfile(path):
      print("At line " + str(self.line_ct_stack[0]) + \
        ": Patch file: " + path + " not found.")
      return "fail"

    self.run_patch_file (path)
    return "success"


  #### exit_command ###############################################
  # Call POSIX exit to exit the running program,
  # returning a numerical status value.

  def exit_command (self, line, script_file):
    m = re.match (_exit_arg_re, line)
    intfound = False
    if m != None:
      if m.group(1) != None:
        intfound = True
        status = int(m.group(1))
      elif m.group(2) != None:
        intfound = True
        status = int(m.group(2))
      else:
        intfound = False
    if self.verbose:
      if intfound: stat_str = str(status)
      else: stat_str = line
      print("Calling sys.exit (" +  stat_str + ") at line: " + \
        str(self.line_ct_stack[0]) + ".")
    if intfound: sys.exit (status)
    else: sys.exit(line)


  #### print_command ###############################################
  # Print text from the running script
  # If verbose is set, say what line in the script containing
  # the print command.

  def print_command (self, line, script_file):
    if self.verbose:
      print("Line: " + str(self.line_ct_stack[0]) + ": " + line)
    else:
      print(self.print_expand(line))
    return "success"


  #### _command ###########################################
  # 

  def _command (self, line, script_file):
    return "success"


  #### run_script_file ############################################
  # Run os8 command script file
  # Call parsers as needed for supported sub commands.
  #
  # Commands:
  # mount <simh-dev> <image-file> [<option> ...]
  #       option: required | preserve | readonly | ro | scratch
  # umount <simh-dev>
  # boot <simh-dev>
  # os8 <command-line>
  #       the rest of the line is passed verbatim to OS/8
  # pal8 <pal-command line>
  # include <script-file>
  # configure <device> <parameter>
  #       device: tti | tape | rx
  #       tt parameter: KSR | 7b
  #       tape parameter: td | dt
  #       rx parameter: rx8e | rx28 | rx01 | rx02
  # enable <parameter>
  # disable <parameter>
  # cpto <posix-file> [<os8-file>] [<format>]
  # cpfrom <os8-file> <posix-file> [<format>]
  #       format: /A | /I | /B
  # copy <from-file> <to-file>
  # patch <patch-file>
  # resume
  # restart
  # begin <sub-command> <os8-path>
  # end <sub-command>
  # print <output text>
  # exit <status>

  # Sub-commands:
  # build, fotp, absldr
  #
  # Commands return, "success", "fail", or "die".
  
  def run_script_file (self, script_path):
  # Strings, regexps, and command arrays used by run_system
    commands = {"mount": self.mount_command,
                "boot": self.boot_command,
                "os8": self.os8_command,
                "pal8": self.pal8_command,
                "include": self.include_command,
                "begin": self.begin_command,
                "end": self.end_command,
                "exit": self.exit_command,
                "print": self.print_command,
                "umount": self.umount_command,
                "simh": self.simh_command,
                "configure": self.configure_command,
                "enable": self.enable_option_command,
                "disable": self.disable_option_command,
                "cpto": self.cpto_command,
                "cpfrom": self.cpfrom_command,
                "copy": self.copy_command,
                "resume": self.resume_command,
                "restart": self.restart_command,
                "patch": self.patch_command}
  
    try:
      script_file = open(script_path, "r")
    except IOError:
      print(script_path + " not found.")
      return "fail"

    # Every time we start a new script
    # We append a new line number count of 0
    # onto our line_ct_stack
    self.line_ct_stack.insert(0, 0)
    if self.debug:
      print("New line_ct_stack: " + str(self.line_ct_stack))
  
    for line in script_file:
      line = self.basic_line_parse (line, script_file)
      if line == None: continue
      
      m = re.match(_comm_re, line)
      if m == None:
        print("Ignoring command line at line " + \
          str(self.line_ct_stack[0]) + ": " + line)
        continue
  
      if m.group(1) not in commands:
        print("Unrecognized script command at line " + \
          str(self.line_ct_stack[0]) + ": " + m.group(1))
        continue
  
      # print("arg: " + m.group(3))
      if m.group(3) == None: rest = ""
      else: rest = m.group(3)
      retval = commands[m.group(1)](rest, script_file)
      if retval == "die":
        print("\nFatal error encountered in " + script_path + \
          " at line " +  str(self.line_ct_stack[0]) + ":")
        print("\t" + line)
        sys.exit(-1)

    # Done.  Pop the line count off our line_ct_stack
    self.line_ct_stack.pop(0)
    if self.debug:
      print("popped line_ct_stack: " + str(self.line_ct_stack))
    
    return "success"


  #### end_command #####################################################
  
  def end_command (self, line, script_file):
    print("Unexpectedly encountered end command at line " + \
      str(self.line_ct_stack[0]) + ": " + line)
    return "fail"

  
  #### parse_odt #######################################################
  
  def parse_odt (self, com, line):
    if self.debug: print(line)
    
    if line == "\\c": return "break"
    match = _odt_parse.match(line)
    if match == None:
      print("Aborting because of bad ODT line: " + line)
      self.simh.os8_send_ctrl('C')
      return "err"
    loc = match.group(1)
    old_val = match.group(2)
    new_val = match.group(3)
    expect_val_str = "\s*[0-7]{4} "
    
    if self.debug:
      print("Loc: " + loc + ", old_val: " + old_val + ", new_val: " + \
          new_val)
    self.simh.os8_send_str (loc + "/")
    self.simh._child.expect(expect_val_str)
  
    if old_val.isdigit():          # We need to check old value
      found_val = self.simh._child.after.decode().strip()
      if found_val != old_val:
        print("\tOld value: " + found_val + " does not match " +
            old_val + ". Aborting patch.")
        # Abort out of ODT back to the OS/8 Monitor
        self.simh.os8_send_ctrl('C')
        return "err"
  
    self.simh.os8_send_line (new_val)
    return "cont"


  #### futil_exit ########################################################
  
  def futil_exit (self, com, line):
    self.simh.os8_send_line(line)
    return "break"


  #### futil_file ########################################################
  
  def futil_file (self, com, line):
    # Redundant re-parse of line but nobody else wants args right now.
    match = _com_split_parse.match(line)
    if match == None:
      print("Aborting because of mal-formed FUTIL FILE command: " + line)
      self.simh.os8_send_ctrl('C')
      return "err"
    fname = match.group(2)
    expect_futil_file_str = "\n" + fname + "\s+(.*)$"
    self.simh.os8_send_line (line)
    self.simh._child.expect(expect_futil_file_str)
    result = self.simh._child.after.decode().strip()
    match = _com_split_parse.match(result)
    if match == None:
      print("Aborting because unexpected return status: " + result + \
          " from: " + line)
      self.simh.os8_send_ctrl('C')
      return "err"
    if match.group(2).strip() == "LOOKUP FAILED":
      print("Aborting because of FUTIL lookup failure on: " + fname)
      self.simh.os8_send_ctrl('C')
      return "err"
    else:
      return "cont"
  
  
  #### parse_futil #####################################################
  #
  # Very simple minded:
  # If first char on line is an alpha, run the command.
  # If the first char on line is number, do the substitute command.
  #
  # Substitute command acts like ODT.
  # Future version should support the IF construct.
  #
  # When we encounter the EXIT command, we return success.
  
  def parse_futil (self, com, line):
    futil_specials = {
      "EXIT": self.futil_exit,
      "FILE": self.futil_file
    }
  
    if line[0].isdigit():
      # Treat the line as ODT
      return self.parse_odt(com, line)
    else:
      match = _com_split_parse.match(line)
      if match == None:
        print("Ignoring failed FUTIL command parse of: " + line)
        return "cont"
      fcom = match.group(1)
      rest = match.group(2)
  
      if fcom not in futil_specials:
        # Blind faith and no error checking.
        self.simh.os8_send_line(line)
        return "cont"
      else:
        return futil_specials[fcom](fcom, line)


  #### run_patch_file ##################################################
  
  def run_patch_file (self, pathname):
    sys.stdout.write ("Applying patch " + os.path.basename (pathname) + "...")
    sys.stdout.flush ()

    try:
      patch_file = open(pathname, "r")
    except IOError:
      print(pathname + " not found. Skipping.")
      return "fail"
  
    special_commands = {
      "ODT": self.parse_odt,
      "R": None,               # Get next parser.
      "FUTIL": self.parse_futil
    }
  
    inside_a_command = False
    the_command = ""
    the_command_parser = None
    
    # Resume OS/8 if necessary.
    if self.simh._context == "simh":
      self.resume_command(line, script_file)

    for line in patch_file:
      line = line.rstrip()
      if line == "": continue
      elif line[0] == '#': continue     # Ignore Comments
      elif inside_a_command:
        retval = the_command_parser (the_command, line)
        if retval == "break":
          inside_a_command = False
          self.simh.os8_send_ctrl('C')
        elif retval == "err":
          patch_file.close()
          return "fail"
      elif line[0] == '.':        # New OS/8 Command
        match = _com_os8_parse.match(line)
        if match == None:
          print("Aborting patch on failed OS/8 command parse of: " + line)
          return "fail"
        com = match.group(1)
        rest = match.group(2)
  
        if com in special_commands:
          if com == "R":
            # Run command is special.  Take arg as the command and run it.
            com = rest
          inside_a_command = True
          the_command = com
          the_command_parser = special_commands[com]
  
        # We carefully separate com and args
        # But don't make much use of that yet.
        if self.verbose and self.debug: print(line)
        self.simh.os8_send_cmd ("\\.", line[1:])  # Skip Prompt.
  
    patch_file.close()
  
    print("Success.")
    return "success"


  #### skip_patch ######################################################
  # Returns true if the given filename matches one of the regex string
  # keys of the given skips dict and the flag value for that key is set.
  # See skips definition in make_patch, which calls this.
  
  def skip_patch (fn, skips):
      for p in skips:
          if re.search (p, fn) and skips[p]: return True
      return False
  

  #### call_pal8 #######################################################
  # Generic call out to PAL8 with error recovery.
  # We rely on the caller to have good specifications for source,
  # binary and optional listing files.
  
  def call_pal8 (self, source, binary):
    pal8_replies = ["ERRORS DETECTED: ", "BE\s+\S+", "CF\s+\S+", "DE\s+\S+", "DF", "IC\s+\S+", "ID\s+\S+",
                    "IE\s+\S+", "II\s+\S+", "IP\s+\S+", "IZ\s+\S+", "LD\s+\S+", "LG\s+\S+", "PE\s+\S+",
                    "PH\s+\S+", "RD\s+\S+", "SE\s+\S+", "UO\s+\S+", "US\s+\S+", "ZE\s+\S+", "\S+ NOT FOUND"]
  
    if self.verbose: print("Assembling " + source)
    com_line = binary + "<" + source
    self.simh.os8_send_cmd ("\\.", "R PAL8")
    # Did the command successfully run and enter the command decoder?
    reply = self.simh._child.expect (self.simh._cd_replies)
    if reply != 0:
      print("PAL8 failed to start at line " + str(self.line_ct_stack[0]))
      return "fail"

    self.simh.os8_send_line (com_line)
    err_count = 0
    reply = self.simh._child.expect (pal8_replies)
    executed_line = self.simh._child.before.decode().strip()
    reply_str = self.simh._child.after.decode().strip()
    if reply == 0:
      self.simh._child.expect("\d+")
      err_count = int(self.simh._child.after.decode().strip())
      reply_str += " " + self.simh._child.after.decode().strip()
    if reply > 0 or err_count > 0:
      print("PAL8 Error: ")
      print("\t*" + executed_line)
      print("\t" + reply_str)
      self.simh.os8_send_ctrl ('c')      # exit PAL8 Just in case.
      # We could do something better than just dying, I expect.
      return "fail"
    # self.simh.os8_send_ctrl ('[')      # exit PAL8
    return "success"


  #### simh_command ####################################################
  # I tried to avoid including this command but sometimes you just
  # have to reconfigure subtle bits of device drivers.
  # We assume we can call a simh command at any time, but
  # doing so puts us in the simh context that persists until we
  # issue a boot or go command.
  
  def simh_command (self, line, script_file):
    print("simh command is disabled. Line " + \
      str(self.line_ct_stack[0]) + " ignored.")
    return "fail"
    if self.verbose: print(line)
    self.simh.send_cmd(line)
    return "success"


  #### umount_command ##################################################
  def umount_command (self, line, script_file):
    detach_comm = "det " + line
    if self.verbose: print("line: " + \
        str(self.line_ct_stack[0]) + ": " + detach_comm)
    self.simh.send_cmd(detach_comm)
    return "success"


  #### make_scratch ####################################################
  # Create a copy of the contents of image file using the python
  # method to create a secure, named temp filename.
  # The caller has split the image file name into a base path,
  # and an extension which we use.
  # After copying the source image, the file is closed and the name
  # of the scratch file is returned.
  def make_scratch (self, base_imagename, extension):
    try:
      src_image = open(base_imagename+extension, "rb")
    except IOError:
      print("Open of imagefile " + base_imagename + extension + " failed.")
      return None
      
    dest_image = tempfile.NamedTemporaryFile(mode='w+b', \
                                             prefix=base_imagename+'-temp-', \
                                             suffix=extension, \
                                             delete=False)
    destname = dest_image.name
    if self.debug: print("Temp file name is: " + destname)

    try:
      shutil.copyfileobj(src_image, dest_image)
    except shutil.Error as e:
      print("Copy of imagefile " + base_imagename + extension + \
        " to scratch failed with error: " + str(e))
      return None
    except IOError as e:
      print("Copy of imagefile " + base_imagename + extension + \
          ", failed with IOError: " + str(e))
      return None
    src_image.close()
    dest_image.close()
    self.scratch_list.append(destname)
    return destname
    

  #### mount_command ###################################################
  # mount <simh-device> <image-file> [option ...]
  #
  # Interface to SIMH attach command with options that do a bit more.
  #
  # Options:
  # required: <image-file> is required to exist, otherwise abort the script.
  # preserve if <image-file> already exists, create a copy with a
  #    version number suffix. This is useful when you want to prevent
  #    overwrites of a good image file with changes that might not work.
  #    os8-run preserves all versions seen and creates a new
  #    version that doesn't overwrite any of the previous ones.
  # readonly:  Passes the `-r` option to SIMH attach to mount the
  #    device in read only mode.
  # ro: abbreviation for  readonly.
  # scratch: Create a writeable scratch copy of the named image
  #    file and mount it.  This is helpful when you are booting a
  #    distribution DECtape.  Booted DECtape images must be writeable.
  #    To protect a distribution DECtape, use this option.
  #    When all script runs are done, the scratch version is deleted.
  # new: If there is an existing file, move it aside as a .save because
  #    we want to create a new empty image file.
  #
  # If the mount command fails for any reason, we presume
  # it is a fatal error and abort the script.
  
  def mount_command (self, line, script_file):
    m = re.match(_mount_re, line)
    if m == None:
      print("At line " + str(self.line_ct_stack[0]) + \
        ", could not parse mount.  Ignoring: {" + line + "}.")
      return "die"
    simh_dev = m.group(1)
    unit = m.group(2)
    rest = m.group(3)
    parts = rest.split()
    if len(parts) == 0:
      print("At line " + str(self.line_ct_stack[0]) + \
        ": No image name specified in: {" + line + "}")
      return "die"
    ro_arg = ""
    imagename = self.path_expand(parts[0])
    if imagename == None: return "die"
    
    dot = imagename.rindex(".")
    base_imagename = imagename[:dot]
    extension = imagename[dot:]
    copy_imagename = ""
    # Case of additional arguments.
    if len (parts) > 1:
      # Perform must_exist before scratch
      if "new" in parts[1:]:
        save_if_needed(imagename)
      if "must_exist" in parts[1:] or "required" in parts[1:]:
          if not os.path.exists(imagename):
            print("At line " + str(self.line_ct_stack[0]) + ", " + \
                imagename + " must exist but was not found. Not mounting.")
            return "die"
      if "scratch" in parts[1:]:
        copy_imagename = self.make_scratch(base_imagename, extension)
        if copy_imagename == None: return "die"
        imagename = copy_imagename
        
      if "readonly" in parts[1:] or "ro" in parts[1:]:
        if copy_imagename != "":
          print("At line " + str(self.line_ct_stack[0]) + \
            ": You don't really need to set readonly on a scratch copy.")
        ro_arg = "-r "
      if "no_overwrite" in parts[1:] or "preserve" in parts[1:]:
        if copy_imagename != "":
          print("Ignoring preserve option at line " + \
              str(self.line_ct_stack[0]) + \
              " because scratch option is present.")
        else:
          next_tape = 0
          while os.path.isfile(imagename):
            print("Found: " + imagename)
            next_tape += 1
            imagename =  base_imagename + "-" + str(next_tape) + extension
    if unit == None or unit == "":
      print("Need unit number for: " + line)
      return "die"
  
    if simh_dev not in _os8_from_simh_dev:
      print("At line " + str(self.line_ct_stack[0]) + \
          ": Unrecognized simh dev: " + simh_dev)
      return "die"
    os8dev = _os8_from_simh_dev[simh_dev]
  
    attach_comm = "att " + ro_arg + simh_dev + unit + " " + imagename
  
    if self.verbose:
      print("Line " + str(self.line_ct_stack[0]) + ": mount: " + \
          attach_comm)
    self.simh.send_cmd(attach_comm)
    return "success"


  #### boot_command ####################################################
  #
  # Check to see if the device to be booted has something attached.
  # If not die.
  # If so, boot it, and set our booted state to True.
  
  def boot_command (self, line, script_file):
    # First confirm something is attached to boot from.
    ucname = line.upper()
    boot_replies = [ucname + "\s+(.+)\r", "Non-existent device"]
    self.simh.send_cmd("show " + line)
    retval = self.simh._child.expect(boot_replies)
    if retval == 1:
      print("Attempt to boot non-existent device: " + line)
      return "die"
    reply = self.simh._child.after.decode()
    m = re.match("^(\S+)\s(\S+),\s+(attached to |not attached)(\S+)?,\s+(.+)\r",
        reply)
    if m == None:
      print("Could not determine if device " + line + " is attached; " +
          "got '" + reply + "'")
      return "die"

    # Caution match group we want ends with a space.
    if m.group(3) != "attached to ":
      print("Attempt to boot on non-attached device: " + line)
      return "die"
    
    boot_comm = "boot " + line
    if self.verbose:
      print("Line " + str(self.line_ct_stack[0]) + ": " + boot_comm)
    self.simh.send_cmd(boot_comm)
    self.booted = True
    return "success"


  #### os8_command #####################################################
  
  def os8_command (self, line, script_file):
    if not self.booted:
      print("Cannot run os8 command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"

    os8_comm = line
    if self.verbose: print("Line: " + \
       str(self.line_ct_stack[0]) + ": os8_command: " + os8_comm)

    # Resume OS/8 if necessary.
    if self.simh._context == "simh":
      self.resume_command(line, script_file)

    self.simh.os8_send_cmd ("\\.", os8_comm, self.debug)
    return "success"


  #### pal8_command ####################################################
  # The "pal8" script command comes in two forms:
  # The two argument form where the PAL8 status is printed on the fly
  # and the 3 argument form where all status goes into the listing file.
  # We do the 3 argument form with a simple "os8" script command.
  
  def pal8_command (self, line, script_file):
    if not self.booted:
      print("Cannot run pal8 command at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"

    m_2form = re.match (_two_arg_pal_re, line)
    if m_2form != None:

      # Resume OS/8 if necessary.
      if self.simh._context == "simh":
        self.resume_command(line, script_file)

      # Call the 2arg pal8 code that works hard at error analysis.
      return self.call_pal8 (m_2form.group(4), m_2form.group(1))
    else:
      m_3form = re.match (_three_arg_pal_re, line)
      if m_3form != None:
        # Just run the OS/8 command.
        os8_comm = "pal8 " + line
        if self.verbose:
          print("Line: " + str(self.line_ct_stack[0]) + \
              ": Calling 3-arg pal8 command: " + os8_comm)
        self.simh.os8_send_cmd ("\\.", os8_comm)
      else:
        print("At line " + str(self.line_ct_stack[0]) + \
          ": Unrecognized pal8 form: {" + line + "}.")
        return "fail"
    return "success"


  #### begin_command ###################################################
  
  def begin_command (self, line, script_file):
    if not self.booted:
      print("Cannot execute begin subcommand block at line " + \
        str(self.line_ct_stack[0]) + ". OS/8 has not been booted.")
      return "die"

    sub_commands = {"fotp": self.fotp_subcomm, "build": self.build_subcomm,
                    "absldr": self.absldr_subcomm,
                    "cdprog": self.cdprog_subcomm}
  
    m = re.match(_comm_re, line)
    if m == None:
      print("Could not parse sub-command at line " + \
        str(self.line_ct_stack[0]) + ": " + line)
    if m.group(1) not in sub_commands:
      print("Ignoring unrecognized sub-command at line " + \
        str(self.line_ct_stack[0]) + ": " + m.group(1))
      print("Ignoring everything to next end.")
      self.ignore_to_subcomm_end(line, script_file, "")
      return "fail"
    else:
      # Resume OS/8 if necessary.
      if self.simh._context == "simh":
        self.resume_command(line, script_file)

      return sub_commands[m.group(1)](m.group(3), script_file)
  
  
  #### run_build_build #################################################
  # ***CAUTION***
  # When you do this you are instructing BUILD to
  # OVERWRITE the system area.  If you do this to your
  # running RK05 pack by mistake, you WILL make a mess
  # and need to re-run mkos8 to re-make it.
  
  def run_build_build (self, os8_spec, cd_spec):
    self.simh.os8_send_cmd ("\\$", "BUILD", debug=True)
    self.simh.os8_send_cmd ("LOAD OS/8: ", os8_spec, debug=True)
    self.simh.os8_send_cmd ("LOAD CD: ", cd_spec, debug=True)
    return "success"


  #### build_subcomm ###################################################
  
  def build_subcomm (self, old_line, script_file):
    # A race condition results if we send ^C when we are already at
    # Monitor level.  So need_exit gets set to False, when we know
    # we have already exited build, and are at the monitor prompt.
    need_exit = True
    os8_comm = "RU " + old_line
    if self.verbose:
      print("Line " + str(self.line_ct_stack[0]) + ": " + os8_comm)
    prompt_str = "\n\\$$"
    if self.debug:
      print("sending to simh: " + os8_comm)
      print(" and expecting prompt: '\\n\\\\$$'")
    self.simh.os8_send_cmd ("\\.", os8_comm)
    self.simh._child.expect(prompt_str)
    
    for line in script_file:
      # if self.debug:
      #  print("line: " + line)
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      m = re.match(_comm_re, line)
      if m == None:
        print("Ignoring mal-formed build sub-command at line " + \
          str(self.line_ct_stack[0]) + ": " + line)
        continue
  
      build_sub = m.group(1)
      rest = m.group(3)
      if rest == None: rest = ""
      
      if build_sub not in _build_comm_regs:
        print("Unrecognized BUILD command at line " + \
            str(self.line_ct_stack[0]) + ": " + build_sub)
        continue
  
      if build_sub == "end":
        if rest == "":
          print("Warning! end statement encountered inside build with no argument at line " + \
              str(self.line_ct_stack[0]) + ".\nExiting build.")
          return "fail"
        elif rest != "build": 
          print("Warning! Mismatched begin/end blocks in BUILD at line " + \
              str(self.line_ct_stack[0]) + ".\nEncountered end: {" + \
              rest + "}. Exiting BUILD.")
          return "fail"
        
        if self.verbose:
          print("Line " + str(self.line_ct_stack[0]) + ": end BUILD")
        if self.debug:
          print("before: " + self.simh._child.before.decode().strip())
          print("after: " + self.simh._child.after.decode().strip())
        # Return to monitor level unless need_exit == False.
        if need_exit:
          self.simh.os8_send_ctrl ('c')

        return "success"
        
      build_re = _build_comm_regs[build_sub]
  
      if build_re != None:
        m2 = re.match(build_re, rest)
        if m2 == None:
          print("Ignoring mal-formed BUILD at line " + \
              str(self.line_ct_stack[0]) + ": " + build_sub + \
              " command: " + rest)
          continue
      
        if build_sub == "BUILD":
          if m2.group(1) == None or m2.group(1) == "":
            print("Missing source of OS8.BN. Ignoring BUILD command at line " + \
                str(self.line_ct_stack[0]) + ".")
            continue
          else: kbm_arg = m2.group(1)

          if m2.group(3) == None or m2.group(3) == "":
            print("Missing sorce of CD.BN. Ignoring BUILD command at line " + \
                str(self.line_ct_stack[0]) + ".")
            continue
          else: cd_arg = m2.group(3)
          
          if self.verbose:
            print("Line " + str(self.line_ct_stack[0]) + \
                 ": BUILD KBM: " + kbm_arg + ", CD: " + cd_arg)
          if self.debug:
            print("sending to simh: BUILD")
          self.simh.os8_send_line ("BUILD")

          build_build_replies = ["LOAD OS/8: "]
          build_build_replies.extend(_build_replies)
          
          if self.debug:
            print("expecting: " + str(build_build_replies))
          reply = self.simh._child.expect(build_build_replies)
          if self.debug:
            print("reply: " + str(reply))
            print("before: " + self.simh._child.before.decode().strip())
            print("after: " + self.simh._child.after.decode().strip())
          if reply != 0:
            print("No prompt for LOAD OS/8 in BUILD command within BUILD at line " + \
              str(self.line_ct_stack[0]) + ".")
            print("Instead got: {" + self.simh._child.after.decode() + "}.")
            print("Exiting BUILD.")
            return "die"
          if self.debug:
            print("sending to simh: " + kbm_arg)
          self.simh.os8_send_line (kbm_arg)

          build_build_replies = ["LOAD CD: "]
          build_build_replies.extend(_build_replies)
          
          if self.debug:
            print("expecting: " + str(build_build_replies))
          reply = self.simh._child.expect(build_build_replies)
          if self.debug:
            print("reply: " + str(reply))
            print("before: " + self.simh._child.before.decode().strip())
            print("after: " + self.simh._child.after.decode().strip())
          if reply != 0:
            print("No prompt for LOAD CD in BUILD command within BUILD at line " + \
              str(self.line_ct_stack[0]) + ".")
            print("Instead got: {" + self.simh._child.after.decode() + "}.")
            print("Exiting BUILD.")
            return "die"
          if self.debug:
            print("sending to simh: " + cd_arg)
          self.simh.os8_send_line (cd_arg)

          # Done with BUILD command dialog within BUILD.SV
          # Get that BUILD.SV prompt.
          if self.debug:
            print("Expecting prompt: '\\n\\\\$$'")
          self.simh._child.expect(prompt_str)
          if self.debug:
            print("Resume BUILD.SV command loop.")
          continue

      comm = build_sub + " " + rest
      if self.verbose:
        print("Line " + str(self.line_ct_stack[0]) + ": BUILD-> " + comm)

      if self.debug:
        print("sending to simh: " + comm)
      self.simh.os8_send_line (comm)
      if self.debug:
        print("expecting: " + str(_build_replies))
      reply = self.simh._child.expect(_build_replies)
      if self.debug:
        print("reply: " + str(reply))
        print("before: " + self.simh._child.before.decode().strip())
        print("after: " + self.simh._child.after.decode().strip())
      if reply > 3:
        print("BUILD error at line " + str(self.line_ct_stack[0]) + \
          " with command " + self.simh._child.before.decode().strip())
        print("\t" + self.simh._child.after.decode().strip())
        self.simh.os8_send_ctrl ('c')
      # Special case "BOOT" sub-command: May ask, "WRITE ZERO DIRECT?"
      if build_sub == "BOOT":
        if reply == 2:
          if self.debug:
            print("Boot received \"WRITE ZERO DIRECT?\"")
            print("sending to simh: Y")
          self.simh.os8_send_line("Y")
          if self.debug:
            print("Expecting \"SYS BUILT\"")
          reply = self.simh._child.expect("SYS BUILT")
          if self.debug:
            print("ZeroDir: reply: " + str(reply))
            print("before: " + self.simh._child.before.decode().strip())
            print("after: " + self.simh._child.after.decode().strip())
          need_exit = False
        elif reply == 0:
          reply = self.simh._child.expect("SYS BUILT")
          if self.debug:
            print("$: reply: " + str(reply))
            print("before: " + self.simh._child.before.decode().strip())
            print("after: " + self.simh._child.after.decode().strip())
          need_exit = False
        elif reply == 1:
          reply = self.simh._child.expect("\\.")
          if self.debug:
            print("SysBuilt: reply: " + str(reply))
            print("before: " + self.simh._child.before.decode().strip())
            print("after: " + self.simh._child.after.decode().strip())
    print("Warning end of file encountered with no end of BUILD command block at line " + \
      str(self.line_ct_stack[0]) + ".")
    return "fail"
  
  #### cdprog_subcomm ##################################################
  # Cycle through OS/8 command decoder with the command specified
  # in the argument.
  
  def cdprog_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    end_str = "cdprog " + old_line
    if self.verbose:
      print("Line: " + str(self.line_ct_stack[0]) + ": " + os8_comm)
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        retval = "fail"  # Return fail unless proven successful.
        if rest == None or rest == "":
          print("Warning! end statement encountered inside cdprog " + \
              "with no argument at line " + \
              str(self.line_ct_stack[0]) + ".")
          print("Expecting: {" + end_str + "}.")
        elif rest != end_str:
          print("Warning! Mismatched begin/end blocks in cdprog at line " + \
            str(self.line_ct_stack[0]) + ".\n")
          print("Expecting: {" + end_str + "}. Got: {" + rest + "}.\n")
        else:
          retval = "success"
          if self.verbose:
            print("Line " + str(self.line_ct_stack[0]) + ": end " + end_str)
        if retval == "fail":
          print("Exiting cdprog, possibly earlier than expected at line " + \
            str(self.line_ct_stack[0]) + ".")
        self.simh.os8_send_ctrl ('[')
        return retval
  
      # We could do some basic OS/8 command decoder synax checking here.
      comm = line
      if self.verbose:
        print("Line: " + str(self.line_ct_stack[0]) + ": * " + line)
      self.simh.os8_send_cmd ("\\*", line)
    print("Warning end of file encountered at line " + \
        str(self.line_ct_stack[0]) + \
        " with no end of cdprog command block.")
    self.simh.os8_send_ctrl ('[')
    return "fail"

  
  #### fotp_subcomm ####################################################
  
  def fotp_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    if self.verbose:
      print("Line: " + str(self.line_ct_stack[0]) + ": " + os8_comm)
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        if rest == None or rest == "":
          print("Warning! end statement encountered inside fotp " + \
              "with no argument at line " + \
              str(self.line_ct_stack[0]) + ".")
          return "fail"
        elif rest != "fotp":
          print("Warning! Mismatched begin/end blocks in FOTP at line " + \
              str(self.line_ct_stack[0]) + ".\nEncountered end: {" + \
              rest + "}. Exiting FOTP.")
          return "fail"

        if self.verbose:
          print("Line " + str(self.line_ct_stack[0]) + ": end FOTP")
        self.simh.os8_send_ctrl ('[')
        return "success"
  
      m = re.match(_fotp_re, line)
      if m == None:
        print("At line " + str(self.line_ct_stack[0]) + \
            ": ignoring mal-formed fotp file spec: {" + line + "}.")
        continue
  
      comm = line
      if self.verbose:
        print("Line: " + str(self.line_ct_stack[0]) + ": * " + line)
      self.simh.os8_send_cmd ("\\*", line)
    print("Warning end of file encountered at line " + \
        str(self.line_ct_stack[0]) +
        " with no end of FOTP command block.")
    return "fail"

  
  #### absldr_subcomm ##################################################
  # A clone of fotp_subcom.  Can we find a way to merge the common code?
  
  def absldr_subcomm (self, old_line, script_file):
    os8_comm = "RU " + old_line
    if self.verbose:
      print(" line: " + str(self.line_ct_stack[0]) + ": " + os8_comm)
    self.simh.os8_send_cmd ("\\.", os8_comm)
    
    for line in script_file:
      line = self.basic_line_parse(line, script_file)
      if line == None: continue
  
      # Test for special case, "end" and act on it if present.
      m = re.match(_comm_re, line)
      if m != None and m.group(1) != None and m.group(1) != "" and m.group(1)  == "end":
        rest = m.group(3)
        if rest == None or rest == "":
          print("Warning! end statement encountered inside ABSLDR " + \
              "with no argument at line " +
              str(self.line_ct_stack[0]) + ".")
          return "fail"
        elif rest != "absldr":
          print("Warning! Mismatched begin/end blocks in ABSLDR at " + \
              "line " + str(self.line_ct_stack[0]) + \
              ".\nEncountered end: {" + rest + "}. Exiting ABSLDR.")
          return "fail"
          
        if self.verbose:
          print("End ABSLDR at line " + str(self.line_ct_stack[0]) + ".")
        self.simh.os8_send_ctrl ('[')
        return "success"
  
      m = re.match(_absldr_re, line)
      if m == None:
        print("Ignoring mal-formed absldr file spec at line " + \
            str(self.line_ct_stack[0]) + ": {" + line + "}.")
        continue
  
      comm = line
      if self.verbose:
        print("Line : " + str(self.line_ct_stack[0]) + "* " + line)
      self.simh.os8_send_cmd ("\\*", line)
    print("Warning end of file encountered at line " + \
      str(self.line_ct_stack[0]) + "with no end of ABSLDR command block.")
    return "fail"


  #### check_exists ####################################################
  # Check existence of all files needed
  
  def check_exists (s, image_copyins):
    for copyin in image_copyins:
      image = copyin[1]
      image_path = dirs.os8mi + image
      if (not os.path.isfile(image_path)):
          print("Required file: " + image_path + " not found.")
          mkos8_abort(s)
      # else: print("Found " + image_path)
  
     
  #### Data Structures ################################################
  #
  # The make procedures use helper procedures
  # to confirm that the relevant input image file exists and
  # to perform the file copies.
  #
  # A data structure called "image copyin"
  #    describes the image file pathname relative to an implied root,
  #    provides a message string when the action is run,
  #    names a default destination device for whole image content copies,
  #    offers an optional array of specific file copy actions.
  #
  # FUTURE: Parse source path for ".tu56" vs. ".rk05" for more general use.
  # Currently all code assumes a copyin comes from a DECtape image.
  #
  # Example: We Install all files for ADVENT, the Adventure game:
  #
  # advent_copyin = ['RKB0:', 'subsys/advent.tu56',  "Installing ADVENT...", None]
  #
  # A DECtape device is chosen for attachment in SIMH and
  # a 'COPY *.*' command is filled in with the Destination device, and the chosen DECtape.
  #
  # A data structer called "file copyin"
  #     provides override destination to allow renames or varied destinations.
  #     names individual files within a copyin to use
  #
  # Example:  To copy the C compiler we want all .SV files on SYS
  #           but everything else to RKB0:
  #           (Note the useful /V option to invert the match.)
  #
  # cc8_sv_file_copyin   = ['SYS:', '*.SV']
  # cc8_rest_file_copyin = ['RKB0:', '*.SV/V']
  #
  # A 'COPY' command is filled in with the override destination and
  # The file spec is used with the chosen dectape instead of "*.*"
  #
  
  #### copyin_pair #####################################################
  # Copy two images into two destinations with two messages
  #
  # Assumes our context is "in simh".
  # Assumes dt0 and dt1 are free.
  # Assumes rk0 is the boot device
  # Detaches dt0 and dt1 after using them.
  # copyin0 mounts on dt0.  copyin1 mounts on dt1.
  # Either copyin or both can be None
  
  def copyin_pair (s, copyin0, copyin1, debug):
    if debug:
      if copyin0:
        print("Copying: " + copyin0[1] + " to: " + copyin0[0] + "from dt0")
      else:
        print("copyin0 is empty.")

      if copyin1:
        print("Copying: " + copyin1[1] + " to: " + copyin1[0] + "from dt1")
      else:
        print("copyin1 is empty.")
      
    if not copyin0 and not copyin1: return   # Nothing to do.
  
    # The order of events here is a bit funky because we want
    # to use both DECtape drives but also
    # switch between SIMH and OS/8 as infrequently as possible.
  
    if copyin0: self.simh.send_cmd ("attach -r dt0 " + dirs.os8mi + copyin0[1])
    if copyin1: self.simh.send_cmd ("attach -r dt1 " + dirs.os8mi + copyin1[1])
  
    self.simh.os8_restart()
  
    if copyin0:
      if self.verbose: print(copyin0[2])
      if copyin0[3]:                    # We have specific files to do.
        for file_copyin in copyin0[3]:
          self.simh.os8_send_cmd ("\\.", "COPY " + file_copyin[0] + "<DTA0:" + file_copyin[1])
      else:
        self.simh.os8_send_cmd ("\\.", "COPY " + copyin0[0] + "<DTA0:*.*")
  
    if copyin1:
      if self.verbose: print(copyin1[2])
      if copyin1[3]:                    # We have specific files to do.
        for file_copyin in copyin1[3]:
          self.simh.os8_send_cmd ("\\.", "COPY " + file_copyin[0] + "<DTA1:" + file_copyin[1])
      else:
        self.simh.os8_send_cmd ("\\.", "COPY " + copyin1[0] + "<DTA1:*.*")
  
    self.simh.back_to_cmd("\\.")
  
    if copyin0: self.simh.send_cmd ("detach dt0")
    if copyin1: self.simh.send_cmd ("detach dt1")
  
  
  #### do_all_copyins ##################################################
  
  def do_all_copyins (s, copyins, debug):
    pair_idx = 0
    pair_ct = int(len(copyins) / 2)
    while pair_idx < pair_ct:
      copyin_pair(s, copyins[pair_idx * 2], copyins[pair_idx * 2 + 1], debug)
      pair_idx += 1
    if pair_ct * 2 < len(copyins):
      copyin_pair(s, copyins[len(copyins) - 1], None, debug)
  
  
  
Deleted lib/simh.py.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# simh/__init__.py - A wrapper class around pexpect for communicating
#   with an instance of the PiDP-8/I SIMH simulator running OS/8.
#
#   See ../doc/class-simh.md for a usage tutorial.
#
# Copyright © 2017 by Jonathan Trites, © 2017-2019 by William 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.
########################################################################

import os
import pexpect
import pkg_resources
import subprocess
import tempfile
import time
import re
import sys

import pidp8i

class simh: 
  # pexpect object instance, set by ctor
  _child = None

  # Constant used by os8_kbd_delay, assembled in stages:
  #
  # 1. PDP-8 RS-232 bits per character: 7-bit ASCII plus necessary
  #    start, stop, and parity bits.
  #
  # 2. The ratio of the instructions per second ratios of a PDP-8/I to
  #    that of the host hardware running the simulator.  The former is
  #    an approximate value; see lib/pidp8i/ips.py.in for the value and
  #    its defense.  The latter is either the IPS rate for:
  #
  #    a) a Raspberry Pi Model B+, that being the slowest host system
  #       we run this simulator on; or
  #
  #    b) the IPS rate of the actual host hardware if you have run the
  #       "bin/teco-pi-demo -b" benchmark on it.
  #
  # 2. The fact that real PDP-8s ran OS/8 reliably at 300 bps, and have
  #    been claimed to get flaky as early as 600 bps by some.  (Others
  #    claim to have run them up to 9,600 bps.)
  #
  # 3. The "safe BPS" value is the fastest bit per second speed actual
  #    PDP-8 hardware was known to run OS/8 terminal I/O at.  In this
  #    case, it is the high-speed tape reader.
  #
  #    TODO: We may be able to increase this.
  #
  #    We have one report that OS/8 was tested with terminals up to
  #    about ~600 bps before becoming unreliable.
  #
  #    We have another report that OS/8 could run under ETOS with
  #    9,600 bps terminals, but we don't know if that tells us anything
  #    about OS/8 running without the ETOS multitasking hardware.
  #
  # 4. Given above, calculate safe characters per second for host HW.
  #
  # 5. Invert to get seconds per character, that being the delay value.
  _bpc = 7 + 1 + 1 + 1                                       # [1]
  _ips_ratio = float (pidp8i.ips.current) / pidp8i.ips.pdp8i # [2]
  _pdp8i_safe_bps = 300                                      # [3]
  _host_safe_cps = _pdp8i_safe_bps * _ips_ratio / _bpc       # [4]
  _os8_kbd_delay = 1 / _host_safe_cps                        # [5]

  # Known OS/8 error strings and a flag indicating whether the error
  # dumps us back out to the OS/8 command monitor or leaves us in the
  # called program.

  _os8_errors = [
    # The date comment tells when each message is observed and validated
    #
    # OS/8 Handbook 1974 page 1-43/81 Keyboard Monitor Error Messages:
    ["MONITOR ERROR 2 AT \d+ \\(DIRECTORY I/O ERROR\\)", True],   # 2018.02.11
    ["MONITOR ERROR 5 AT \d+ \\(I/O ERROR ON SYS\\)", True],
    ["MONITOR ERROR 6 AT \d+ \\(DIRECTORY I/O ERROR\\)", True],
    ["(\S+) NOT AVAILABLE", False],
    ["(\S+) NOT FOUND", False],                                   # 2018.02.11
    # OS/8 Handbook 1974 page 1-51/89 Command Decoder Error Messages
    ["ILLEGAL SYNTAX", False],                                    # 2018.02.11
    ["(\S+) DOES NOT EXIST", False],
    # ["(\S+) NOT FOUND", False],                                 # See above
    ["TOO MANY FILES", False],
    # OS/8 Handbook 1974 page 1-75/113 CCL Error Messages
    ["BAD DEVICE", False],
    ["BAD EXTENSION", False],
    # OS/8 Handbook 1974 page 1-106/144 PIP Error Messages
    ["ARE YOU SURE", False],
    ["BAD DIRECTORY ON DEVICE #\s?\d+", False],
    ["BAD SYSTEM HEAD", False],
    ["CAN'T OPEN OUTPUT FILE", False],
    ["DEVICE #\d+ NOT A DIRECTORY DEVICE", False],
    ["DIRECTORY ERROR", False],
    ["ERROR DELETING FILE", False],
    ["ILLEGIAL BINARY INPUT, FILE #\d+", False],
    ["INPUT ERROR, FILE #\s?\d+", False],
    ["IO ERROR IN \\(file name\\) --CONTINUING", False],
    ["NO ROOM FOR OUTPUT FILE", False],
    ["NO ROOM IN \\(file name\\) --CONTINUING", False],
    ["OUTPUT ERROR", False],
    ["PREMATURE END OF FILE, FILE #\s?\d+", False],
    ["ZERO SYS?", False],
    # OS/8 Handbook 1974 page 2-81/244: DIRECT Error Messages
    ["BAD INPUT DIRECTORY", False],
    ["DEVICE DOES NOT HAVE A DIRECTORY", False],
    ["ERROR CLOSING FILE", False],
    ["ERROR CLOSING FILE", False],
    ["ERROR READING INPUT DIRECTORY", False],
    ["ILLEGAL \\*", False],
    # OS/8 Handbook 1974 page: 2-109/272: FOTP Error Messages
    ["ERROR ON INPUT DEVICE, SKIPPING \\((\S+)\\)", False],
    ["ERROR ON OUTPUT DEVICE, SKIPPING \\((\S+)\\)", False],
    ["ERROR READING INPUT DIRECTORY", False],
    ["ERROR READING OUTPUT DIRECTORY", False],
    ["ILLEGAL \\?", False],
    ["NO FILES OF THE FORM (\S+)", False],
    ["NO ROOM, SKIPPING \\((\S+)\\)", False],
    ["SYSTEM ERROR-CLOSING FILE", False],
    ["USE PIP FOR NON-FILE STRUCTURED DEVICE", False],
    ["LINE TOO LONG IN FILE#\d+", False],
  ]

  # Pattern to match a SIMH command.  The command verb ends up in
  # match().group(1), and anything after the verb in group(3).
  _simh_comm_re = re.compile ("^\s*(\S+)(\s+(.*))?$")

  # Significant prefixes of SIMH command verbs that transition from SIMH
  # command context back into the simulation: BOOT, CONTINUE, and GO.
  # We need only the first letter in all cases, since these particular
  # commands are not ambiguous.  They're uppercase because the code that
  # uses this always uppercases the command before searching this list.
  _enters_os8_context = ["B", "C", "G"]


  #### ctor ############################################################
  # basedir is the parent of bin/{pdp8,pidp8i-sim}.
  #
  # Setting the skip_gpio flag to True causes us to use bin/pdp8 instead
  # of bin/pidp8i-sim, which runs the simulator with the PiDP-8/I GPIO 
  # thread disabled.  You might do this because you don't need any front
  # panel interaction or because you know you're not running on an RPi
  # in the first place.
  
  def __init__ (self, basedir, skip_gpio = False):
    # Start the simulator instance
    sim = 'pdp8' if skip_gpio else 'pidp8i-sim';
    self._child = pexpect.spawn (os.path.join (basedir, 'bin', sim))
    self._valid_pip_options = ["/A", "/B", "/I"]
    self._os8_file_re = re.compile("(\S+):(\S+)?")
    self._os8_error_match_strings = []
    self._os8_fatal_check = []
    self.verbose = False

    # We keep track of what our command context is so our caller does
    # not need to explicitly call back_to_cmd() or sendcontrol ('e').
    # We keep track of the command context and transition automatically.
    self._context = "simh"
    
    # Parse our OS/8 Errors table into actionable chunks
    for error_spec in self._os8_errors:
      self._os8_error_match_strings.append(error_spec[0])
      self._os8_fatal_check.append(error_spec[1])

    self._pip_to_replies = ['\\^']
    self._pip_to_replies.extend(self._os8_error_match_strings)
    # Did command start the command decoder or die with a monitor error?
    self._cd_replies = ['\\*']
    self._cd_replies.extend(self._os8_error_match_strings)

    # Turn off pexpect's default inter-send() delay.  We add our own as
    # necessary.  The conditional tracks an API change between 3 and 4.
    pev4 = (pkg_resources.get_distribution("pexpect").parsed_version >
            pkg_resources.parse_version("4.0"))
    self._child.delaybeforesend = None if pev4 else 0

    # Wait for the simulator's startup message.
    if not self.try_wait ('PDP-8 simulator V.*git commit id: [0-9a-f]', 10):
      raise RuntimeError ('Simulator failed to start')


  #### back_to_cmd ######################################################
  # Pause the simulation and return to the SIMH command prompt when the
  # simulated software emits the given prompt string.  Typically used to
  # wait for OS/8 to finish running a command so we can do something
  # down at the SIMH layer instead.

  def back_to_cmd (self, prompt):
    self._child.expect ("\n%s$" % prompt)
    self.os8_kbd_delay ()
    self._child.sendcontrol ('e')
    self._context = "simh"


  #### os8_get_file ####################################################
  # Rough inverse of os8_send_file.
  #
  # Both paths must be given and are used literally.  (Contrast our
  # inverse, where the destinatinon file name is produced from the
  # source if not given.)
  #
  # When this function is called to pull a file sent by our inverse, the
  # conversion should be lossless except for the transforms done by our
  # underlying utility tools, such as the LF -> CR+LF done by txt2ptp
  # but not undone by ptp2txt.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_get_file (self, intname, extname):
    # Attach a blank paper tape to the simulator.
    ptf = tempfile.NamedTemporaryFile (suffix = '.pt', delete = False)
    ptf.close ()
    ptn = ptf.name
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach ptp ' + ptn)

    # Punch internal file to external paper tape image
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'PUNCH ' + intname);
    self.back_to_cmd ('\\.')        # wait for transfer to finish

    # Convert text file from SIMH paper tape format
    tool = os.path.join (pidp8i.dirs.build, 'bin', 'ptp2txt')
    self.send_cmd ('detach ptp')
    subprocess.call (tool + ' < ' +  ptn + ' > ' + extname, shell = True)

    # Return to OS/8, just because that's where we were on entry, so we
    # should not change that.
    self.os8_restart ()


  #### os8_kbd_delay ###################################################
  # Artificially delay the media generation process to account for the
  # fact that OS/8 lacks a modern multi-character keyboard input buffer.
  # It is unsafe to send text faster than a contemporary terminal could,
  # though we can scale it based on how much faster this host is than a
  # real PDP-8.  See the constants above for the calculation.

  def os8_kbd_delay (self):
    time.sleep (self._os8_kbd_delay)


  #### os8_send_cmd ####################################################
  # Wait for an OS/8 command prompt running within SIMH, then send the
  # given line.
  #
  # The default timeout may seem excessive, but it is based on hard
  # experience: when SIMH is running on a slow host with slow devices
  # (e.g. the byte-by-byte transfer of the TD8E tape controller) a
  # single OS/8 command can take a very long time if it requires a lot
  # of I/O.  If you are calling this for a command that you know for a
  # fact takes less time on all hosts and with all practical device
  # configurations, we encourage you to pass a smaller value.
  #
  # The caller must pass a prompt string because OS/8 has several
  # different prompt types: ., *, $, and #, at least.  Beware in passing
  # these that they're treated as regular expressions, so characters
  # special in Python REs must be escaped.  And then since the RE escape
  # character (\) is also special in Python strings, you must double-
  # escape *it*.  So, '\\$' is a reasonable thing to pass as the prompt
  # value, meaning "look for a literal $ character."
  #
  # This routine requires the caller to ensure that the system is in
  # OS/8 Keyboard Monitor context — that is, ready for another OS/8
  # command — before calling it.  While this routine is able to check
  # whether we're in OS/8 context as a prerequisite, it is not practical
  # for us to return the system to OS/8 context automatically from some
  # other context because that would require us to know the current
  # context in detail, but only the caller has that full knowledge.
  # 
  # Part of the problem is that in order to synchronize this object's
  # internal state machine with the SIMH + OS/8 + running program state,
  # we have to somehow provoke a prompt character from the running
  # program.  How do we do that without knowing the current context?
  # In some contexts, a CR or LF will do it, in others BS, and in others
  # it'll take Ctrl-C.  Then you have a new problem, with is that those
  # same characters aren't harmless: they'll change the very context
  # we're trying to probe!  For instance, a Ctrl-C sent to the OS/8
  # Keyboard Monitor just results in another prompt, but a Ctrl-C sent
  # to a program running *under* OS/8 might kill it.  Or it might be
  # treated as input.  Or it might be ignored entirely.
  #
  # There is no magic sequence we can send to SIMH or OS/8 to return the
  # system to OS/8 Keyboard Monitor context without either changing the
  # context in some way that might break the caller's needed state (e.g.
  # Ctrl-E, go 7600) or lose data (e.g. Ctrl-C) or fail entirely (e.g.
  # Enter.)  It's up to the caller to arrange this.

  def os8_send_cmd (self, prompt, line, debug=False, timeout=60):
    if self._context != 'os8': 
      print "OS/8 is not running. Cannot execute: " + line 
      return
    if debug:
      print "os8_send_cmd: expecting: " + prompt
      print "\tLast match before: {" + self._child.before + "}"
      print "\tLast match after: {" + self._child.after + "}"
    self._child.expect ("\n%s$" % prompt, timeout = timeout)
    self.os8_send_line (line)


  #### os8_send_ctrl ###################################################
  # Send a control character to OS/8 corresponding to the ASCII letter
  # given.  We precede it with the OS/8 keyboard delay, since we're
  # probably following a call to os8_send_line or os8_send_cmd.

  def os8_send_ctrl (self, char):
    cc = char[0].lower ()
    self.os8_kbd_delay ()
    self._child.sendcontrol (cc)
    
    if cc == 'e': self._context = 'simh'


  #### mk_os8_name # ###################################################
  # Create an OS/8 filename: of the form XXXXXX.YY
  # From a POSIX path.

  def mk_os8_name(self, dev, path):
    bns = os.path.basename (path)
    bns = re.sub("-|:|\(|\)|!", "", bns)
    bns = bns.upper()
    if "." not in bns:
      return dev + bns[:min(6, len(bns))]
    else:
      dot = bns.index('.')
      return dev + bns[:min(6, dot, len(bns))] + "." + bns[dot+1: dot+3]


  #### os8_send_file ###################################################
  # Send a copy of a local text file to OS/8.  The local path may
  # contain directory components, but the remote must not, of course.
  #
  # If the destination file name is not uppercase, it will be so forced.
  #
  # If the destination file name is not given, it is taken as the
  # basename of the source file name.
  #
  # The file is sent via the SIMH paper tape device through PIP in its
  # default ASCII mode, rather than character by character for two reasons:
  #
  # 1. It's faster.  It runs as fast as the simulator can process the
  #    I/O instructions, without any os8_kbd_delay() hooey.
  #
  # 2. It allows lowercase input regardless of the way the simulator is
  #    configured.  ASCII is ASCII.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_send_file (self, source, dest = None):
    # Create path and file names not given
    bns = os.path.basename (source)
    if dest == None: dest = bns
    dest = dest.upper ()

    # Convert text file to SIMH paper tape format
    bdir = pidp8i.dirs.build
    pt   = os.path.join (bdir, 'obj', bns + '.pt')
    tool = os.path.join (bdir, 'bin', 'txt2ptp')
    subprocess.call (tool + ' < ' + source + ' > ' + pt, shell = True)

    # Paper tape created, so attach it read-only and copy it in.  We're
    # relying on txt2ptp to insert the Ctrl-Z EOF marker at the end of
    # the file.
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach -r ptr ' + pt)
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'R PIP')
    self.os8_send_cmd ('\\*', dest + '<PTR:')
    self._child.expect ('\\^')
    self.os8_send_ctrl ('[')      # finish transfer
    self._child.expect ('\\*')
    self.os8_send_ctrl ('[')      # exit PIP


  #### pip_error_handler ###############################################
  # Common error handler for os8_pip_to and os8_pip_from

  def pip_error_handler(self, caller, reply):
    print "PIP error from inside " + caller + ": "
    print "\t" + self._child.before.strip()
    print "\t" + self._child.after.strip()
    
    # Was this error fatal or do we need to clean up?
    # Remember we subtract 1 from reply to get index into error tables.
    if not self._os8_fatal_check[reply - 1]:
      # Non fatal error.  Exit pip to the monitor
      self.os8_send_ctrl ('[')      # exit PIP


  #### os8_pip_to ###################################################
  # Send a copy of a local file to OS/8 using PIP.
  #
  # The file is sent via the SIMH paper tape device through PIP
  # specifying a transfer option.  If no option is specified,
  # ASCII is assumed.
  #
  # In ASCII mode, we pre-process with txt2ptp which translates
  # POSIX ASCII conventions to OS/8 conventions.  In all other
  # modes, we do not do any translation.
  #
  # However, we should supply a sacrificial NULL as an additional character
  # because the OS/8 PTR driver throws the last character away. (NOT DONE YET)
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_pip_to (self, path, os8name, option = None):
    if option == None: option = ""
    # If os8name is just a device, synthesize an upcased name from
    # the POSIX file basename.
    if not os.path.exists(path):
      print path + " not found. Skipping."
      return
    m = re.match(self._os8_file_re, os8name)
    if m != None and (m.group(2) == None or m.group(2) == ""):
        dest = self.mk_os8_name(os8name, path)
    else:
        dest = os8name

    did_conversion = False
    if option == "" or option == "/A":
      # Convert text file to SIMH paper tape format in current dir of path.
      if self.verbose: print "Format converting " + path
      bdir = pidp8i.dirs.build
      # Create uniquified temp path name.
      pt   = path + "-" + str(os.getpid()) + ".pt_temp"
      tool = os.path.join (bdir, 'bin', 'txt2ptp')
      subprocess.call (tool + ' < ' + path + ' > ' + pt, shell = True)
      did_conversion = True
    elif option not in self._valid_pip_options:
      print "Invalid PIP option: " + option + ". Ignoring: " + path + " to OS/8."
      return
    else:
      pt = path

    # TODO: Sacrificial extra character code goes here.

    # Paper tape created, so attach it read-only and copy it in.  We're
    # relying on txt2ptp to insert the Ctrl-Z EOF marker at the end of
    # the file.
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach -r ptr ' + pt)
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'R PIP')
    # Was the start of PIP successful, or did we get a Monitor error?
    reply = self._child.expect (self._cd_replies)
    if reply != 0:
      self.pip_error_handler ("os8_pip_to", reply)
      return

    # Has the read-in been successful?
    self.os8_send_line (dest + '<PTR:' + option)
    reply = self._child.expect (self._pip_to_replies)
    if reply !=0:
      self.pip_error_handler("os8_pip_to", reply)
      if did_conversion:
        os.remove(pt)
      return

    self.os8_send_ctrl ('[')      # finish transfer
    self._child.expect ('\\*')
    self.os8_send_ctrl ('[')      # exit PIP
    # We could detach ptr and restart OS/8 here, but we don't need to.
    # Do remove the temp file if we created one.
    if did_conversion:
      os.remove (pt)


  #### os8_pip_from ###################################################
  # Fetch a file from OS/8 to a local path using PIP.
  #
  # The OS/8 source filename is synthesized from the basename of the path,
  # upcasing if necessary.
  #
  # The file is sent via the SIMH paper tape device through PIP
  # specifying a transfer option.  If no option is specified,
  # ASCII is assumed.
  #
  # In ASCII mode, we post-process with ptp2txt which translates
  # POSIX ASCII conventions to OS/8 conventions.  In all other
  # modes, we do not do any translation.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_pip_from (self, os8name, path, option = None):
    if option == None: option = ""
    # If path is not a file, use the name portion of os8name.
    if os.path.isdir(path):
      colon = os8name.find(':')
      if colon == -1:                # No dev, just a name.
        path = path + "/" + os8name
      else:
        path = path + "/" + os8name[colon+1:]

    if option != "" and option not in self._valid_pip_options:
      print "Invalid PIP option: " + option + ". Ignoring os8_pip_from on: " + path
      return

    self.back_to_cmd ('\\.')
    self.send_cmd ('attach ptp ' + path)
    self.os8_restart ()
    
    self.os8_send_cmd ('\\.', 'R PIP')
    # Was the start of PIP successful or did we get a Monitor error?
    reply = self._child.expect (self._cd_replies)
    if reply != 0:
      self.pip_error_handler ("os8_pip_from", reply)
      return
    
    self.os8_send_line ('PTP:<' + os8name + option)

    reply = self._child.expect (self._cd_replies)
    if reply !=0:
      self.pip_error_handler ("os8_pip_from", reply)
      # There is an empty PTP file we need to remove.
      os.remove(path)
      return

    self.os8_send_ctrl ('[')      # exit PIP
    self.back_to_cmd ('\\.')
    self.send_cmd ('detach ptp')  # Clean flush of buffers.
    self.os8_restart ()

    if option == "" or option == "/A":
      if self.verbose: print "Format converting " + path
      # Convert text file to SIMH paper tape format
      bdir = pidp8i.dirs.build
      # Create uniquified temp path name.
      pf = path + "-" + str(os.getpid()) + ".pf_temp"
      os.rename(path, pf)
      tool = os.path.join (bdir, 'bin', 'ptp2txt')
      subprocess.call (tool + ' < ' + pf + ' > ' + path, shell = True)
      os.remove(pf)


  #### os8_send_line ###################################################
  # Core of os8_send_cmd.  Also used by code that needs to send text
  # "blind" to OS/8, without expecting a prompt, as when driving EDIT.

  def os8_send_line (self, line):
    self.os8_send_str (line)
    self._child.send ("\r")


  #### os8_send_str ########################################################
  # Core of os8_send_line.  Also used by code that needs to send text
  # "blind" to OS/8, without expecting a prompt and without a CR, as
  # when driving TECO.

  def os8_send_str (self, str):
    for i in xrange (0, len (str)):
      self._child.send (str[i])
      self.os8_kbd_delay ()


  #### os8_resume #######################################################
  # Resume OS/8.
  #
  # It would be nice if we could just send  the "cont" command
  # and have python expect and OS/8 synch right up.
  # But so far we have not figured out how to do that.
  # To resume OS/8 from SIMH we need to provoke a prompt.
  # Typing a rubout or ^U at a SIMH terminal session does this.
  # But not when SIMH is run under python expect.
  # We don't know why.
  #
  # boot works
  # go 7600 works
  # ^C <pause> \n\r works.
  #
  # The resume command uses the ^C method as the least disruptive
  # to system state.

  def os8_resume (self):
    if self._context == "os8": return   # Already running.
    
    self.send_cmd("cont")   # sets os8 context for us.

    # Now provoke a keyboard monitor prompt.
    self.os8_send_ctrl('c')
    self.os8_kbd_delay()
    self.os8_send_str('\r\n')
  

  #### os8_restart #######################################################
  # Called while in the SIMH command prompt, this restarts OS/8.
  #
  # This one-line function exists to abstract the method we use and to
  # document the reason we do it this way.
  #
  # Currently we do this by calling the OS/8 command entry point, which
  # has the virtue that it forces another . prompt, which keeps the
  # send/expect sequencing simple when switching between OS/8 and SIMH
  # command modes.
  #
  # That is why we don't use "cont" here instead: it requires that the
  # caller always be aware of when the . prompt went out; complicated.
  #
  # Another simple alternative is "boot rk0", which actually benchmarks
  # a smidge faster here.  We choose this method instead because we
  # expect that some of our callers will want to do several different
  # things in a single OS/8 session, which rebooting would screw up.

  def os8_restart (self):
    self.send_cmd ("go 7600")


  #### os8_squish ########################################################
  # Wraps the OS/8 SQUISH command for a given device.

  def os8_squish (self, device):
    self.os8_send_cmd ('\\.', "SQUISH " + device + ":")
    self.os8_kbd_delay ()
    self._child.send ("Y\r");


  #### os8_zero_core ###################################################
  # Starting from OS/8 context, bounce out to SIMH context and zero all
  # of core excepting:
  #
  # 0. zero page - many apps put temporary data here
  # 1. the top pages of fields 1 & 2 - OS/8 is resident here
  # 2. the top page of field 2 - OS/8's TD8E driver (if any) lives here
  #
  # We then restart OS/8, which means we absolutely need to do #1 and
  # may need to do #2.  We could probably get away with zeroing page 0.
  #
  # All of the above explains why we have this special OS/8 alternative
  # to the zero_core() method.

  def os8_zero_core (self):
    self.back_to_cmd ('\\.')
    self.send_cmd ('de 00200-07577 0')
    self.send_cmd ('de 10000-17577 0')
    self.send_cmd ('de 20000-27577 0')
    self.send_cmd ('de 30000-77777 0')
    self.os8_restart ()


  #### quit ############################################################
  # Quits the simulator and waits for it to exit

  def quit (self):
    self.send_cmd ("q")
    self._child.expect (pexpect.EOF)


  #### read_tail #######################################################
  # Watch for a literal string, then get what follows on that line.

  def read_tail (self, head, timeout = -1):
    self._child.expect_exact ([head], timeout)
    return self._child.readline ()


  #### send_cmd ########################################################
  # Wait for a SIMH command prompt and then send the given command.
  # If we are not in the simh context send ^e and set context "simh".
  # If we are not in simh context, send ^e set context "simh"
  #    and hope for the best.
  # If we issue a command that enters os8 context, set context "os8".
  # Note exiting out of OS/8 into the SIMH context is a bit of a
  # trap door. Resynchronizing with python expect requires provoking
  # a prompt, and prompts are context specific.
  # Perhaps we should require separate and explicit commands to
  # escape to SIMH. But for now, just be careful to use os8_resume
  # after calling send_cmd.
  
  def send_cmd (self, cmd):
    if self._context == "os8":
      self._child.expect ("\n\\.$")
      self._child.sendcontrol ('e')
      self._context = "simh"
    elif self._context != "simh":
      self._child.sendcontrol ('e')
      self._context = "simh"
      
    self._child.expect ("sim> $")
    self._child.sendline (cmd)
    m = re.match (self._simh_comm_re, cmd)
    if m != None and m.group(1)[:1].upper() in self._enters_os8_context:
      self._context = "os8"


  #### send_line #######################################################
  # Sends the given line "blind", without waiting for a prompt.

  def send_line (self, line):
    self._child.sendline (line)


  #### set_logfile #####################################################

  def set_logfile (self, lf):
    self._child.logfile = lf


  #### spin ############################################################
  # Let child run without asking anything more from it, with an optional
  # timeout value.  If no value is given, lets child run indefinitely.

  def spin (self, timeout = None):
    self._child.expect (pexpect.EOF, timeout = timeout)


  #### try_wait ########################################################
  # A wrapper around self._child.expect which catches exceptions and
  # returns false on pexpect timeout.  If you pass a list instead of a
  # string, it also returns true if the match wasn't for the first
  # element, so you can pass [success, failure1, failure2, etc.] to
  # check for a known-success case and one or more failure cases.

  def try_wait (self, matches, timeout = -1):
    try:
      return self._child.expect (matches, timeout = timeout) == 0
    except pexpect.exceptions.TIMEOUT:
      sys.stderr.write ("Exceeded " + str(timeout) + " sec timeout " +
          "waiting for " + str(matches) + "\n")
      return False
    except:
      sys.stderr.write ("Failed to match " + str(matches) + 
          ": unknown exception.\n")
      return False


  #### zero_core #######################################################
  # From SIMH context, zero the entire contents of core, which is
  # assumed to be 32 kWords.
  #
  # SIMH's PDP-8 simulator doesn't start with core zeroed, on purpose,
  # because the actual hardware did not do that.  SIMH does not attempt
  # to simulate the persistence of core memory by saving it to disk
  # between runs, but the SIMH developers are right to refuse to do this
  # by default: you cannot trust the prior state of a PDP-8's core
  # memory before initializing it yourself.
  #
  # See os8_zero_core () for a less heavy-handed alternative for use
  # when running under OS/8.

  def zero_core (self):
    self.send_cmd ('de all 0')
  

  #### describe_dev_config #############################################
  # We provide an interface to alter SIMH device configurations for
  # specific parameters and specific devices
  #
  # dev configs supported:  rx, tti, tape
  #
  # rx:     RX8E, RX28   RX8E is the simh name for RX01 support.
  #                      RX28 is the simh name for RX02 support.
  # tti:    KSR, 7b      7b is full keyboard support.
  #                      KSR forces upcase of lower case keys on input.
  # tape:   td, dt       td is the TD8E DECtape device
  #                      dt is the TC08 DECtape device

  def describe_dev_config (self, name):
    if name == "tape":
      lines = self.do_simh_show("dt")
      dev_status = self.parse_show_tape_dev(lines)

      if dev_status == "dt": return "dt"
      else:
        lines = self.do_simh_show("td")
        return self.parse_show_tape_dev(lines)
    elif name == "rx":
      lines = self.do_simh_show("rx")
      return self.parse_show_rx_dev(lines)
    elif name == "tti":
      lines = self.do_simh_show("tti")
      return self.parse_show_tti(lines)
    else: return None

      
  #### do_simh_show  ###################################################
  # Calls show on the device name.
  # Returns array of lines from output.

  def do_simh_show (self, name):
    supported_shows = ["dt", "td", "tti", "rx"]
    if name not in supported_shows: return None
    
    ucname = name.upper()
    self.send_cmd("show " + name)
    self._child.expect(ucname + "\s+(.+)\r")
    lines = self._child.after.split ("\r")
    return lines


  #### parse_show_tape_dev  ############################################
  # Returns current state of DECtape support.
  # One of: disabled, td, dt, or None if parse fails.

  def parse_show_tape_dev (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^(TD|DT)\s+(disabled|(devno=\S+,\s(\d)\s+units))$")
    m = re.match(is_enabled_re, lines[0])

    if m == None or m.group(1) == None or m.group(2) == None: return None
    if m.group(2) == "disabled": return "disabled"
    elif m.group(1) == "TD": return "td"
    elif m.group(1) == "DT": return "dt"
    else: return None


  #### parse_show_tape_attached  ########################################
  # Returns an ordered list of files attached or None if disabled.
  def parse_show_tape_attached (self, lines):
    if lines == None: return None
    if len(lines) < 2: return None
    attached = {}
    attachment_re = re.compile("^\s+(((DT|TD)(\d)(.+),\s+(not\s+attached|attached\s+to\s+(\S+)),(.+))|12b)$")
    for line in lines[1:]:
      m = re.match(attachment_re, line)
      if m == None or m.group(1) == None or m.group(1) == "12b": continue
      filename = m.group(7)
      if filename == None: filename = ""
      attached[m.group(4)] = filename
    return attached

  #### do_print_lines ###################################################
  # Debugging aid. Prints what we parsed out of _child.after.
  def do_print_lines (self, lines):
    for line in lines:
      print line

  #### simh_configure routines #########################################
  # These routines affect the state of device configuration in SIMH.
  # They are intended as robust ways to toggle between incompatible
  # configurations of SIMH:
  # Choice of TD8E or TC08 DECtape (SIMH td and dt devices).
  # Choice of RX01 or RX02 Floppy emulation.
  # The SIMH rx device sets RX8E for RX01, and RX28 for RX02.  
  # Choice of KSR or 7bit console configuration.
  #
  # When re-configuring dt, dt, and rx devices, any attached
  # images are detached before reconfiguration is attempted.
  # (SIMH errors out if you don't detach them.)
  #
  # The check to see if the change is unnecessary.
  # For now they return None if no change necessary.
  #
  # After re-configuring the device, the SIMH show command is used
  # to confirm the re-configuration was successful.
  #
  # In future, we should add exception handling for no change necessary.
  # For now, return True if the change was successful and False if not.


  def set_tape_config (self, to_tape):
    if to_tape == "dt": from_tape = "td"
    elif to_tape == "td": from_tape = "dt"
    else:
      print "Cannot set_tape_config for " + to_tape
      return False

    if self.verbose: print "Disable: " + from_tape + ", and enable: " + to_tape
    
    lines = self.do_simh_show(from_tape)
    from_status = self.parse_show_tape_dev(lines)

    if from_status == None:
      print "do_tape_change: Trouble parsing \'show " + from_tape + "\' output from simh. Giving up on:"
      self.do_print_lines (lines)
      return False

    if from_status == "disabled": print from_tape + " already is disabled."
    else:
      attached_from = self.parse_show_tape_attached(lines)
      if attached_from == None:
        print "do_tape_change: Trouble parsing \'show " + from_tape + "\' output from simh. Giving up on:"
        self.do_print_lines (lines)
        return False
      else:
        for unit in attached_from.keys():
          if attached_from[unit] != "":
            det_comm = "det " + from_tape + unit
            if self.verbose: print det_comm + "(Had: " + attached_from[unit] + ")"
            self.send_cmd(det_comm)
        self.send_cmd("set " + from_tape + " disabled")

    lines = self.do_simh_show(to_tape)
    to_status = self.parse_show_tape_dev(lines)

    if to_status == None:
      print "do_tape_change: Trouble parsing \'show " + to_tape + "\' output from simh. Giving up on:"
      self.do_print_lines (lines)
      return False
    elif to_status != "disabled": print to_tape + " already is enabled."
    else:
      self.send_cmd("set " + to_tape + " enabled")    

    # Test to confirm to_tape is now enabled.

    lines = self.do_simh_show(to_tape)
    to_status = self.parse_show_tape_dev(lines)

    if to_status == None:
      print "Failed enable of " + to_tape + ". Parse fail on \'show " + to_tape + "\'. Got:"
      self.do_print_lines (lines)
      return False
    elif to_status == "disabled":
      print "Failed enable of " + to_tape + ". Device still disabled."
      return False
    else: 
      return True


  #### parse_show_rx_dev ###############################################
  # Show the rx device configuration.

  def parse_show_rx_dev (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^\s*(RX)\s+(disabled|((RX8E|RX28),\s+devno=\S+,\s+(\d)\s+units))$")
    m = re.match(is_enabled_re, lines[0])
    if m == None or m.group(2) == None: return None
    if m.group(2) == "disabled": return "disabled"
    return m.group(4)


   #### parse_show_rx_attached ##########################################
  # Returns an ordered list of files attached or None if disabled.

  def parse_show_rx_attached (self, lines):
    if len(lines) < 2: return None
    attached = {}
    attachment_re = re.compile("^\s+(((RX)(\d)(.+),\s+(not\s+attached|attached\s+to\s+(\S+)),(.+))|autosize)$")
    for line in lines[1:]:
      m = re.match(attachment_re, line)
      if m == None or m.group(1) == None or m.group(1) == "autosize": continue
      filename = m.group(7)
      if filename == None: filename = ""
      attached[m.group(4)] = filename
    return attached


  #### set_rx_config ####################################################
  
  def set_rx_config (self, to_rx):
    to_rx = to_rx.lower()
    if to_rx == "rx8e": from_rx = "rx28"
    elif to_rx == "rx01":
      to_rx = "rx8e"
      from_rx = "rx28"
    elif to_rx == "rx28": from_rx = "rx8e"
    elif to_rx == "rx02":
      to_rx = "rx28"
      from_rx = "rx8e"
    else:
      print "Cannot set_rx_config for " + to_rx
      return False
      
    if self.verbose: print "Switch rx driver: " + from_rx + ", to: " + to_rx
    lines = self.do_simh_show("rx")

    rx_type = self.parse_show_rx_dev (lines)
    if rx_type == None:
      print "do_rx_change: Trouble parsing \'show rx\' output from simh. Giving up on:"
      self.do_print_lines(lines)
      return False
    elif rx_type == "disabled":
      if self.verbose: print "rx is disabled. Enabling..."
      self.send_cmd("set rx enabled")
      # Retry getting rx info
      lines = self.do_simh_show("rx")
      rx_type = self.parse_show_rx_dev (lines)
      if rx_type == None:
        print "do_rx_change after re-enable: Trouble parsing \`show rx\` output from simh. Giving up on:"
        self.do_print_lines(lines)
        return False
      elif rx_type == "disabled":
        print "do_rx_change re-enable of rx failed. Giving up."
        return False

    if rx_type.lower() == to_rx:
      print "rx device is already set to " + to_rx
      return None
      
    attached_rx= self.parse_show_rx_attached(lines)
    if attached_rx == None:
      print "do_rx_change: Trouble parsing /'show rx\' from simh to find rx attachments. Got:"
      self.do_print_lines(lines)
    else:
      for unit in attached_rx.keys():
        if attached_rx[unit] != "":
          det_comm = "det rx" + unit
          if self.verbose: print det_comm + "(Had: " + attached_rx[unit] + ")"
          self.send_cmd(det_comm)

    self.send_cmd("set rx " + to_rx)

    # Test to confirm new setting of RX
    lines = self.do_simh_show("rx")
    rx_type = self.parse_show_rx_dev (lines)

    if rx_type == None:
      print "Failed change of rx to " + to_rx + ". Parse fail on \'show rx\'."
      return False
    elif rx_type.lower() != to_rx: 
      print "Failed change of rx to " + to_rx + ". Instead got: " + rx_type
      return False
    return True


  #### get_tti ##################################################
  # Returns an ordered list of files attached or None if disabled.
  def parse_show_tti (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^(KSR|7b)$")
    if len(lines) < 2: return None

    # That second line of output contains embedded newlines.
    m = re.match(is_enabled_re, lines[1].strip())
    if m == None or m.group(1) == None: return None
    return m.group(1)



  #### do_tti_change ###################################################

  def set_tti_config (self, to_tti):
    if to_tti == "KSR": from_tti = "7b"
    elif to_tti == "7b": from_tti = "KSR"
    else:
      print "Cannot set_tti_config to " + to_tti
      return
    
    if self.verbose: print "Switch tti driver from: " + from_tti + ", to: " + to_tti

    lines = self.do_simh_show("tti")
    tti_type = self.parse_show_tti (lines)
    if tti_type == None:
      print "do_tti_change: Trouble parsing \'show tti\' output from simh. Giving up on:"
      self.do_print_lines(lines)
      return False
    elif tti_type == to_tti:
      print "tti device is already set to " + to_tti
      return None

    self.send_cmd("set tti " + to_tti)

    # Test to confirm new setting of tti
    lines = self.do_simh_show("tti")
    tti_type = self.parse_show_tti (lines)
    
    if tti_type == None:
      print "Failed change of tti to " + to_tti + ". Parse fail on \'show tti\'."
      return False
    elif tti_type != to_tti: 
      print "Failed change of tti to " + to_tti + ". Instead got: " + tti_type
      return False
    else:
      return True


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Added lib/simh.py.in.




















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# simh/__init__.py - A wrapper class around pexpect for communicating
#   with an instance of the PiDP-8/I SIMH simulator running OS/8.
#
#   See ../doc/class-simh.md for a usage tutorial.
#
# Copyright © 2017 by Jonathan Trites, © 2017-2019 by William 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.
########################################################################

import os
import pexpect
import pkg_resources
import subprocess
import tempfile
import time
import re
import sys

import pidp8i

class simh: 
  # pexpect object instance, set by ctor
  _child = None

  # Constant used by os8_kbd_delay, assembled in stages:
  #
  # 1. PDP-8 RS-232 bits per character: 7-bit ASCII plus necessary
  #    start, stop, and parity bits.
  #
  # 2. The ratio of the instructions per second ratios of a PDP-8/I to
  #    that of the host hardware running the simulator.  The former is
  #    an approximate value; see lib/pidp8i/ips.py.in for the value and
  #    its defense.  The latter is either the IPS rate for:
  #
  #    a) a Raspberry Pi Model B+, that being the slowest host system
  #       we run this simulator on; or
  #
  #    b) the IPS rate of the actual host hardware if you have run the
  #       "bin/teco-pi-demo -b" benchmark on it.
  #
  # 2. The fact that real PDP-8s ran OS/8 reliably at 300 bps, and have
  #    been claimed to get flaky as early as 600 bps by some.  (Others
  #    claim to have run them up to 9,600 bps.)
  #
  # 3. The "safe BPS" value is the fastest bit per second speed actual
  #    PDP-8 hardware was known to run OS/8 terminal I/O at.  In this
  #    case, it is the high-speed tape reader.
  #
  #    TODO: We may be able to increase this.
  #
  #    We have one report that OS/8 was tested with terminals up to
  #    about ~600 bps before becoming unreliable.
  #
  #    We have another report that OS/8 could run under ETOS with
  #    9,600 bps terminals, but we don't know if that tells us anything
  #    about OS/8 running without the ETOS multitasking hardware.
  #
  # 4. Given above, calculate safe characters per second for host HW.
  #
  # 5. Invert to get seconds per character, that being the delay value.
  _bpc = 7 + 1 + 1 + 1                                       # [1]
  _ips_ratio = float (pidp8i.ips.current) / pidp8i.ips.pdp8i # [2]
  _pdp8i_safe_bps = 300                                      # [3]
  _host_safe_cps = _pdp8i_safe_bps * _ips_ratio / _bpc       # [4]
  _os8_kbd_delay = 1 / _host_safe_cps                        # [5]

  # Known OS/8 error strings and a flag indicating whether the error
  # dumps us back out to the OS/8 command monitor or leaves us in the
  # called program.

  _os8_errors = [
    # The date comment tells when each message is observed and validated
    #
    # OS/8 Handbook 1974 page 1-43/81 Keyboard Monitor Error Messages:
    ["MONITOR ERROR 2 AT \d+ \\(DIRECTORY I/O ERROR\\)", True],   # 2018.02.11
    ["MONITOR ERROR 5 AT \d+ \\(I/O ERROR ON SYS\\)", True],
    ["MONITOR ERROR 6 AT \d+ \\(DIRECTORY I/O ERROR\\)", True],
    ["(\S+) NOT AVAILABLE", False],
    ["(\S+) NOT FOUND", False],                                   # 2018.02.11
    # OS/8 Handbook 1974 page 1-51/89 Command Decoder Error Messages
    ["ILLEGAL SYNTAX", False],                                    # 2018.02.11
    ["(\S+) DOES NOT EXIST", False],
    # ["(\S+) NOT FOUND", False],                                 # See above
    ["TOO MANY FILES", False],
    # OS/8 Handbook 1974 page 1-75/113 CCL Error Messages
    ["BAD DEVICE", False],
    ["BAD EXTENSION", False],
    # OS/8 Handbook 1974 page 1-106/144 PIP Error Messages
    ["ARE YOU SURE", False],
    ["BAD DIRECTORY ON DEVICE #\s?\d+", False],
    ["BAD SYSTEM HEAD", False],
    ["CAN'T OPEN OUTPUT FILE", False],
    ["DEVICE #\d+ NOT A DIRECTORY DEVICE", False],
    ["DIRECTORY ERROR", False],
    ["ERROR DELETING FILE", False],
    ["ILLEGIAL BINARY INPUT, FILE #\d+", False],
    ["INPUT ERROR, FILE #\s?\d+", False],
    ["IO ERROR IN \\(file name\\) --CONTINUING", False],
    ["NO ROOM FOR OUTPUT FILE", False],
    ["NO ROOM IN \\(file name\\) --CONTINUING", False],
    ["OUTPUT ERROR", False],
    ["PREMATURE END OF FILE, FILE #\s?\d+", False],
    ["ZERO SYS?", False],
    # OS/8 Handbook 1974 page 2-81/244: DIRECT Error Messages
    ["BAD INPUT DIRECTORY", False],
    ["DEVICE DOES NOT HAVE A DIRECTORY", False],
    ["ERROR CLOSING FILE", False],
    ["ERROR CLOSING FILE", False],
    ["ERROR READING INPUT DIRECTORY", False],
    ["ILLEGAL \\*", False],
    # OS/8 Handbook 1974 page: 2-109/272: FOTP Error Messages
    ["ERROR ON INPUT DEVICE, SKIPPING \\((\S+)\\)", False],
    ["ERROR ON OUTPUT DEVICE, SKIPPING \\((\S+)\\)", False],
    ["ERROR READING INPUT DIRECTORY", False],
    ["ERROR READING OUTPUT DIRECTORY", False],
    ["ILLEGAL \\?", False],
    ["NO FILES OF THE FORM (\S+)", False],
    ["NO ROOM, SKIPPING \\((\S+)\\)", False],
    ["SYSTEM ERROR-CLOSING FILE", False],
    ["USE PIP FOR NON-FILE STRUCTURED DEVICE", False],
    ["LINE TOO LONG IN FILE#\d+", False],
  ]

  # Pattern to match a SIMH command.  The command verb ends up in
  # match().group(1), and anything after the verb in group(3).
  _simh_comm_re = re.compile ("^\s*(\S+)(\s+(.*))?$")

  # Significant prefixes of SIMH command verbs that transition from SIMH
  # command context back into the simulation: BOOT, CONTINUE, and GO.
  # We need only the first letter in all cases, since these particular
  # commands are not ambiguous.  They're uppercase because the code that
  # uses this always uppercases the command before searching this list.
  _enters_os8_context = ["B", "C", "G"]


  #### ctor ############################################################
  # basedir is the parent of bin/{pdp8,pidp8i-sim}.
  #
  # Setting the skip_gpio flag to True causes us to use bin/pdp8 instead
  # of bin/pidp8i-sim, which runs the simulator with the PiDP-8/I GPIO 
  # thread disabled.  You might do this because you don't need any front
  # panel interaction or because you know you're not running on an RPi
  # in the first place.
  
  def __init__ (self, basedir, skip_gpio = False):
    # Start the simulator instance
    sim = 'pdp8' if skip_gpio else 'pidp8i-sim';
    self._child = pexpect.spawn (os.path.join (basedir, 'bin', sim))
    self._valid_pip_options = ["/A", "/B", "/I"]
    self._os8_file_re = re.compile("(\S+):(\S+)?")
    self._os8_error_match_strings = []
    self._os8_fatal_check = []
    self.verbose = False

    # We keep track of what our command context is so our caller does
    # not need to explicitly call back_to_cmd() or sendcontrol ('e').
    # We keep track of the command context and transition automatically.
    self._context = "simh"
    
    # Parse our OS/8 Errors table into actionable chunks
    for error_spec in self._os8_errors:
      self._os8_error_match_strings.append(error_spec[0])
      self._os8_fatal_check.append(error_spec[1])

    self._pip_to_replies = ['\\^']
    self._pip_to_replies.extend(self._os8_error_match_strings)
    # Did command start the command decoder or die with a monitor error?
    self._cd_replies = ['\\*']
    self._cd_replies.extend(self._os8_error_match_strings)

    # Turn off pexpect's default inter-send() delay.  We add our own as
    # necessary.  The conditional tracks an API change between 3 and 4.
    pev4 = (pkg_resources.get_distribution("pexpect").parsed_version >
            pkg_resources.parse_version("4.0"))
    self._child.delaybeforesend = None if pev4 else 0

    # Wait for the simulator's startup message.
    if not self.try_wait ('PDP-8 simulator V.*git commit id: [0-9a-f]', 10):
      raise RuntimeError ('Simulator failed to start')


  #### back_to_cmd ######################################################
  # Pause the simulation and return to the SIMH command prompt when the
  # simulated software emits the given prompt string.  Typically used to
  # wait for OS/8 to finish running a command so we can do something
  # down at the SIMH layer instead.

  def back_to_cmd (self, prompt):
    self._child.expect ("\n%s$" % prompt)
    self.os8_kbd_delay ()
    self._child.sendcontrol ('e')
    self._context = "simh"


  #### os8_get_file ####################################################
  # Rough inverse of os8_send_file.
  #
  # Both paths must be given and are used literally.  (Contrast our
  # inverse, where the destinatinon file name is produced from the
  # source if not given.)
  #
  # When this function is called to pull a file sent by our inverse, the
  # conversion should be lossless except for the transforms done by our
  # underlying utility tools, such as the LF -> CR+LF done by txt2ptp
  # but not undone by ptp2txt.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_get_file (self, intname, extname):
    # Attach a blank paper tape to the simulator.
    ptf = tempfile.NamedTemporaryFile (suffix = '.pt', delete = False)
    ptf.close ()
    ptn = ptf.name
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach ptp ' + ptn)

    # Punch internal file to external paper tape image
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'PUNCH ' + intname);
    self.back_to_cmd ('\\.')        # wait for transfer to finish

    # Convert text file from SIMH paper tape format
    tool = os.path.join (pidp8i.dirs.build, 'bin', 'ptp2txt')
    self.send_cmd ('detach ptp')
    subprocess.call (tool + ' < ' +  ptn + ' > ' + extname, shell = True)

    # Return to OS/8, just because that's where we were on entry, so we
    # should not change that.
    self.os8_restart ()


  #### os8_kbd_delay ###################################################
  # Artificially delay the media generation process to account for the
  # fact that OS/8 lacks a modern multi-character keyboard input buffer.
  # It is unsafe to send text faster than a contemporary terminal could,
  # though we can scale it based on how much faster this host is than a
  # real PDP-8.  See the constants above for the calculation.

  def os8_kbd_delay (self):
    time.sleep (self._os8_kbd_delay)


  #### os8_send_cmd ####################################################
  # Wait for an OS/8 command prompt running within SIMH, then send the
  # given line.
  #
  # The default timeout may seem excessive, but it is based on hard
  # experience: when SIMH is running on a slow host with slow devices
  # (e.g. the byte-by-byte transfer of the TD8E tape controller) a
  # single OS/8 command can take a very long time if it requires a lot
  # of I/O.  If you are calling this for a command that you know for a
  # fact takes less time on all hosts and with all practical device
  # configurations, we encourage you to pass a smaller value.
  #
  # The caller must pass a prompt string because OS/8 has several
  # different prompt types: ., *, $, and #, at least.  Beware in passing
  # these that they're treated as regular expressions, so characters
  # special in Python REs must be escaped.  And then since the RE escape
  # character (\) is also special in Python strings, you must double-
  # escape *it*.  So, '\\$' is a reasonable thing to pass as the prompt
  # value, meaning "look for a literal $ character."
  #
  # This routine requires the caller to ensure that the system is in
  # OS/8 Keyboard Monitor context — that is, ready for another OS/8
  # command — before calling it.  While this routine is able to check
  # whether we're in OS/8 context as a prerequisite, it is not practical
  # for us to return the system to OS/8 context automatically from some
  # other context because that would require us to know the current
  # context in detail, but only the caller has that full knowledge.
  # 
  # Part of the problem is that in order to synchronize this object's
  # internal state machine with the SIMH + OS/8 + running program state,
  # we have to somehow provoke a prompt character from the running
  # program.  How do we do that without knowing the current context?
  # In some contexts, a CR or LF will do it, in others BS, and in others
  # it'll take Ctrl-C.  Then you have a new problem, with is that those
  # same characters aren't harmless: they'll change the very context
  # we're trying to probe!  For instance, a Ctrl-C sent to the OS/8
  # Keyboard Monitor just results in another prompt, but a Ctrl-C sent
  # to a program running *under* OS/8 might kill it.  Or it might be
  # treated as input.  Or it might be ignored entirely.
  #
  # There is no magic sequence we can send to SIMH or OS/8 to return the
  # system to OS/8 Keyboard Monitor context without either changing the
  # context in some way that might break the caller's needed state (e.g.
  # Ctrl-E, go 7600) or lose data (e.g. Ctrl-C) or fail entirely (e.g.
  # Enter.)  It's up to the caller to arrange this.

  def os8_send_cmd (self, prompt, line, debug=False, timeout=60):
    if self._context != 'os8': 
      print("OS/8 is not running. Cannot execute: " + lin)
      return
    if debug:
      print("os8_send_cmd: expecting: " + prompt)
      print("\tLast match before: {" + self._child.before.decode() + "}")
      print("\tLast match after: {" + self._child.after.decode() + "}")
    self._child.expect ("\n%s$" % prompt, timeout = timeout)
    self.os8_send_line (line)


  #### os8_send_ctrl ###################################################
  # Send a control character to OS/8 corresponding to the ASCII letter
  # given.  We precede it with the OS/8 keyboard delay, since we're
  # probably following a call to os8_send_line or os8_send_cmd.

  def os8_send_ctrl (self, char):
    cc = char[0].lower ()
    self.os8_kbd_delay ()
    self._child.sendcontrol (cc)
    
    if cc == 'e': self._context = 'simh'


  #### mk_os8_name # ###################################################
  # Create an OS/8 filename: of the form XXXXXX.YY
  # From a POSIX path.

  def mk_os8_name(self, dev, path):
    bns = os.path.basename (path)
    bns = re.sub("-|:|\(|\)|!", "", bns)
    bns = bns.upper()
    if "." not in bns:
      return dev + bns[:min(6, len(bns))]
    else:
      dot = bns.index('.')
      return dev + bns[:min(6, dot, len(bns))] + "." + bns[dot+1: dot+3]


  #### os8_send_file ###################################################
  # Send a copy of a local text file to OS/8.  The local path may
  # contain directory components, but the remote must not, of course.
  #
  # If the destination file name is not uppercase, it will be so forced.
  #
  # If the destination file name is not given, it is taken as the
  # basename of the source file name.
  #
  # The file is sent via the SIMH paper tape device through PIP in its
  # default ASCII mode, rather than character by character for two reasons:
  #
  # 1. It's faster.  It runs as fast as the simulator can process the
  #    I/O instructions, without any os8_kbd_delay() hooey.
  #
  # 2. It allows lowercase input regardless of the way the simulator is
  #    configured.  ASCII is ASCII.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_send_file (self, source, dest = None):
    # Create path and file names not given
    bns = os.path.basename (source)
    if dest == None: dest = bns
    dest = dest.upper ()

    # Convert text file to SIMH paper tape format
    bdir = pidp8i.dirs.build
    pt   = os.path.join (bdir, 'obj', bns + '.pt')
    tool = os.path.join (bdir, 'bin', 'txt2ptp')
    subprocess.call (tool + ' < ' + source + ' > ' + pt, shell = True)

    # Paper tape created, so attach it read-only and copy it in.  We're
    # relying on txt2ptp to insert the Ctrl-Z EOF marker at the end of
    # the file.
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach -r ptr ' + pt)
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'R PIP')
    self.os8_send_cmd ('\\*', dest + '<PTR:')
    self._child.expect ('\\^')
    self.os8_send_ctrl ('[')      # finish transfer
    self._child.expect ('\\*')
    self.os8_send_ctrl ('[')      # exit PIP


  #### pip_error_handler ###############################################
  # Common error handler for os8_pip_to and os8_pip_from

  def pip_error_handler(self, caller, reply):
    print("PIP error from inside " + caller + ": ")
    print("\t" + self._child.before.decode().strip())
    print("\t" + self._child.after.decode().strip())
    
    # Was this error fatal or do we need to clean up?
    # Remember we subtract 1 from reply to get index into error tables.
    if not self._os8_fatal_check[reply - 1]:
      # Non fatal error.  Exit pip to the monitor
      self.os8_send_ctrl ('[')      # exit PIP


  #### os8_pip_to ###################################################
  # Send a copy of a local file to OS/8 using PIP.
  #
  # The file is sent via the SIMH paper tape device through PIP
  # specifying a transfer option.  If no option is specified,
  # ASCII is assumed.
  #
  # In ASCII mode, we pre-process with txt2ptp which translates
  # POSIX ASCII conventions to OS/8 conventions.  In all other
  # modes, we do not do any translation.
  #
  # However, we should supply a sacrificial NULL as an additional character
  # because the OS/8 PTR driver throws the last character away. (NOT DONE YET)
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_pip_to (self, path, os8name, option = None):
    if option == None: option = ""
    # If os8name is just a device, synthesize an upcased name from
    # the POSIX file basename.
    if not os.path.exists(path):
      print(path + " not found. Skipping.")
      return
    m = re.match(self._os8_file_re, os8name)
    if m != None and (m.group(2) == None or m.group(2) == ""):
        dest = self.mk_os8_name(os8name, path)
    else:
        dest = os8name

    did_conversion = False
    if option == "" or option == "/A":
      # Convert text file to SIMH paper tape format in current dir of path.
      if self.verbose: print("Format converting " + path)
      bdir = pidp8i.dirs.build
      # Create uniquified temp path name.
      pt   = path + "-" + str(os.getpid()) + ".pt_temp"
      tool = os.path.join (bdir, 'bin', 'txt2ptp')
      subprocess.call (tool + ' < ' + path + ' > ' + pt, shell = True)
      did_conversion = True
    elif option not in self._valid_pip_options:
      print("Invalid PIP option: " + option + ". Ignoring: " + path + \
          " to OS/8.")
      return
    else:
      pt = path

    # TODO: Sacrificial extra character code goes here.

    # Paper tape created, so attach it read-only and copy it in.  We're
    # relying on txt2ptp to insert the Ctrl-Z EOF marker at the end of
    # the file.
    self.back_to_cmd ('\\.')
    self.send_cmd ('attach -r ptr ' + pt)
    self.os8_restart ()
    self.os8_send_cmd ('\\.', 'R PIP')
    # Was the start of PIP successful, or did we get a Monitor error?
    reply = self._child.expect (self._cd_replies)
    if reply != 0:
      self.pip_error_handler ("os8_pip_to", reply)
      return

    # Has the read-in been successful?
    self.os8_send_line (dest + '<PTR:' + option)
    reply = self._child.expect (self._pip_to_replies)
    if reply !=0:
      self.pip_error_handler("os8_pip_to", reply)
      if did_conversion:
        os.remove(pt)
      return

    self.os8_send_ctrl ('[')      # finish transfer
    self._child.expect ('\\*')
    self.os8_send_ctrl ('[')      # exit PIP
    # We could detach ptr and restart OS/8 here, but we don't need to.
    # Do remove the temp file if we created one.
    if did_conversion:
      os.remove (pt)


  #### os8_pip_from ###################################################
  # Fetch a file from OS/8 to a local path using PIP.
  #
  # The OS/8 source filename is synthesized from the basename of the path,
  # upcasing if necessary.
  #
  # The file is sent via the SIMH paper tape device through PIP
  # specifying a transfer option.  If no option is specified,
  # ASCII is assumed.
  #
  # In ASCII mode, we post-process with ptp2txt which translates
  # POSIX ASCII conventions to OS/8 conventions.  In all other
  # modes, we do not do any translation.
  #
  # Entry context should be inside OS/8.  Exit context is inside OS/8.

  def os8_pip_from (self, os8name, path, option = None):
    if option == None: option = ""
    # If path is not a file, use the name portion of os8name.
    if os.path.isdir(path):
      colon = os8name.find(':')
      if colon == -1:                # No dev, just a name.
        path = path + "/" + os8name
      else:
        path = path + "/" + os8name[colon+1:]

    if option != "" and option not in self._valid_pip_options:
      print("Invalid PIP option: " + option + \
          ". Ignoring os8_pip_from on: " + path)
      return

    self.back_to_cmd ('\\.')
    self.send_cmd ('attach ptp ' + path)
    self.os8_restart ()
    
    self.os8_send_cmd ('\\.', 'R PIP')
    # Was the start of PIP successful or did we get a Monitor error?
    reply = self._child.expect (self._cd_replies)
    if reply != 0:
      self.pip_error_handler ("os8_pip_from", reply)
      return
    
    self.os8_send_line ('PTP:<' + os8name + option)

    reply = self._child.expect (self._cd_replies)
    if reply !=0:
      self.pip_error_handler ("os8_pip_from", reply)
      # There is an empty PTP file we need to remove.
      os.remove(path)
      return

    self.os8_send_ctrl ('[')      # exit PIP
    self.back_to_cmd ('\\.')
    self.send_cmd ('detach ptp')  # Clean flush of buffers.
    self.os8_restart ()

    if option == "" or option == "/A":
      if self.verbose: print("Format converting " + path)
      # Convert text file to SIMH paper tape format
      bdir = pidp8i.dirs.build
      # Create uniquified temp path name.
      pf = path + "-" + str(os.getpid()) + ".pf_temp"
      os.rename(path, pf)
      tool = os.path.join (bdir, 'bin', 'ptp2txt')
      subprocess.call (tool + ' < ' + pf + ' > ' + path, shell = True)
      os.remove(pf)


  #### os8_send_line ###################################################
  # Core of os8_send_cmd.  Also used by code that needs to send text
  # "blind" to OS/8, without expecting a prompt, as when driving EDIT.

  def os8_send_line (self, line):
    self.os8_send_str (line)
    self._child.send ("\r")


  #### os8_send_str ########################################################
  # Core of os8_send_line.  Also used by code that needs to send text
  # "blind" to OS/8, without expecting a prompt and without a CR, as
  # when driving TECO.

  def os8_send_str (self, str):
    for i in range (0, len (str)):
      self._child.send (str[i])
      self.os8_kbd_delay ()


  #### os8_resume #######################################################
  # Resume OS/8.
  #
  # It would be nice if we could just send  the "cont" command
  # and have python expect and OS/8 synch right up.
  # But so far we have not figured out how to do that.
  # To resume OS/8 from SIMH we need to provoke a prompt.
  # Typing a rubout or ^U at a SIMH terminal session does this.
  # But not when SIMH is run under python expect.
  # We don't know why.
  #
  # boot works
  # go 7600 works
  # ^C <pause> \n\r works.
  #
  # The resume command uses the ^C method as the least disruptive
  # to system state.

  def os8_resume (self):
    if self._context == "os8": return   # Already running.
    
    self.send_cmd("cont")   # sets os8 context for us.

    # Now provoke a keyboard monitor prompt.
    self.os8_send_ctrl('c')
    self.os8_kbd_delay()
    self.os8_send_str('\r\n')
  

  #### os8_restart #######################################################
  # Called while in the SIMH command prompt, this restarts OS/8.
  #
  # This one-line function exists to abstract the method we use and to
  # document the reason we do it this way.
  #
  # Currently we do this by calling the OS/8 command entry point, which
  # has the virtue that it forces another . prompt, which keeps the
  # send/expect sequencing simple when switching between OS/8 and SIMH
  # command modes.
  #
  # That is why we don't use "cont" here instead: it requires that the
  # caller always be aware of when the . prompt went out; complicated.
  #
  # Another simple alternative is "boot rk0", which actually benchmarks
  # a smidge faster here.  We choose this method instead because we
  # expect that some of our callers will want to do several different
  # things in a single OS/8 session, which rebooting would screw up.

  def os8_restart (self):
    self.send_cmd ("go 7600")


  #### os8_squish ########################################################
  # Wraps the OS/8 SQUISH command for a given device.

  def os8_squish (self, device):
    self.os8_send_cmd ('\\.', "SQUISH " + device + ":")
    self.os8_kbd_delay ()
    self._child.send ("Y\r");


  #### os8_zero_core ###################################################
  # Starting from OS/8 context, bounce out to SIMH context and zero all
  # of core excepting:
  #
  # 0. zero page - many apps put temporary data here
  # 1. the top pages of fields 1 & 2 - OS/8 is resident here
  # 2. the top page of field 2 - OS/8's TD8E driver (if any) lives here
  #
  # We then restart OS/8, which means we absolutely need to do #1 and
  # may need to do #2.  We could probably get away with zeroing page 0.
  #
  # All of the above explains why we have this special OS/8 alternative
  # to the zero_core() method.

  def os8_zero_core (self):
    self.back_to_cmd ('\\.')
    self.send_cmd ('de 00200-07577 0')
    self.send_cmd ('de 10000-17577 0')
    self.send_cmd ('de 20000-27577 0')
    self.send_cmd ('de 30000-77777 0')
    self.os8_restart ()


  #### quit ############################################################
  # Quits the simulator and waits for it to exit

  def quit (self):
    self.send_cmd ("q")
    self._child.expect (pexpect.EOF)


  #### read_tail #######################################################
  # Watch for a literal string, then get what follows on that line.

  def read_tail (self, head, timeout = -1):
    self._child.expect_exact ([head], timeout)
    return self._child.readline ()


  #### send_cmd ########################################################
  # Wait for a SIMH command prompt and then send the given command.
  # If we are not in the simh context send ^e and set context "simh".
  # If we are not in simh context, send ^e set context "simh"
  #    and hope for the best.
  # If we issue a command that enters os8 context, set context "os8".
  # Note exiting out of OS/8 into the SIMH context is a bit of a
  # trap door. Resynchronizing with python expect requires provoking
  # a prompt, and prompts are context specific.
  # Perhaps we should require separate and explicit commands to
  # escape to SIMH. But for now, just be careful to use os8_resume
  # after calling send_cmd.
  
  def send_cmd (self, cmd):
    if self._context == "os8":
      self._child.expect ("\n\\.$")
      self._child.sendcontrol ('e')
      self._context = "simh"
    elif self._context != "simh":
      self._child.sendcontrol ('e')
      self._context = "simh"
      
    self._child.expect ("sim> $")
    self._child.sendline (cmd)
    m = re.match (self._simh_comm_re, cmd)
    if m != None and m.group(1)[:1].upper() in self._enters_os8_context:
      self._context = "os8"


  #### send_line #######################################################
  # Sends the given line "blind", without waiting for a prompt.

  def send_line (self, line):
    self._child.sendline (line)


  #### set_logfile #####################################################

  def set_logfile (self, lf):
    self._child.logfile = lf


  #### spin ############################################################
  # Let child run without asking anything more from it, with an optional
  # timeout value.  If no value is given, lets child run indefinitely.

  def spin (self, timeout = None):
    self._child.expect (pexpect.EOF, timeout = timeout)


  #### try_wait ########################################################
  # A wrapper around self._child.expect which catches exceptions and
  # returns false on pexpect timeout.  If you pass a list instead of a
  # string, it also returns true if the match wasn't for the first
  # element, so you can pass [success, failure1, failure2, etc.] to
  # check for a known-success case and one or more failure cases.

  def try_wait (self, matches, timeout = -1):
    try:
      return self._child.expect (matches, timeout = timeout) == 0
    except pexpect.exceptions.TIMEOUT:
      sys.stderr.write ("Exceeded " + str(timeout) + " sec timeout " +
          "waiting for " + str(matches) + "\n")
      return False
    except:
      sys.stderr.write ("Failed to match " + str(matches) + 
          ": unknown exception.\n")
      return False


  #### zero_core #######################################################
  # From SIMH context, zero the entire contents of core, which is
  # assumed to be 32 kWords.
  #
  # SIMH's PDP-8 simulator doesn't start with core zeroed, on purpose,
  # because the actual hardware did not do that.  SIMH does not attempt
  # to simulate the persistence of core memory by saving it to disk
  # between runs, but the SIMH developers are right to refuse to do this
  # by default: you cannot trust the prior state of a PDP-8's core
  # memory before initializing it yourself.
  #
  # See os8_zero_core () for a less heavy-handed alternative for use
  # when running under OS/8.

  def zero_core (self):
    self.send_cmd ('de all 0')
  

  #### describe_dev_config #############################################
  # We provide an interface to alter SIMH device configurations for
  # specific parameters and specific devices
  #
  # dev configs supported:  rx, tti, tape
  #
  # rx:     RX8E, RX28   RX8E is the simh name for RX01 support.
  #                      RX28 is the simh name for RX02 support.
  # tti:    KSR, 7b      7b is full keyboard support.
  #                      KSR forces upcase of lower case keys on input.
  # tape:   td, dt       td is the TD8E DECtape device
  #                      dt is the TC08 DECtape device

  def describe_dev_config (self, name):
    if name == "tape":
      lines = self.do_simh_show("dt")
      dev_status = self.parse_show_tape_dev(lines)

      if dev_status == "dt": return "dt"
      else:
        lines = self.do_simh_show("td")
        return self.parse_show_tape_dev(lines)
    elif name == "rx":
      lines = self.do_simh_show("rx")
      return self.parse_show_rx_dev(lines)
    elif name == "tti":
      lines = self.do_simh_show("tti")
      return self.parse_show_tti(lines)
    else: return None

      
  #### do_simh_show  ###################################################
  # Calls show on the device name.
  # Returns array of lines from output.

  def do_simh_show (self, name):
    supported_shows = ["dt", "td", "tti", "rx"]
    if name not in supported_shows: return None
    
    ucname = name.upper()
    self.send_cmd("show " + name)
    self._child.expect(ucname + "\s+(.+)\r")
    lines = self._child.after.decode().split ("\r")
    return lines


  #### parse_show_tape_dev  ############################################
  # Returns current state of DECtape support.
  # One of: disabled, td, dt, or None if parse fails.

  def parse_show_tape_dev (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^(TD|DT)\s+(disabled|(devno=\S+,\s(\d)\s+units))$")
    m = re.match(is_enabled_re, lines[0])

    if m == None or m.group(1) == None or m.group(2) == None: return None
    if m.group(2) == "disabled": return "disabled"
    elif m.group(1) == "TD": return "td"
    elif m.group(1) == "DT": return "dt"
    else: return None


  #### parse_show_tape_attached  ########################################
  # Returns an ordered list of files attached or None if disabled.
  def parse_show_tape_attached (self, lines):
    if lines == None: return None
    if len(lines) < 2: return None
    attached = {}
    attachment_re = re.compile("^\s+(((DT|TD)(\d)(.+),\s+(not\s+attached|attached\s+to\s+(\S+)),(.+))|12b)$")
    for line in lines[1:]:
      m = re.match(attachment_re, line)
      if m == None or m.group(1) == None or m.group(1) == "12b": continue
      filename = m.group(7)
      if filename == None: filename = ""
      attached[m.group(4)] = filename
    return attached

  #### do_print_lines ###################################################
  # Debugging aid. Prints what we parsed out of _child.after.
  def do_print_lines (self, lines):
    for line in lines:
      print(line)

  #### simh_configure routines #########################################
  # These routines affect the state of device configuration in SIMH.
  # They are intended as robust ways to toggle between incompatible
  # configurations of SIMH:
  # Choice of TD8E or TC08 DECtape (SIMH td and dt devices).
  # Choice of RX01 or RX02 Floppy emulation.
  # The SIMH rx device sets RX8E for RX01, and RX28 for RX02.  
  # Choice of KSR or 7bit console configuration.
  #
  # When re-configuring dt, dt, and rx devices, any attached
  # images are detached before reconfiguration is attempted.
  # (SIMH errors out if you don't detach them.)
  #
  # The check to see if the change is unnecessary.
  # For now they return None if no change necessary.
  #
  # After re-configuring the device, the SIMH show command is used
  # to confirm the re-configuration was successful.
  #
  # In future, we should add exception handling for no change necessary.
  # For now, return True if the change was successful and False if not.


  def set_tape_config (self, to_tape):
    if to_tape == "dt": from_tape = "td"
    elif to_tape == "td": from_tape = "dt"
    else:
      print("Cannot set_tape_config for " + to_tape)
      return False

    if self.verbose:
      print("Disable: " + from_tape + ", and enable: " + to_tape)
    
    lines = self.do_simh_show(from_tape)
    from_status = self.parse_show_tape_dev(lines)

    if from_status == None:
      print("do_tape_change: Trouble parsing \'show " + from_tape + \
          "\' output from simh. Giving up on:")
      self.do_print_lines (lines)
      return False

    if from_status == "disabled":
      print(from_tape + " already is disabled.")
    else:
      attached_from = self.parse_show_tape_attached(lines)
      if attached_from == None:
        print("do_tape_change: Trouble parsing \'show " + from_tape + \
            "\' output from simh. Giving up on:")
        self.do_print_lines (lines)
        return False
      else:
        for unit in attached_from.keys():
          if attached_from[unit] != "":
            det_comm = "det " + from_tape + unit
            if self.verbose:
              print(det_comm + "(Had: " + attached_from[unit] + ")")
            self.send_cmd(det_comm)
        self.send_cmd("set " + from_tape + " disabled")

    lines = self.do_simh_show(to_tape)
    to_status = self.parse_show_tape_dev(lines)

    if to_status == None:
      print("do_tape_change: Trouble parsing \'show " + to_tape + \
          "\' output from simh. Giving up on:")
      self.do_print_lines (lines)
      return False
    elif to_status != "disabled":
      print(to_tape + " already is enabled.")
    else:
      self.send_cmd("set " + to_tape + " enabled")    

    # Test to confirm to_tape is now enabled.

    lines = self.do_simh_show(to_tape)
    to_status = self.parse_show_tape_dev(lines)

    if to_status == None:
      print("Failed enable of " + to_tape + \
          ". Parse fail on \'show " + to_tape + "\'. Got:")
      self.do_print_lines (lines)
      return False
    elif to_status == "disabled":
      print("Failed enable of " + to_tape + ". Device still disabled.")
      return False
    else: 
      return True


  #### parse_show_rx_dev ###############################################
  # Show the rx device configuration.

  def parse_show_rx_dev (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^\s*(RX)\s+(disabled|((RX8E|RX28),\s+devno=\S+,\s+(\d)\s+units))$")
    m = re.match(is_enabled_re, lines[0])
    if m == None or m.group(2) == None: return None
    if m.group(2) == "disabled": return "disabled"
    return m.group(4)


   #### parse_show_rx_attached ##########################################
  # Returns an ordered list of files attached or None if disabled.

  def parse_show_rx_attached (self, lines):
    if len(lines) < 2: return None
    attached = {}
    attachment_re = re.compile("^\s+(((RX)(\d)(.+),\s+(not\s+attached|attached\s+to\s+(\S+)),(.+))|autosize)$")
    for line in lines[1:]:
      m = re.match(attachment_re, line)
      if m == None or m.group(1) == None or m.group(1) == "autosize": continue
      filename = m.group(7)
      if filename == None: filename = ""
      attached[m.group(4)] = filename
    return attached


  #### set_rx_config ####################################################
  
  def set_rx_config (self, to_rx):
    to_rx = to_rx.lower()
    if to_rx == "rx8e": from_rx = "rx28"
    elif to_rx == "rx01":
      to_rx = "rx8e"
      from_rx = "rx28"
    elif to_rx == "rx28": from_rx = "rx8e"
    elif to_rx == "rx02":
      to_rx = "rx28"
      from_rx = "rx8e"
    else:
      print("Cannot set_rx_config for " + to_rx)
      return False
      
    if self.verbose:
      print("Switch rx driver: " + from_rx + ", to: " + to_rx)
    lines = self.do_simh_show("rx")

    rx_type = self.parse_show_rx_dev (lines)
    if rx_type == None:
      print("do_rx_change: Trouble parsing \'show rx\' output from simh.  Giving up on:")
      self.do_print_lines(lines)
      return False
    elif rx_type == "disabled":
      if self.verbose: print("rx is disabled. Enabling...")
      self.send_cmd("set rx enabled")
      # Retry getting rx info
      lines = self.do_simh_show("rx")
      rx_type = self.parse_show_rx_dev (lines)
      if rx_type == None:
        print("do_rx_change after re-enable: Trouble parsing \`show rx\` output from simh. Giving up on:")
        self.do_print_lines(lines)
        return False
      elif rx_type == "disabled":
        print("do_rx_change re-enable of rx failed. Giving up.")
        return False

    if rx_type.lower() == to_rx:
      print("rx device is already set to " + to_rx)
      return None
      
    attached_rx= self.parse_show_rx_attached(lines)
    if attached_rx == None:
      print("do_rx_change: Trouble parsing /'show rx\' from simh to find rx attachments. Got:")
      self.do_print_lines(lines)
    else:
      for unit in attached_rx.keys():
        if attached_rx[unit] != "":
          det_comm = "det rx" + unit
          if self.verbose:
            print(det_comm + "(Had: " + attached_rx[unit] + ")")
          self.send_cmd(det_comm)

    self.send_cmd("set rx " + to_rx)

    # Test to confirm new setting of RX
    lines = self.do_simh_show("rx")
    rx_type = self.parse_show_rx_dev (lines)

    if rx_type == None:
      print("Failed change of rx to " + to_rx + \
          ". Parse fail on \'show rx\'.")
      return False
    elif rx_type.lower() != to_rx: 
      print("Failed change of rx to " + to_rx + ". Instead got: " + \
          rx_type)
      return False
    return True


  #### get_tti ##################################################
  # Returns an ordered list of files attached or None if disabled.
  def parse_show_tti (self, lines):
    if lines == None: return None
    is_enabled_re = re.compile("^(KSR|7b)$")
    if len(lines) < 2: return None

    # That second line of output contains embedded newlines.
    m = re.match(is_enabled_re, lines[1].strip())
    if m == None or m.group(1) == None: return None
    return m.group(1)



  #### do_tti_change ###################################################

  def set_tti_config (self, to_tti):
    if to_tti == "KSR": from_tti = "7b"
    elif to_tti == "7b": from_tti = "KSR"
    else:
      print("Cannot set_tti_config to " + to_tti)
      return
    
    if self.verbose:
      print("Switch tti driver from: " + from_tti + ", to: " + to_tti)

    lines = self.do_simh_show("tti")
    tti_type = self.parse_show_tti (lines)
    if tti_type == None:
      print("do_tti_change: Trouble parsing \'show tti\' output from simh. Giving up on:")
      self.do_print_lines(lines)
      return False
    elif tti_type == to_tti:
      print("tti device is already set to " + to_tti)
      return None

    self.send_cmd("set tti " + to_tti)

    # Test to confirm new setting of tti
    lines = self.do_simh_show("tti")
    tti_type = self.parse_show_tti (lines)
    
    if tti_type == None:
      print("Failed change of tti to " + to_tti + \
          ". Parse fail on \'show tti\'.")
      return False
    elif tti_type != to_tti: 
      print("Failed change of tti to " + to_tti + ". Instead got: " + \
          tti_type)
      return False
    else:
      return True


Deleted media/etos/etosv5b-demo.rk05.
Deleted tools/test-os8-send-file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
# test-os8-send-file - Repeatedly sends random files through class simh
#   method os8_send_file() and pulls it back through os8_get_file(),
#   then checks that the file is unchanged.
#
# Copyright © 2017 by 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 sys
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 core modules we need
import filecmp
import os.path
import random
import tempfile


#### gen_file ##########################################################
# Generate a random text file.  In order that the process be lossless
# through the txt2ptp/ptp2txt filters and the SIMH + OS/8 terminal
# handling, we use only printable ASCII plus CR+LF characters.  Returns
# the name of the generated file.

def gen_file ():
    f = tempfile.NamedTemporaryFile (delete = False, suffix = '.tmp')
    for i in range (0, random.randint (10, 4000)):
      if random.randint (0, 10) != 0:
        # Normal case: write some number of printable ASCII characters
        # on this line.
        for j in range (0, random.randint (1, 79)):
          f.write (chr (random.randint (32, 126)))
      # else: Every now and then, just write a blank line

      f.write ('\r\n')

    f.close ()
    return f.name


#### main ##############################################################

def main ():
  # Create the SIMH child instance and tell it where to send log output
  try:
    s = simh (dirs.build)
  except (RuntimeError) as e:
    print "Could not start simulator: " + e.message + '!'
    exit (1)
  s.set_logfile (os.fdopen (sys.stdout.fileno (), 'w', 0))

  # Find and boot the built OS/8 bin disk
  rk = os.path.join (dirs.os8mo, 'v3d.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  # Setup
  random.seed ()

  # Transfer several random files through.  Beware increasing the range
  # too far: max is 99999 due to the file name length limit of OS/8 due
  # to the temporary file naming scheme we use in the loop.
  for i in range (0, 1000):
    # Build another temp file
    ifn = gen_file ()
    of = tempfile.NamedTemporaryFile (suffix = '.out', delete = False)
    of.close ()

    # Send it
    ofn = of.name
    tfn = 'T%05d.TX' % i
    s.os8_send_file (ifn, tfn)
    s.os8_get_file (tfn, ofn)

    # Did it change?
    if filecmp.cmp (ifn, ofn):
      print ifn + ' transferred successfully.'
      s.os8_send_cmd ('\\.', 'DEL ' + tfn)
      os.remove (ifn)
      os.remove (ofn)
    elif os.path.getsize (ofn) == 0:
      print "\nDifferences found: output is empty!\n"
    else:
      print "\nDifferences found:\n--------------------------------"
      os.system ('diff -wu "' + ifn + '" "' + ofn + '"')
      print 'Left ' + tfn + ' inside OS/8.'


if __name__ == "__main__":
    main()
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































Added tools/test-os8-send-file.in.






























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env @PYCMD@
# -*- coding: utf-8 -*-
########################################################################
# test-os8-send-file - Repeatedly sends random files through class simh
#   method os8_send_file() and pulls it back through os8_get_file(),
#   then checks that the file is unchanged.
#
# Copyright © 2017-2019 by 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 sys
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 core modules we need
import filecmp
import os.path
import random
import tempfile


#### gen_file ##########################################################
# Generate a random text file.  In order that the process be lossless
# through the txt2ptp/ptp2txt filters and the SIMH + OS/8 terminal
# handling, we use only printable ASCII plus CR+LF characters.  Returns
# the name of the generated file.

def gen_file ():
    f = tempfile.NamedTemporaryFile (delete = False, suffix = '.tmp')
    for i in range (0, random.randint (10, 4000)):
      if random.randint (0, 10) != 0:
        # Normal case: write some number of printable ASCII characters
        # on this line.
        for j in range (0, random.randint (1, 79)):
          f.write (chr (random.randint (32, 126)))
      # else: Every now and then, just write a blank line

      f.write ('\r\n')

    f.close ()
    return f.name


#### main ##############################################################

def main ():
  # Create the SIMH child instance and tell it where to send log output
  try:
    s = simh (dirs.build)
  except (RuntimeError) as e:
    print "Could not start simulator: " + e.message + '!'
    exit (1)
  s.set_logfile (os.fdopen (sys.stdout.fileno (), 'wb', 0))

  # Find and boot the built OS/8 bin disk
  rk = os.path.join (dirs.os8mo, 'v3d.rk05')
  if not os.path.isfile (rk):
    print "Could not find " + rk + "; OS/8 media not yet built?"
    exit (1)
  print "Booting " + rk + "..."
  s.send_cmd ("att rk0 " + rk)
  s.send_cmd ("boot rk0")

  # Setup
  random.seed ()

  # Transfer several random files through.  Beware increasing the range
  # too far: max is 99999 due to the file name length limit of OS/8 due
  # to the temporary file naming scheme we use in the loop.
  for i in range (0, 1000):
    # Build another temp file
    ifn = gen_file ()
    of = tempfile.NamedTemporaryFile (suffix = '.out', delete = False)
    of.close ()

    # Send it
    ofn = of.name
    tfn = 'T%05d.TX' % i
    s.os8_send_file (ifn, tfn)
    s.os8_get_file (tfn, ofn)

    # Did it change?
    if filecmp.cmp (ifn, ofn):
      print ifn + ' transferred successfully.'
      s.os8_send_cmd ('\\.', 'DEL ' + tfn)
      os.remove (ifn)
      os.remove (ofn)
    elif os.path.getsize (ofn) == 0:
      print "\nDifferences found: output is empty!\n"
    else:
      print "\nDifferences found:\n--------------------------------"
      os.system ('diff -wu "' + ifn + '" "' + ofn + '"')
      print 'Left ' + tfn + ' inside OS/8.'


if __name__ == "__main__":
    main()