App Playpen – Python with Xcode UI

App Playpen – Python with Xcode UI

APP PLAYPEN – PYTHON WITH XCODE UI

 

What is App Playpen?


App Playpen is designed to be used as a convenient automation tool for building a disk image launcher in which to place an application with exceptional requirements beyond an average application. The outputted launcher application limits modifications to a mounted shadow image that can be discarded on logout in shared system environments like a student lab or can be retained in single-user systems but preventing modifications to sensitive and secured areas of the Mac file system.

Here are some examples of enterprise unfriendly application exceptional requirements:

  • Applications that require insecure permissions
  • Applications that updates with every launch
  • Applications that require additional configuration post-launch
  • Applications that require to be launched by a specific user or group

 

Terminology


Components

For overall components of this tool, there is some terminology that we use which may not be intuitive, so I have included their definitions here:

  • Builder – Tool to automate the process of creating a launcher script that contains enterprise unfriendly application image
  • Launcher – Script that mounts image and runs additional command/scripts pre or post-launch or quitting the application
  • Image – Disk image that contains enterprise unfriendly application
  • Playpen – Used synonymously with image, describing the finished result of App Playpen

 

Fields

Terminology for App Playpen field options:

  • Name – Name of enterprise unfriendly application
  • Location – Path to enterprise unfriendly application/folder
  • Version – The short version for the enterprise unfriendly application – CFBundleShortVersionString
  • Icon – The icon for the enterprise unfriendly application – CFBundleIconFile
  • Identifier – The unique identifier or the enterprise unfriendly application – CFBundleIdentifier
  • Image Size – The created disk image size in GB
  • Pre-Script – Script or command that runs just after the image is mounted before the application is launched
  • Post-Script – Script or command that runs after the image is mounted and application is opened
  • Close-Script – Script or command that runs after the application is quit and before the image is unmounted

 

History


To give some background on App Playpen, the first version of it was created using AppleScript initially written by Pierce Darragh around 2001 – 2002 and later revised by Sam Forester. This was made into an application file format and the disk image file was added to the launcher application where it would be mounted and launched. This was okay for the time however, but the creation process was very time-intensive, was prone to mistakes which meant you had to restart the process. Because of this and the needed to edit the AppleScript any time you created a new launcher which meant spending additional time and energy creating this it for enterprise unfriendly applications. We wanted an easier tool that didn’t require editing every time, that would be easy to use and wasn’t prone to errors due to uneducated staff or from small mistakes. This allows us to just distribute the tool out instead of having each staff member follow defined steps to create launchers for these enterprise unfriendly applications.

App Playpen was initially created to better secure the classic environment running in shared environments. Because the classic environment had security and state issues it was safer and more secure to place it in a separate disk image where it had read-write permissions and the shadow file to preserve work done for the user.

Here is an example of how appName needed to be changed every time you wanted to update or create a new “Crappy App”:

on run
	-- Set all of the locations for things.
	-- Most likely, you'll only ever need to change the 'appName'. If you give everything else
	-- similar names, it'll make your life easier.
	--
	-- appName:		the name of the application
	-- diskName:		the name of the disk
	-- appPath:		the path to the application in the image
	-- imagePath:		the path to the image in this bundle
	-- homeFolder:	the path of the current user's home folder
	-- shadowFolder:	the directory in which the shadow file will be saved
	-- shadowFile:	the full path to the shadow file
	-- mountRoot:	the location to mount the volume under (change for obnoxious applications)
	set myPath to (path to me as Unicode text)
	set appName to "<TEMPLATE>"
	set diskName to appName
	set appPath to diskName & ":Applications:" & appName & ".app"
	set imagePath to POSIX path of (myPath & "Contents:Resources:" & appName & ".dmg") as Unicode text
	set homeFolder to (get path to home folder) as Unicode text
	set shadowFolder to (homeFolder & "Library:Application Support:" & appName)
	set shadowFile to POSIX path of (shadowFolder & ":" & appName & ".shadow")
	set mountRoot to "/Volumes"
	my main()
end run

