mirror of
https://github.com/RasppleII/a2server.git
synced 2025-01-11 11:31:21 +00:00
390 lines
14 KiB
Python
Executable File
390 lines
14 KiB
Python
Executable File
#! /usr/bin/env python
|
|
# vim: set tabstop=4 shiftwidth=4 expandtab filetype=py:
|
|
|
|
import os, sys, subprocess
|
|
import tempfile
|
|
import hashlib
|
|
import shutil
|
|
import xml.etree.cElementTree as ET
|
|
|
|
gsosDir = '/media/A2SHARED/FILES'
|
|
imagesDir = gsosDir + '/GSOS.INSTALLER/IMAGES'
|
|
imageToolsDir = gsosDir + '/GSOS.INSTALLER/IMAGE.TOOLS'
|
|
netInstallDir = gsosDir + '/GSOS.INSTALLER/NET.INSTALL'
|
|
|
|
p8Dir = '/media/A2SHARED/A2FILES'
|
|
diskToolsP8Dir = p8Dir + '/DISK.TOOLS.P8'
|
|
|
|
commDir = '/media/A2SHARED/A2FILES/COMM'
|
|
spectrumDir = commDir + '/SPECTRUM'
|
|
protermDir = commDir + '/PROTERM'
|
|
zlinkDir = commDir + '/Z.LINK'
|
|
adtproDir = commDir + '/ADTPRO'
|
|
|
|
disk7_sources = [
|
|
{
|
|
'type' : 'sea.bin',
|
|
'url' : 'http://download.info.apple.com/Apple_Support_Area/Apple_Software_Updates/English-North_American/Apple_II/Apple_IIGS_System_6.0.1/Disk_7_of_7-Apple_II_Setup.sea.bin',
|
|
'file' : 'Disk_7_of_7-Apple_II_Setup.sea.bin'
|
|
},
|
|
{
|
|
'type' : 'sea.bin',
|
|
'url' : 'http://archive.org/download/download.info.apple.com.2012.11/download.info.apple.com.2012.11.zip/download.info.apple.com%2FApple_Support_Area%2FApple_Software_Updates%2FEnglish-North_American%2FApple_II%2FApple_IIGS_System_6.0.1%2FDisk_7_of_7-Apple_II_Setup.sea.bin',
|
|
'file' : 'Disk_7_of_7-Apple_II_Setup.sea.bin'
|
|
}
|
|
]
|
|
|
|
a2boot_files = [
|
|
{
|
|
'unix' : 'Apple ::e Boot Blocks',
|
|
'hfsutils' : 'Apple_--e_Boot_Blocks.bin',
|
|
'netatalk' : 'Apple :2f:2fe Boot Blocks',
|
|
'digest' : 'cada362ac2eca3ffa506e9b4e76650ba031e0035',
|
|
'patches' : {
|
|
'6.0.1' : (
|
|
[
|
|
'Cleartext password login bug',
|
|
(0x4d43, '\xA8\xA2\x01\xBD\x80\x38\x99\xA0\x38\xC8\xE8\xE0\x09\x90\xF4')
|
|
],
|
|
'6b7fc12fd118e1cb9e39c7a2b8cc870c844a3bac'
|
|
)
|
|
}
|
|
},
|
|
{
|
|
'unix' : 'Basic.System',
|
|
'hfsutils' : 'Basic.System.bin',
|
|
'digest' : '4d53424f1451cd2e874cf792dbdc8cc6735dcd36'
|
|
},
|
|
{
|
|
'unix' : 'ProDOS16 Boot Blocks',
|
|
'hfsutils' : 'ProDOS16_Boot_Blocks.bin',
|
|
'digest' : 'fab829e82e6662ed6aab119ad18e16ded7d43cda',
|
|
},
|
|
{
|
|
'unix' : 'ProDOS16 Image',
|
|
'hfsutils' : 'ProDOS16_Image.bin',
|
|
'digest' : 'db4608067b9e7877f45eb557971c4d8c45b46be5',
|
|
'patches' : {
|
|
'6.0.1' : (
|
|
[
|
|
'Cleartext password login bug',
|
|
(0x5837, '\xA8\xA2\x01\xBD\x80\x38\x99\xA0\x38\xC8\xE8\xE0\x09\x90\xF4'),
|
|
|
|
'Enable pressing "8" during GS/OS netboot to load ProDOS 8',
|
|
(0x0100, '\x92'),
|
|
(0x0360, '\x20\x7d\x14'),
|
|
(0x067d, '\xad\x00\xc0\x29\xff\x00\xc9\xb8\x00\xd0\x06\xa9\x02\x00\x8d\x53\x14\xa9\x10\x0f\x60')
|
|
],
|
|
'5c35d5533901b292ab7c2f5a3c76cb3113f66085'
|
|
)
|
|
}
|
|
},
|
|
{
|
|
'unix' : 'p8',
|
|
'hfsutils' : 'p8.bin',
|
|
'digest' : '36c288a5272cf01e0a64eed16786258959118e0e'
|
|
}
|
|
]
|
|
|
|
# True for Python 3.0 and later
|
|
PY3 = sys.version_info >= (3, 0)
|
|
|
|
if PY3:
|
|
stdin_input = input
|
|
import urllib.request as urlrequest
|
|
else:
|
|
stdin_input = raw_input
|
|
import urllib2 as urlrequest
|
|
|
|
|
|
# Differing from the shell script in that we explicitly strip the / here
|
|
if 'A2SERVER_SCRIPT_URL' in os.environ:
|
|
scriptURL = os.environ['A2SERVER_SCRIPT_URL']
|
|
# Strip trailing slash
|
|
if scriptURL.endswith('/'):
|
|
scriptURL = scriptURL[:-1]
|
|
else:
|
|
scriptURL = 'http://appleii.ivanx.com/a2server'
|
|
|
|
def sha1file (filename, blocksize=65536):
|
|
f = open(filename, "rb")
|
|
digest = hashlib.sha1()
|
|
buf = f.read(blocksize)
|
|
while len(buf) > 0:
|
|
digest.update(buf)
|
|
buf = f.read(blocksize)
|
|
f.close()
|
|
return digest.hexdigest()
|
|
|
|
def download_url(url, filename):
|
|
try:
|
|
html = urlrequest.urlopen(url)
|
|
data = html.read()
|
|
f = open(filename, 'wb')
|
|
f.write(data)
|
|
f.close
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
# Apple's GS/OS 6.0.1 images are stored in MacBinary-wrapped
|
|
# self-extracting disk image files. The Unarchiver's unar is able
|
|
# to unwrap the MacBinary wrapper for us, but we have to extract the
|
|
# disk image oursselves. Fortunately, it's uncompressed.
|
|
def extract_800k_sea_bin(wrapper_name, image_name, extract_dir):
|
|
# First we need to get rid of the MacBinary wrapper
|
|
# FIXME: Can we learn to read MacBinary? I bet we can!
|
|
if not os.path.isfile(wrapper_name):
|
|
raise IOError('Archive file "' + wrapper_name + '" does not exist')
|
|
|
|
# Extract the original filename from the file
|
|
# MacBinary II header is 128 bytes. The first byte is NUL, followed by a
|
|
# Pascal string of length 63 (so 64 bytes total) containing the encoded
|
|
# filename.
|
|
#
|
|
# Source: http://files.stairways.com/other/macbinaryii-standard-info.txt
|
|
# FIXME: We should eventually implement a full MacBinary reader.
|
|
|
|
f = open(wrapper_name, "rb")
|
|
sea_name = f.read(65)
|
|
f.close()
|
|
if PY3:
|
|
sea_name = sea_name[2:2 + sea_name[1]].decode('mac_roman')
|
|
else:
|
|
sea_name = sea_name[2:2 + ord(sea_name[1])]
|
|
|
|
cmdline = ['unar', '-q', '-o', extract_dir, '-k', 'skip', wrapper_name]
|
|
ret = subprocess.call(cmdline)
|
|
if ret != 0:
|
|
raise IOError('unar returned with status %i' % (ret))
|
|
|
|
# Do we have the right file?
|
|
sea_name = os.path.join(extract_dir, sea_name)
|
|
if not os.path.isfile(sea_name):
|
|
raise IOError('Expected image archive "' + sea_name + '" does not exist')
|
|
|
|
# Cowardly refuse to overwrite image_name
|
|
if os.path.exists(image_name):
|
|
raise IOError('"' + image_name + '" already exists')
|
|
|
|
# The image starts 84 bytes in, and is exactly 819200 bytes long
|
|
with open(sea_name, 'rb') as src, open(image_name, 'wb') as dst:
|
|
src.seek(84)
|
|
dst.write(src.read(819200))
|
|
if dst.tell() != 819200:
|
|
raise IOError(wrapper_name + ' did not contain an 800k floppy image')
|
|
|
|
# Now just clean up the archive files and we're done
|
|
os.unlink(sea_name)
|
|
|
|
def plist_keyvalue(plist_dict, key):
|
|
if plist_dict.tag != 'dict':
|
|
raise ValueError('not a plist dict')
|
|
found = False
|
|
for elem in plist_dict:
|
|
if found:
|
|
return elem
|
|
if elem.tag == 'key' and elem.text == key:
|
|
found = True
|
|
return None
|
|
|
|
def find_mountpoint(xmlstr):
|
|
plistroot = ET.fromstring(xmlstr)
|
|
if plistroot.tag != 'plist':
|
|
raise ValueError('xmlstr is not an XML-format plist')
|
|
if plistroot[0].tag == 'dict':
|
|
sys_entities = plist_keyvalue(plistroot[0], 'system-entities')
|
|
if sys_entities.tag != 'array':
|
|
raise ValueError('expected dict to contain an array')
|
|
for child in sys_entities:
|
|
if child.tag == 'dict':
|
|
mountpoint = plist_keyvalue(child, 'mount-point')
|
|
return mountpoint.text
|
|
else:
|
|
raise ValueError('system-entities should be an array of dict objects')
|
|
else:
|
|
raise ValueError('Root element is not a dict')
|
|
|
|
def install_bootblocks(installdir, installtype):
|
|
if installtype not in ['unix', 'netatalk']:
|
|
raise ValueError('Only basic UNIX and netatalk formats are supported for now')
|
|
|
|
devnull = open(os.devnull, "wb")
|
|
if not os.path.isdir(installdir):
|
|
os.makedirs(installdir, mode=0o0755)
|
|
|
|
bootblock_tmp = tempfile.mkdtemp(prefix = "tmp-a2sv-bootblocks.")
|
|
|
|
platform = os.uname()[0]
|
|
if platform not in ['Linux', 'Darwin']:
|
|
platform = "hfsutils"
|
|
elif platform == 'Linux':
|
|
use_sudo = False
|
|
if os.geteuid() != 0:
|
|
reply = stdin_input("""
|
|
You must have either root access or the hfsutils package to
|
|
access the disk image containing Apple // boot blocks. Do
|
|
you want to mount the image using the sudo command? [y] """)
|
|
if reply.startswith('y') or reply.startswith('Y') or reply == '':
|
|
use_sudo = True
|
|
print("""
|
|
Okay, if asked for a password, type your user password. It
|
|
will not be echoed when you type.""")
|
|
else:
|
|
platform = 'hfsutils'
|
|
|
|
unpacked_a2boot = False
|
|
for bootfile in a2boot_files:
|
|
if installtype == 'unix':
|
|
dst = bootfile['unix']
|
|
elif installtype == 'netatalk':
|
|
dst = bootfile['netatalk'] or bootfile['unix']
|
|
dst = os.path.join(installdir, dst)
|
|
|
|
if not os.path.isfile(dst):
|
|
# We need to fetch it
|
|
if not unpacked_a2boot:
|
|
a2setup_img = os.path.join(bootblock_tmp, 'A2SETUP.img')
|
|
disk7_downloaded = False
|
|
for disk7_source in disk7_sources:
|
|
disk7_file = os.path.join(bootblock_tmp, disk7_source['file'])
|
|
if download_url(disk7_source['url'], disk7_file):
|
|
disk7_downloaded = True
|
|
else:
|
|
continue
|
|
|
|
# If file is wrapped as .sea.bin (always true for now)
|
|
if disk7_source['type'] == 'sea.bin':
|
|
extract_800k_sea_bin(disk7_file, a2setup_img, bootblock_tmp)
|
|
os.unlink(disk7_file)
|
|
else:
|
|
# Implement non .sea.bin version
|
|
pass
|
|
break
|
|
|
|
if not disk7_downloaded:
|
|
raise IOError('Could not download disk7')
|
|
|
|
if platform == 'Linux':
|
|
mountpoint = os.path.join(bootblock_tmp, 'a2boot')
|
|
os.mkdir(mountpoint)
|
|
mount_cmd = ['mount', '-t', 'hfs', '-o', 'ro,loop', a2setup_img, mountpoint]
|
|
if use_sudo:
|
|
mount_cmd = ['sudo'] + mount_cmd
|
|
subprocess.call(mount_cmd)
|
|
srcdir = os.path.join(mountpoint, 'System Folder')
|
|
elif platform == 'Darwin':
|
|
xmlstr = subprocess.check_output(['hdiutil', 'attach', '-plist',
|
|
a2setup_img])
|
|
mountpoint = find_mountpoint(xmlstr)
|
|
srcdir = os.path.join(mountpoint, 'System Folder')
|
|
elif platform == 'hfsutils':
|
|
srcdir = os.path.join(bootblock_tmp, 'a2boot')
|
|
os.mkdir(srcdir)
|
|
subprocess.call(['hmount', a2setup_img], stdout=devnull)
|
|
subprocess.call(['hcopy', 'Apple II Setup:System Folder:*',
|
|
srcdir], stdout=devnull)
|
|
subprocess.call(['humount', 'Apple II Setup'], stdout=devnull)
|
|
|
|
unpacked_a2boot = True
|
|
|
|
# Copy the file
|
|
if platform == 'Linux' or platform == 'Darwin':
|
|
src = os.path.join(srcdir, bootfile['unix'])
|
|
elif platform == 'hfsutils':
|
|
src = os.path.join(srcdir, bootfile['hfsutils'])
|
|
shutil.copyfile(src, dst)
|
|
|
|
# Clean up the mounted/unpacked image
|
|
if unpacked_a2boot:
|
|
if platform == 'Linux':
|
|
umount_cmd = ['umount', mountpoint]
|
|
if use_sudo:
|
|
umount_cmd = ['sudo'] + umount_cmd
|
|
subprocess.call(umount_cmd)
|
|
os.rmdir(mountpoint)
|
|
elif platform == 'Darwin':
|
|
subprocess.call(['hdiutil', 'eject', mountpoint], stdout=devnull)
|
|
elif platform == 'hfsutils':
|
|
for bootfile in a2boot_files:
|
|
name = os.path.join(srcdir, bootfile['hfsutils'])
|
|
if os.path.isfile(name):
|
|
os.unlink(name)
|
|
os.rmdir(srcdir)
|
|
os.unlink(a2setup_img)
|
|
|
|
devnull.close()
|
|
os.rmdir(bootblock_tmp)
|
|
|
|
def do_install():
|
|
netboot_tmp = tempfile.mkdtemp(suffix = '.a2server-netboot')
|
|
print('You\'ll want to go and delete this directory:')
|
|
print(netboot_tmp)
|
|
os.chdir(netboot_tmp)
|
|
|
|
# If we need boot files:
|
|
# Download a disk image
|
|
# If it is one we need to unpack (.sea.bin):
|
|
# unar it
|
|
# extract the embedded image
|
|
# If we need to apply boot block patches:
|
|
# fix cleartext password bug in //e boot block
|
|
# fix cleartext password bug in IIgs boot block
|
|
# patch IIgs boot block to allow booting ProDOS 8
|
|
# If we don't have A2SERVER tools:
|
|
# Download the installer script
|
|
# Run the installer script
|
|
# Copy Basic.System to A2FILES for ProDOS 8
|
|
# If NETBOOT.P8 (battery ram set to boot into ProDOS 8) doesn't exist:
|
|
# Create it
|
|
# If NETBOOT.GSOS (battery ram set to boot into GSOS) doesn't exist:
|
|
# Create it
|
|
# Set GS/OS to boot SYSTEM/FINDER (registered user or guest)
|
|
# Set ProDOS 8 to boot BASIC.SYSTEM (guest)
|
|
# If SYSTEM/START.GS.OS doesn't exist in A2FILES:
|
|
# Ask if user wants to install GS/OS 6.0.1
|
|
# If they answer yes:
|
|
# create imagesDir
|
|
# create netInstallDir
|
|
# For each disk:
|
|
# Download the disk
|
|
# If it is one we need to unpack (.sea.bin):
|
|
# extract_800k_sea_bin it
|
|
# unpack the disk to netInstallDir
|
|
|
|
|
|
# XXX Re-enable this
|
|
#os.rmdir(netboot_tmp)
|
|
|
|
if __name__ == '__main__':
|
|
# bail out on automated netboot setup unless -b is also specified
|
|
# FIXME: This logic belongs in a2server-setup, not here
|
|
autoAnswerYes = os.path.isfile('/tmp/a2server-autoAnswerYes')
|
|
if autoAnswerYes and not os.path.isfile('/tmp/a2server-setupNetBoot'):
|
|
sys.exit(0)
|
|
|
|
# We need root to do this. If we don't have it, just rerun the command with
|
|
# sudo and be done with it.
|
|
#
|
|
# FIXME: Should we be doing this? A generic installer should not assume it's
|
|
# writing to a root-owned dir, nor care. Probably the reason to do it this way
|
|
# is as a proof of concept that it can be done this way in the main
|
|
# a2server-setup script, which of course means we don't actually need to change
|
|
# the password for the user.
|
|
#
|
|
# XXX Disabling this for development
|
|
"""
|
|
if os.geteuid() != 0:
|
|
args = sys.argv
|
|
args.insert(0, 'sudo')
|
|
# At the very least, we should print the command line we're running here
|
|
print ('Rerunning with sudo...')
|
|
ret = subprocess.call(args)
|
|
sys.exit(ret)
|
|
"""
|
|
|
|
install_bootblocks(os.path.join(os.getcwd(), 'a2boot'), 'unix')
|
|
#reply = stdin_input("""\nDo you want to set up A2SERVER to be able to boot Apple II\ncomputers over the network? [y] """)
|
|
#if reply.startswith('y') or reply.startswith('Y') or reply == '':
|
|
# do_install()
|