Apple - Save open applications and tabs state to file and be able to restore

NOTE: Answer still under construction...

One more section to write, detailing the scripts that will sync the running state of the applications. I have to write these scripts first.


The Objective

Broadly, to devise a means for two Macs to save and share information on the user environment, including application states/properties, loaded documents, and actively running applications.

Your initial request was to seamlessly move from one [machine] to the other and recreate the state of open applications I had in the previous one with a command. You've since relaxed your expectations a little, and clarified that "hot-reloading" applications is not sought, and minutiae such as window sizes and positions are not a concern.

Conceptual Methodology

1. Two scripts + a shared data file

My initial idea was to have a copy of a script (probably an AppleScript) on each machine that would have a handler that was responsible for getting information about the application environments and storing it in a shared file to which both machines would have access (e.g. via Dropbox, iCloud, ...). The script would have a separate handler that would retrieve the contents of this shared file, and apply the settings to the local environment. This would require the user to run the script once upon arriving at a machine in order to apply the settings; and once just before departing the machine in order to update the shared data file with new settings.

2. .savedState synchronisation

My second thought was inspired from another person's notion of synchronising the file located at ~/Library/Preferences/ByHost/com.apple.loginwindow.*. Whilst doing so would not be helpful, it raises the question of whether synchronising .savedState files found in the folder ~/Library/Saved Application State would be the solution.

Admittedly, I can't claim to know what the full ramifications of attempting to sync these files across two different systems would be. That's not to say that anything bad would happen, but it's unknown territory, so the potential for undesirable results is there. However, my feeling is that it would largely be safe, and probably quite innocuous, given that, if anything were to go awry from a bad .savedState file, the solution is pretty much always to just delete the .savedState file, and allow the application in question to generate a new one.

I suspect a combination of these two methods will be needed.

Syncing

Syncing the ~/ folder via Dropbox

Syncing the home folder would likely only provide you with a means to make documents available on each machine, but possibly not allow you to sync the ~/Library folder if its location cannot be changed. Also, this folder is 26GB in size on my system, so it would involve paying a subscription for extra cloud storage space.

Ultimately, if syncing the home folder is limited to Downloads, Documents, Music, and Movies, it isn't much of a benefit over having the Documents and Desktop folders synced through iCloud.

However, having documents synced is a good idea, as it will be important to have access to the same files on both computers, and for changes on one to be reflected on the other. iCloud, whilst quite slow, is fine for this job.

Syncing the ~/Library folder

The ~/Library folder, or at least specific subfolders of it, would be a crucial goal if contemplating using .savedState files to map application states from one system to another. A home cloud storage solution would be ideal for this task, as it doesn't levy any subscription fees; your sensitive data would be safe in your possession and not owned by Dropbox or Apple; and there'd be no speed throttling outside of that imposed by your ISP if syncing across the internet.

I already use Resilio Sync, which is free for individual use, and uses peer-to-peer transfers to keep selected folders in sync between any machines or devices on which you install the application. It would be a simple case of selecting the ~/Library folder or the ~/Library/Saved Application State folder to be monitored and synced in real-time. The folders can remain where they are, and if you choose to pay for additional features (one-off payment), you can selectively sync individual files within a folder, and encrypt them for data security. I really recommend this application, and it seems ideal for this specific situation.

Scripting

If synchronising the .savedState files is successful, what might be needed is a script to open (or reopen) the applications that were running on the other machine. Reopening will hopefully get the application to read its updated .savedState files and replicate the state it was in on the other system. I think this will have partial success, but perhaps not total.

Retrieving a list of running applications

This is trickier than it sounds because there are various ways to test if an application is running, and no one test is perfect. There are the applications in the foreground that are visible with windows; there are menu bar applications, some with no windows, running as background processes; and there could be hidden applications minimised in the dock, with no visible windows.

After experimenting with a few different methods in AppleScript, this line of code seems to produce a good, exhaustive list of running applications, including menu bar applications:

    tell application "System Events" to set OpenApps to the ¬
        name of every application process whose ¬
        class of its menu bar contains menu bar and ¬
        POSIX path of the application file does not start with "/System" and ¬
        POSIX path of the application file does not start with "/Library"

It takes a couple of seconds to return a result, but I don't think that speed will matter too much if the scripts are implemented in a sensible way.

A Bash method of getting a list of running applications is this:

    IFS=$'\r\n'; basename $( \
    egrep -i -e '^/Applications/.*\.app/Contents/MacOS/[^/]+$' \
    <<<"$(ps -U 501 -o 'comm')" ) | sort -u

It's fast (instantaneous), but does miss a couple of menu bar and background applications.

I think either method will ultimately be fine for what's needed, as the most important applications to capture are the ones you're interacting with in the foreground, and those ones are caught by most methods.

Storing information in a shared settings file

Writing the settings that need to be saved can be done to a .plist file, which stores key/value pairs of data along with the data types. This allows any value to be saved and retrieved by its name. They're also relatively simply to read and edit programmatically using either AppleScript or the command line utility plutil.

Let's assume the shared settings file will be saved at ~/Example/savedState.plist where ~/Example/ is some sort of shared folder (e.g. a cloud storage folder) to which both computers have access.

Using AppleScript, this is how one might save a list of applications that are currently open:

    property plist : "~/Example/SavedState.plist"

    tell application "System Events"
        if not (the file plist exists) then make new property list file ¬
            with properties {name:plist}

        set plistf to the property list file named plist

        tell plistf to make new property list item with properties ¬
            {name:"OpenApps", kind:list, value:OpenApps}
    end tell

where the variable OpenApps is taken from the AppleScript snippet above. Cleverly, the make new property list command will overwrite any existing key/value item that already has the name "OpenApps", so it won't be duplicated.

Next, we need a way to quit applications on a system:

    to kill(A as list, X as list)
        local A, X

        if A = {} then return

        script jury
            property A0 : item 1 of A
            property An : rest of A
            property OK : A0 is not in X
        end script

        tell the jury
            ignoring application responses
                if its OK then quit the application named (its A0)
                kill(its An, X)
            end ignoring
        end tell
    end kill

This is a recursive handler that is passed a list of applications to quit (parameter A) and a list of applications that are to be excluded (parameter X). It then goes through the list one by one and checks the application isn't one of the ones in X before quitting it and moving onto the rest of the list.

Re-opening documents

By employing the use of synchronised .savedState files, the hope is that, once an application on one system re-initialises (say, by closing and opening it again), it will re-open documents that were open on the other.

However, it may be prudent store our own list of documents that we can detect (to the best of our abilities) are open and active.


[NOTES FOR ME TO INTEGRATE INTO ANSWER]

  • I Noticed that some .savedState files are stored in ~/Library/Saved Application State as aliases (symbolic links) instead of the actual file itself. These typically link to ~/Library/Containers/%bundle-id%/Data/Library/Saved Application State/%file% where %bundle-id% is, for example, com.apple.Preview, and its corresponding %file% is com.apple.Preview.savedState.

This means that ~/Library/Containers will likely have to be synchronised between the two machines as well.

  • .savedState files are deleted whenever an application exits cleanly. Therefore, bear in mind that closing applications on one system will delete those data files, then potentially synchronise these deletions with the other machine.