Tuesday, September 29, 2020

#1 2020-09-13 10:04:04 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 338

Retrieving properties of applications

The current handler, appInfo, returns properties of one or more applications efficiently.  The applications may be running or non-running.  If non-running, properties will be retrieved without launching the application.

The handler's input argument is an application reference, or a list of application references. Each application reference may be in any of the following forms:
     - Application name with or without a ".app" extension (e.g., "Safari" or "Safari.app")
     - Bundle identifier (e.g., "com.apple.Safari")
     - Full HFS path to the application bundle (e.g., "[startup disk name]:Applications:Safari.app")
     - Full POSIX path to the application bundle (e.g., "/Applications/Safari.app")
     - AppleScript alias to the application bundle (e.g., alias "[startup disk name]:Applications:Safari.app:")
     - «class furl» file reference to the application bundle (e.g., "/Applications/Safari.app" as POSIX file)
     - NSURL object referring to the application bundle (e.g., current application's |NSURL|'s fileURLWithPath:"/Applications/Safari.app")
     - Unix process ID as an integer (e.g., 60839) or text string (e.g., "60839"), if the application is running

The handler's return value is an AppleScript record of application properties of the form {appName:..., executableName:..., bundleIdentifier:..., posixPath:..., hfsPath:..., bundleURL:..., versionString:..., isAppleScriptEnabled:..., allowsUserInteraction:..., isBackgroundOnly:..., isRunning:..., processID:..., processIDs:...}
     appName
          ->  application bundle name = [app name].app
     executableName
          ->  executable name = /.../[app name].app/Contents/MacOS/[executableName]
     bundleIdentifier
          ->  Apple's bundle identifier in reverse DNS notation, e.g., "com.apple.Safari"
     posixPath
          ->  POSIX path to .app bundle = "/.../[app name].app"
     hfsPath
          ->  HFS path to .app bundle = "[startup disk name]:...:[app name].app"
     bundleURL
          ->  NSURL object referring to [app name].app bundle
     versionString
          ->  application version as text string (for some applications, this value is available only if the application is running)
     isAppleScriptEnabled
          ->  boolean true value if the application is AppleScript-enabled (i.e., if its Info.plist item NSAppleScriptEnabled = YES or 1 or true)
     allowsUserInteraction
          ->  boolean true value if the application runs in the background but allows user interaction (i.e., if its Info.plist item LSUIElement = YES or 1 or true)
     isBackgroundOnly
          ->  boolean true value if the application runs in the background and does not allow user interaction (i.e., if its Info.plist item LSBackgroundOnly = YES or 1 or true)
     isRunning
          ->  boolean true value if the application is running
     processID
          ->  Unix ID of the newest running instance of the application, or the missing value if the application is not running
     processIDs
          ->  list of the Unix IDs of all running instances of the application, or the empty list if the application is not running
     NOTES:
          - If the input argument is a single application reference, the output property values are the values themselves; if the input argument is a list of application references, the output record properties are lists of values corresponding to the input application references.
          - Any missing output property values will be set to missing value instead.
          - If the application can't be found, all its output properties will be set to missing value.

The details of how the handler works are described in the handler's comments.  Some highlights are as follows:
     - The handler retrieves an application's properties efficiently, generally about 0.1 seconds per application, whether the application is running or not
     - The bundle identifier is the first application property to be retrieved, from which the remaining properties are obtained
          - If the application reference is in the form of a running application's Unix process ID, the bundle identifier is retrieved using NSRunningApplication's runningApplicationWithProcessIdentifier: method
          - If the application reference is in any other form, the bundle identifier is retrieved from the application's id property
               - Retrieval via the id property allows efficient retrieval with the added benefit of not launching the application if it isn't running
               - The retrieval is performed within an osascript command wrapper, which has the benefit of preventing a choose application... dialog window from opening if the application isn't found
               - If the application reference is in the form of a bundle identifier, the bundle identifier is retrieved (i.e., validated) with the construct: application id [application reference]'s id
               - If the application reference is in any other form (i.e., neither a process ID nor a bundle identifier), the bundle identifier is retrieved (after converting the reference to a text string if necessary) with the construct: application [application reference]'s id

Here's an example of handler usage with a single application reference as the input argument:

Applescript:


   appInfo("Finder")
   -->
   {
   appName:"Finder",
   executableName:"Finder",
   bundleIdentifier:"com.apple.finder",
   posixPath:"/System/Library/CoreServices/Finder.app",
   hfsPath:"[startup disk name]:System:Library:CoreServices:Finder.app",
   bundleURL:(NSURL) file:///System/Library/CoreServices/Finder.app/,
   versionString:"10.13.6",
   isAppleScriptEnabled:true,
   allowsUserInteraction:false,
   isBackgroundOnly:false,
   isRunning:true,
   processID:1438,
   processIDs:{1438}
   }

Here's an example of handler usage with multiple application references as the input argument:

Applescript:


