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