on main()
	-- If the application is running, quit.
	-- Otherwise, launch it!
	if (isRunning()) then
		display dialog (appName & " is already running!") buttons {"Thanks for the reminder!"} default button 1 giving up after 5 with icon caution
		return
	else
		launch {}
	end if
end main

The new launcher requires no manual editing beyond fixes and updates:

def main():
    app_name = getName()
    
    if checkForApplication(app_name):
        print('Application is Running!')
    else:
        if shadowFileCheck(app_name) == False:
            shadowFileCreate(app_name)

        launchApplication(app_name)
        checkForPreScript()
        openForUser(app_name)
        checkForPostScript()
        time.sleep(5)
        
    while checkForApplication(app_name) == True:
        time.sleep(5)
    
    shadow_path = pathlib.Path.home()/'Library'/'Application Support'/app_name
    cmd = ['rm', '-rf', shadow_path]
    process = subprocess.Popen(cmd)
    process.wait()
    
    checkForCloseScript()
    detachVolume = ['hdiutil', 'detach', f"/Volumes/{app_name}"]
    detach = subprocess.Popen(detachVolume)
    detach.wait()
    checkForPostScript()

 

Why did I make App Playpen?


Initially, we had to create a disk image manually, selecting the application that we needed to distribute and then custom creating the disk image for the application manually each time we wanted to update an enterprise unfriendly application. We affectionately call it the  “Crappy App” model which we use sanitize applications with exceptional requirements. 

We distribute “Crappy Apps” because of the need for certain applications for students, staff & faculty that we support while also meeting the goals and requirements of different departments on our decentralized campus. Because of this need, some applications are going to be unfriendly especially in a shared user environment like a student lab or classroom. The “Crappy App” approach allowed for a “more” closed environment that allows an application to work properly, but keeps a shared user environment as clean and secure as possible. To meet this need we create a shadow file on the launch of the application, this keeps the information stored for the user but does not affect the shared user space.

An example of a “Crappy App” would be Unreal Engine (Epic Games Launcher), which needs to update frequently and edits files in its own space which require read-write permissions which is not ideal.

This was then coupled with an AppleScript that would launch the application however, it too had to be updated each time we created a  new disk image that contained the enterprise unfriendly application. This meant that the process was prone to errors and the launcher failed and had to be debugged very often. This inspired the need for a better alternative, now with Apple requiring applications to be signed we wanted to use python for the launcher so that it did not have to sign every single application we created and distributed.

 

My Process


I started with the GUI part of App Playpen to provide users an easy way to select the application they wanted to make a launcher to allow people who are not familiar with terminal commands, an easy method around the more command line portions of the process. When creating the GUI I  used “Tkinter” to build it fully in python. Once I had completed a working GUI for App Playpen using “Tkinter”, I started working on the functions on how to actually create the App Playpen.

“Tkinter” is the standard GUI library for Python. Python, when combined with “Tkinter”, provides a fast and easy way to create GUI applications. “Tkinter” provides a powerful object-oriented interface to the Tk GUI toolkit.

The resulting “Tkinter” based GUI:

Tkinter Based GUI

Once I had completed a working GUI for App Playpen using “Tkinter”, I started working on the functions on how to actually create the launcher.

I used subprocess to get many of the commands to be the same as with the manual version, with all the variables being gathered and entered automatically. I found that there had to be two interactions from the user due to how the App Playpen GUI creation process works, once they selected the enterprise unfriendly application, and again when they need to open the application to perform the initial setup.

This need forced me to create a wait window to allow users to edit their application setup anytime needed. Once a user was done, they click “OK” on the wait window which launches the finish methods on the App Playpen and places the segmented disk image into the launcher. The launcher uses the framework from the old AppleScript, but now its made with python and is fully dynamic so no edits are necessary to make it work properly. Once I finished those modifications, I used “py2app” to create the launcher and creator into an app format. However, I ran into an issue where “py2app” and “pyinstaller” would crash 10.14 machines when also using “Tkinter”. In order to get past this issue, I reached out to the python subchannel on MacAdmin slack and was referred to use “nibbler” and Xcode to create the GUI since it was only going to be used on macOS.