appInfo({"com.apple.Dock", "/Applications/QuickTime Player.app", "Nonexistent App"})
-->
   {
       appName:{
           "Dock",
           "QuickTime Player",
           missing value
       },
       executableName:{
           "Dock",
           "QuickTime Player",
           missing value
       },
       bundleIdentifier:{
           "com.apple.dock",
           "com.apple.QuickTimePlayerX",
           missing value
       },
       posixPath:{
           "/System/Library/CoreServices/Dock.app",
           "/Applications/QuickTime Player.app",
           missing value
       },
       hfsPath:{
           "[startup disk name]:System:Library:CoreServices:Dock.app",
           "[startup disk name]:Applications:QuickTime Player.app",
           missing value
       },
       bundleURL:{
           (NSURL) file:///System/Library/CoreServices/Dock.app/,
           (NSURL) file:///Applications/QuickTime%20Player.app/,
           missing value
       },
       versionString:{
           "1.8",
           "10.4",
           missing value
       },
       isAppleScriptEnabled:{
           false,
           true,
           missing value
       },
       allowsUserInteraction:{
           true,
           false,
           missing value
       },
       isBackgroundOnly:{
           false,
           false,
           missing value
       },
       isRunning:{
           true,
           false,
           missing value
       },
       processID:{
           1423,
           missing value,
           missing value
       },
       processIDs:{
           {1423},
           {},
           missing value
       }
   }

Here is the handler:

Applescript:


