0

Imagine you have a program that you want to keep running, even when unattended. But it sometimes freezes or crashes, and so you need am automatic way to detect these cases and act accordingly.

I understand that part of this can be handled by using launchd, but this mechanism is not made for GUI apps (AFAIK), and it also won't detect if the program simply freezes up indefinitely.

(This is related to my question here: How can an OS X application get itself restarted when it crashes)

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149

2 Answers2

1

I've solved this with the following AppleScript. Explanations are included in the source code.

To use this code, you need to save it as an Application with the option "Stay open" in Script Editor or Script Debugger.

(If you have syntactical improvements, please edit the code directly. For functional changes, please comment or post your own.)

-- This script makes sure that a particular program keeps on running,
-- even if it has frozen, in which case it'll be force-quit and relaunched.
--
-- Written 31 May 2021 by Thomas Tempelmann (apps.tempel.org).
-- I claim no copyright. Use it as you like.

-----------
-- Setup
-----------

-- This is the time after which an unresponsive app is considered
-- frozen. I suggest using 30 (for 30 seconds).
property timeoutInSeconds : 30

-- Set the app's bundle ID directly if you know it,
-- such as "com.yourdomain.appname"
property bundleID : ""

-- Otherwise, specify the path to the app:
property theAppPath : "/Applications/The App Name.app"

-----------
-- The following code should need no alterations. It'll force-quit the app if it
-- is frozen, and launch the app if it was not runnnig or if it was force-quit.
-----------

global theAppFile

on run
    if bundleID is "" then
        set theAppFile to POSIX file theAppPath
        set bundleID to bundle identifier of (info for (theAppFile))
    else
        set theAppFile to null
    end if
end run

on idle
    -- This code gets invoked once a minute
    -- It will then check on the app and restart it if necessary:
    my checkTheApp()
    
    return 60 -- idleInterval in seconds
end idle

on checkTheApp()
    -- Locate all running programs that use the given 'bundleID'
    tell application id "com.apple.systemevents" -- System Events.app
        set matchingProcesses to (every process whose bundle identifier is bundleID)
        set isRunning to matchingProcesses is not {}
        if isRunning then
            tell first item of matchingProcesses
                set theID to its unix id
                set theAppFile to (POSIX path of its application file) as POSIX file
            end tell
        end if
    end tell
    
    set isFrozen to false
    if isRunning then
        -- The program is running. Now let's check whether it's frozen.
        with timeout of timeoutInSeconds seconds
            try
                tell application id bundleID
                    get documents
                end tell
            on error errMsg number errNum
                if errNum is -1712 then
                    -- This error means that the request timed out (i.e. no response).
                    set isFrozen to true
                else
                    -- Some other error, usually -1728 for apps that do not process
                    -- this particular AppleEvent request.
                    -- As it's not a timeout, we assume that the app is not frozen.
                end if
            end try
        end timeout
    end if
    
    -- If the app is frozen, we force-quit it.
    -- Unsaved changes will be lost!
    if isFrozen then
        try
            do shell script "/bin/kill -9 " & theID
            set isRunning to false
            delay 1
        end try
    end if
    
    -- Restart the app if it's not runnning
    if not isRunning then
        if (theAppFile is not null) and (theAppFile exists) then
            -- We prefer to restart exactly the very same app that
            -- we had force-quit above
            set itsAlias to theAppFile as alias
            activate application (itsAlias as string)
        else
            -- If don't know the app's path we launch it by its bundleID
            activate application id bundleID
        end if
    end if
end checkTheApp
Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
0

A briefer script has been suggested by Robert Kniazidis on MacScripter.

It also handles waiting for the kill process to succeed better, using a polling loop.

The only issue I'd have with it is that if you have multiple versions of the app and want to run a specific version of it, this script won't give you that option. Apart from that, it looks much better than my version.

-- this script makes sure that a particular program keeps on running,
-- even if it has frozen, in which case it'll be force-quit and relaunched.

property timeoutInSeconds : 30
property appName : "Handbrake"
property bundleID : id of application appName

on idle
   if running of application id bundleID then
       tell application id "com.apple.systemevents" to set theID to ¬
           unix id of (first process whose bundle identifier is bundleID)
       with timeout of timeoutInSeconds seconds
           try
               tell application id bundleID to get documents
           on error errMsg number errNum
               if errNum is -1712 then -- is frozen
                   do shell script "/bin/kill -9 " & theID
                   repeat while its running of application id bundleID
                       delay 0.2
                   end repeat
                   activate application id bundleID
               end if
           end try
       end timeout
   else
       activate application id bundleID
   end if
   return 60
end idle
Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149