In order to use “nibbler”, I had to learn how to use a form of Cocoa and Objective-C. Initially, this was harder than I thought it would be. However, after learning the basic syntax notation that is used for Cocoa and Objective-C in python, it was easy to integrate it into this tool. From there it was a matter of remaking what I had already built using “Tkinter” into Xcode, but now with a drag and drop option for GUI elements, it was a lot faster and easier than using “Tkinter”.

The resulting new GUI for App Playpen:

Once I had created the user interface, I had to figure out how to allow users to select an application and have my script gather the metadata it needed. This was done using “Cocoa”, “pyobjc”, and “python” by creating a window from the App Playpen GUI application that user-selected and collecting the path to the enterprise unfriendly application

def select_app():
    """
    Collects Data from the selected apps plist
    """
    panel = Cocoa.NSOpenPanel.openPanel()
    panel.setCanChooseFiles_(True)
    panel.setCanChooseDirectories_(True)
    panel.setResolvesAliases_(True)

    if(panel.runModal() == Cocoa.NSOKButton):
        pathArray = panel.filenames()
        path = pathlib.Path(pathArray[0])
        
        plistPath = path /'Contents'/'Info.plist'
        infoFile = plistPath

From there I used existing code with some minor modifications to work with the new “nibbler” based GUI. Once that had been created I ran into a problem with it only working with python 2 which was not going to work since I was using python 3 libraries to make my script work. Python 2.7 will not be maintained past 2020, so, needed to find an alternative solution. The solution I found was the version of “nibbler” that I had been using was outdated and that a python 3 compliant version was released. Once I had the updated version of “nibbler” I was able to use my old code with any need to edit it making the process significantly easier.

Because the “Builder” is a native Mac application, the operating system warns about lack of developer signature when launching:

This can be ignored when using the App Playpen “Builder” application. The App Playpen Launcher command-line tool is written entirely with python vs a native application, and will not display this developer certificate warning. This is ideal for distributing many enterprise unfriendly applications, as it makes it faster and easier to push out updates without additional work and overhead.

For the logo, Leah Donaldson of the Marriott Library designed it already for future App Playpen development. I thought it was the most fitting and had some humor to it which I enjoyed, so with the logo added to the header I already created for the tool, the resulting logo became:

 

Why Should You Use App Playpen


If you are an enterprise environment looking to distribute applications there are going to possibly pose a security or data risk due to their needed read-write permissions or that have other special requirements, this is where App Playpen could be useful. It allows for the easy creation of an application launcher that will look like and for the most part act as the enterprise unfriendly application. Some examples of enterprise unfriendly applications would be, NVivo, ZBrush, Epic Games Launcher (Unreal Engine), and Unity. Because these apps either need a license after launch, in the case of NVivo you can’t activate it on download as it will count as an activation using up your licenses, Epic Games Launcher updates all the time needing read-write permissions to do so. With App Playpen, you have a workaround for the application to update as needed without giving it special permissions posing a security & data risk. Another feature of App Playpen is the ability to package scripts or commands that can be run before, after, and on close of the application. In the case of NVivo, you can run the license activation when users actually launch and use the application.

Another reason to use “App Playpen” is that it allows you to have multiple versions of the same application on one system that would otherwise have created an issue. One example of this would be Unity and Unity Pro.

 

Troubleshooting App Playpen


In order to troubleshoot “App Playpen”, there are some error windows that display when an error occurs. However, if no error displays and it is not working properly, you can launch  “App Playpen” from the Mach-O executable file which is located in the application package.

This displays a terminal window showing executable output and any errors that occurred with more details than the standard error dialogs.

 

Python Tips


When creating this tool I found that it was easier to break up the code into different sections for readability. When working with “Tkinter” as the UI, it can get very messy, and without clearly defined sections of functionality, it can be very time-consuming to read through the code to find the one part you want to edit.

When I made the switch to use the “nibbler” tool, I did not see an easy method of adding a second “nibbler” window to the project without it launching at the same time as the original .nib UI which was not ideal. While there may be a solution to this issue, I have not found it yet. But, it did lead me to find that initializing the .nib file in the “if name is main” statement kept it running smoothly and allow it to call methods within the script that called “nibbler” from other scripts without causing issues.