on appInfo(appRef)
   -- Returns properties of running and non-running applications
   -- Wrap the code in a try block to capture any errors
   try
       -- Constants
       set classNSURL to current application's |NSURL|'s |class|()
       set sharedWorkspace to current application's NSWorkspace's sharedWorkspace()
       set noValueToken to "¦NO¦VALUE¦"
       ---- Be sure the input argument is in the form of a list
       tell appRef to if its class ≠ list then set appRef to {it}
       -- Create the shell script that will return the application bundle identifiers, one bundle identifier per line of returned text
       set theScript to ""
       repeat with currRef in appRef
           tell currRef's contents to set {currRef, currClass} to {it, its class}
           try
               -- If the application reference is in the form of a process ID, be sure it is in the form of an integer
               set currRef to currRef as integer
           end try
           tell currRef
               try
                   -- Create the shell comnmands that will return the current reference's bundle identifier, or, if the bundle identifier can't be found, a "no value" token instead
                   if its class = integer then
                       -- If the application reference is in the form of a process ID, get the bundle identifier from Cocoa's NSRunningApplication class
                       -- If the bundle identifier can't be found, an error will be thrown, thereby setting the value to the "no value" token
                       tell ((current application's NSRunningApplication's runningApplicationWithProcessIdentifier:it)'s bundleIdentifier())
                           if it = missing value then error
                           set theScript to theScript & linefeed & "echo " & (it as text)'s quoted form
                       end tell
                       -- The following is an alternative Bash solution that works by reading the bundle identifier directly from the application's Info.plist file; however, it runs about 1.7x slower than the NSRunningApplication solution above
                       -- set theScript to theScript & linefeed & "/usr/libexec/PlistBuddy -c \"Print :CFBundleIdentifier\" \"$(ps -p " & it & " -o comm= | egrep -o -m 1 '^.+\\.app\\/Contents\\/')Info.plist\""
                   else if {its class} is in {text, alias, «class furl», classNSURL} then
                       -- If the application reference is supplied in any other form, coerce it to a text string, then get the bundle identifier from the application "id" property
                       -- Wrap these actions inside an osascript command to prevent a "choose application" dialog window from opening if the application isn't be found
                       -- If the bundle identifier can't be found, a "no value" token is returned instead
                       -- Note that the first construct, "application ...'s id", handles all application reference forms except bundle identifer, which is handled by the second construct, "application id ...'s id", and Unix process ID, which is handled separately by Cocoa's NSRunningApplication class
                       set theScript to theScript & linefeed & "osascript -e \"application \\\"" & it & "\\\"'s id\" || osascript -e \"application id \\\"" & it & "\\\"'s id\" || echo " & noValueToken
                   else
                       -- For any invalid application reference construct, force an error, thereby setting the value to the "no value" token
                       error
                   end if
               on error
                   set theScript to theScript & linefeed & "echo " & noValueToken
               end try
           end tell
       end repeat
       -- Execute the shell script, and capture the bundle identifiers (or "no value" tokens for any bundle identifiers that can't be found) from the returned lines of text
       set bundleIdentifierCandidates to (do shell script theScript)'s paragraphs
       -- Derive the remaining output properties from the bundle identifiers
       set {bundleIdentifier, bundleURL, posixPath, hfsPath, appName, executableName, versionString, isAppleScriptEnabled, allowsUserInteraction, isBackgroundOnly, isRunning, processID, processIDs} to {{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}
       repeat with currBundleIdentifier in bundleIdentifierCandidates
           -- Set the baseline value of all properties of the current application reference (except the bundle identifier property) to the missing value
           set currBundleIdentifier to currBundleIdentifier's contents
           set {currBundleURL, currPOSIXPath, currHFSPath, currAppName, currExecutableName, currVersionString, currIsAppleScriptEnabled, currAllowsUserInteraction, currIsBackgroundOnly, currIsRunning, currProcessID, currProcessIDs} to {missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value}
           if currBundleIdentifier = noValueToken then
               -- If a bundle identifier could not be found for the current application reference, change its bundle identifier property value from the "no value" token to the missing value, and abort any further processing
               set currBundleIdentifier to missing value
           else
               -- If the current application reference has a valid bundle identifier, get its remaining properties
               -- To get properties stored in the application's Info.plist file, convert to plist file to an NSDictionary, and retrieve properties via NSDictionary's objectForKey: method
               try
                   set currBundleURL to (sharedWorkspace's URLForApplicationWithBundleIdentifier:currBundleIdentifier)
               end try
               try
                   tell ((currBundleURL)'s |path|()) to if it ≠ missing value then set currPOSIXPath to it as text
               end try
               try
                   set currHFSPath to currPOSIXPath as POSIX file as text
               end try
               try
                   set currInfoPlistDictionary to (current application's NSDictionary's dictionaryWithContentsOfFile:(currPOSIXPath & "/Contents/Info.plist"))
                   try
                       tell (currInfoPlistDictionary's objectForKey:"CFBundleName") to if it ≠ missing value then set currAppName to it as text
                   end try
                   try
                       tell (currInfoPlistDictionary's objectForKey:"CFBundleExecutable") to if it ≠ missing value then set currExecutableName to it as text
                   end try
                   try
                       tell (currInfoPlistDictionary's objectForKey:"CFBundleShortVersionString")
                           if it = missing value then error
                           set currVersionString to it as text
                       end tell
                   on error
                       try
                           tell (currInfoPlistDictionary's objectForKey:"CFBundleVersion") to if it ≠ missing value then set currVersionString to it as text -- handles the occasional application without a short version string
                       end try
                   end try
                   try
                       tell (currInfoPlistDictionary's objectForKey:"NSAppleScriptEnabled") to set currIsAppleScriptEnabled to {it as text} is in {"true", "1", "YES"}
                   end try
                   try
                       tell (currInfoPlistDictionary's objectForKey:"LSUIElement") to set currAllowsUserInteraction to {it as text} is in {"true", "1", "YES"}
                   end try
                   try
                       tell (currInfoPlistDictionary's objectForKey:"LSBackgroundOnly") to set currIsBackgroundOnly to {it as text} is in {"true", "1", "YES"}
                   end try
               end try
               -- Get the Unix process IDs of all running instances of the application
               try
                   tell ((current application's NSRunningApplication's runningApplicationsWithBundleIdentifier:currBundleIdentifier) as list)
                       set currProcessIDs to {}
                       repeat with currObj in it
                           try
                               set end of currProcessIDs to currObj's processIdentifier() as integer
                           end try
                       end repeat
                       if currProcessIDs = {} then error
                       -- Save the newest running instance of the application in a separate return property from the property containing the full list of running application instances
                       set {currIsRunning, currProcessID} to {true, currProcessIDs's last item}
                   end tell
               on error
                   set currIsRunning to false
               end try
           end if
           -- Add the current application's values to the output variable lists
           set {end of bundleIdentifier, end of bundleURL, end of posixPath, end of hfsPath, end of appName, end of executableName, end of versionString, end of isAppleScriptEnabled, end of allowsUserInteraction, end of isBackgroundOnly, end of isRunning, end of processID, end of processIDs} to {currBundleIdentifier, currBundleURL, currPOSIXPath, currHFSPath, currAppName, currExecutableName, currVersionString, currIsAppleScriptEnabled, currAllowsUserInteraction, currIsBackgroundOnly, currIsRunning, currProcessID, currProcessIDs}
       end repeat
       -- Return the results; if the input argument consisted of only one application reference, delist the output variable values before returning them to the calling program
       if bundleIdentifier's length = 1 then set {appName, executableName, bundleIdentifier, posixPath, hfsPath, bundleURL, versionString, isAppleScriptEnabled, allowsUserInteraction, isBackgroundOnly, isRunning, processID, processIDs} to {appName's first item, executableName's first item, bundleIdentifier's first item, posixPath's first item, hfsPath's first item, bundleURL's first item, versionString's first item, isAppleScriptEnabled's first item, allowsUserInteraction's first item, isBackgroundOnly's first item, isRunning's first item, processID's first item, processIDs's first item}
       return {appName:appName, executableName:executableName, bundleIdentifier:bundleIdentifier, posixPath:posixPath, hfsPath:hfsPath, bundleURL:bundleURL, versionString:versionString, isAppleScriptEnabled:isAppleScriptEnabled, allowsUserInteraction:allowsUserInteraction, isBackgroundOnly:isBackgroundOnly, isRunning:isRunning, processID:processID, processIDs:processIDs}
   on error m number n
       if n = -128 then error number -128
       if n ≠ -2700 then set m to "(" & n & ") " & m
       error ("Problem with handler appInfo:" & return & return & m)
   end try
end appInfo

Edit note: A minor cosmetic change was made to the originally submitted version for the code that retrieves the value of the Info.plist item CFBundleShortVersionString.
Edit note 2: An error in the «class furl» file reference example in the introductory discussion (not the handler code) was corrected.

Last edited by bmose (2020-09-24 10:56:48 am)

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)