if __name__ == '__main__':
    try:
        dir_path = os.path.dirname(os.path.realpath(__file__))
        dir_path = pathlib.Path(dir_path).parent / 'App_Playpen.nib'
        n = nibbler(dir_path)
    except Exception as err:
        print("Unable to load nib: {0}".format(err))
        sys.exit(20)
    
    main()

To get “nibbler” integration from an existing UI it was easiest to create the UI using an Xcode .nib file. This allows you to connect the parts you wanted to the code that were already written, which is significantly easier than having to format everything by hand which was needed to do with “Tkinter”. You should learn some Cocoa, Objective-C, and Swift to get an understanding of how to use the different UI elements provided by Xcode in python using “nibbler”.

I found the following resources to be very helpful when learning more about Cocoa Fundamentals, Objective-C, and Swift:

 

Tkinter Form:

def submit_to_logic(validate=True):
            """
            enters the logic into the main logic Script (I.E the wrapper creator)
            """
            preScript = beforeScriptBox.get() or None # "Tkinter" format
            postScript = postScriptBox.get() or None # "Tkinter" format
            closeScript = closeScriptBox.get() or None # "Tkinter" format
            setName = nameBox.get() or None # "Tkinter" format
            collectedIdentifier = identBox.get() or None # "Tkinter" format
            collectedIcon = iconBox.get() or None # "Tkinter" format
            collectedVersion = versionBox.get() or None # "Tkinter" format
            collectedLocation = locationBox.get() or None # "Tkinter" format
            collectedSize = sizeBox.get() or None # "Tkinter" format

            data = {
                'prescript': preScript,
                'postscript': postScript,
                'closescript' : closeScript,
                'collectedname': collectedName,
                'setname': setName,
                'collectedidentifier': collectedIdentifier,
                'collectedicon': collectedIcon,
                'collectedversion': collectedVersion,
                'collectedlocation': collectedLocation,
                'collectedsize': collectedSize
                }

            if validate:
                # pass information to validate
                if collectedName == None or collectedName == '':
                    error_window('    No Name Found.    ')
                elif collectedSize == None:
                    error_window('    No Size Added.    ')
                else:
                    try:
                        Crappy_App_Logic.main(data)
                    except Exception as err:
                        error_window(err)

Nibbler Form:

def submit_data():
    preScript = n.views['preScript'].stringValue() or None # changed to use "nibbler" format
    postScript = n.views['postScript'].stringValue() or None # changed to use "nibbler" format
    closeScript = n.views['closeScript'].stringValue() or None # changed to use "nibbler" format
    setName = n.views['appName'].stringValue() or None # changed to use "nibbler" format
    collectedIdentifier = n.views['appIdentifier'].stringValue() or None # changed to use "nibbler" format
    collectedIcon = n.views['appIcon'].stringValue() or None # changed to use "nibbler" format
    collectedVersion = n.views['appVersion'].stringValue() or None # changed to use "nibbler" format
    collectedLocation = n.views['appLocation'].stringValue() or None # changed to use "nibbler" format
    collectedSize = n.views['appSize'].stringValue() or None # changed to use "nibbler" format
    
    data = {
    'prescript': preScript,
    'postscript': postScript,
    'closescript' : closeScript,
    'collectedname': collectedName,
    'setname': setName,
    'collectedidentifier': collectedIdentifier,
    'collectedicon': collectedIcon,
    'collectedversion': collectedVersion,
    'collectedlocation': collectedLocation,
    'collectedsize': collectedSize
    }
        # pass information to validate
    if collectedName == None or collectedName == '':
        print('No Name')
        error_window('    No Name Found.    ')
    elif collectedSize == None:
        print('No Size')
        error_window('    No Size Added.    ')
    else:
        try:
            Crappy_App_Logic.main(data)
        except Exception as err:
            print('An Error Occured: {0}'.format(err))
            error_window(err)

This was previously written to utilize “Tkinter”, now with only a few key line changes, I was able to make it fully compatible with “nibbler” implementation.

GitHub Post


The GitHub post for App Playpen can be found here.

No Comments

Leave a Reply