CodeMunki.com
Mostly AppleScript With Some Other Programming Stuff Thrown In For Good Measure
Process Runner: A Script Scheduling and Launching System
05 Jan 2018 9:45 AM

This system will launch any number of scripted processes, each on their own unique schedule of your choosing. The code is written using only "vanilla" AS and has no dependencies other than System Events, Standard Additions, and a handful of shell commands. The system is robust and has been in continuous operation for years, running on a Mac Mini used as a "drone". We reboot the computer about once a week to maintain system stability.

Here's how it works: first, a process folder is set up that contains the following files:
    Process_Runner.app        The controlling script using an idle loop, saved as a stay-open application
    Sample_Process_1.scpt    A process script file to execute
    Sample_Process_1.plist    A property list for the script's "Run Parameters" and execution history

Note that each process consists of a pair of files: a compiled script file and a property list file. There may be any number of processes present, or none at all. The brains of the system, the script "Process_Runner.app", will dynamically detect them and deploy any found processes according to each one's unique schedule. Processes may be added or removed any time the main script is stopped.

Processes can be triggered by one or more of the following "Run Parameters". These parameters are compared against the current day and time, and the history of prior executions of the process, to determine whether or not to run the process. This is rechecked 30 seconds (or at whatever delay you desire) after any triggered processes complete their runs. Launch times are approximate, depending on the number of processes and how long they each take to complete.

    dailyStartTime    (Earliest time of day to execute the process script)
      9:00 AM           = Start at 9:00 AM
    12:01 AM           = Start at midnight

    dailyEndTime      (Latest time of day to run)
       9:00 PM           = End at 9:00 PM
     11:59 PM           = End at midnight

    minsDelayBetweenRuns    (Delay between executions)
    60        = Run once per hour
      0        = No delay between runs; run as often as possible

    numRunsPerDay    (Limit of daily executions)
      4        = Run process 4 times per day
      0        = No limit on daily runs; run as many times as possible

    weekdaysToRun      (Days of week to execute)
    SuMoTuWeThFrSa    = Run every day of the week
    Sa                            = Run only on Saturdays
    MoWeFr                   = Run only on Mondays, Wednesdays, and Fridays

As an example, to run a script every weekday (not weekends) at noon, you'd use the following "Run Parameters":

    dailyStartTime*                  12:00 PM      Begin checking Run Parameters at this time
    dailyEndTime*                    12:30 PM      Stop checking Run Parameters at this time
    minsDelayBetweenRuns      0                  Don't specify a delay between runs
    numRunsPerDay                 1                  Execute only once per day
    weekdaysToRun                  MoTuWeThFr      Execute only on these days of the week

Or another example, to run a script every 4 hours every day, but not more than 4 times, you'd use the following "Run Parameters":

    dailyStartTime*                  6:00 AM      Begin checking Run Parameters at this time
    dailyEndTime*                    7:00 PM      Stop checking Run Parameters at this time
    minsDelayBetweenRuns      240            Specify a 240-minute delay between runs
    numRunsPerDay                 4                Execute four times per day
    weekdaysToRun                  SuMoTuWeThFrSa      Execute every day of the week

Click this link to download a zip file containing the following files:

    About Process_Runner.txt
    Process_Runner.app
    Sample_Process_1.plist
    Sample_Process_1.scpt
    Sample_Process_2.plist
    Sample_Process_2.scpt

______
*If another long-running process may encroach on these start or end times, make 'dailyEndTime' later to allow for it.

Getting Dimensions of EPS and JPEG
Raster Images Saved From Photoshop CS5
18 Dec 2012 12:15 PM

The above header is rather specific, but only because I've not taken time to try other file types or applications. Feel free to try this idea in other situations. You'll need to experiment a bit and tweak things to get it to do what you need.

I needed to determine X and Y pixel dimensions and the resolution (in pixels per inch) for EPS and JPEG image files. I decided to use the AppleScript read file command, so as to avoid opening the files in an application, such as Photoshop. After reading each file, its content is parsed using text manipulation to extract the desired dimensions.

The logic uses two variations of the read file command—with the using delimiter option, and without. With the using delimiter option, the delimiter can only be a single character, which is not terribly useful. However, in the case of EPS files, it works well enough to use the forward slash ("/") as the delimiter. For JPEG files, I chose to read the entire file, then split it myself using multiple delimiters, each containing multiple characters.

Here's the code:

        set {dimX, dimY, resX, resY} to {0.0, 0.0, 0.0, 0.0} -- default
        set f to open for access alias imagePath --HFS path to EPS or JPEG file
        if imagePath ends with ".eps" then
                set parts to read f using delimiter "/"
                close access f
                repeat with i from 1 to (length of parts)
                        set thisText to item i of parts
                        if thisText contains "<exif:PixelXDimension>" then
                                set dimX to getTextItem(thisText, {"<", ">"}, 4)
                        else if thisText contains "<exif:PixelYDimension>" then
                                set dimY to getTextItem(thisText, {"<", ">"}, 4)
                        else if thisText contains "<tiff:XResolution>" then
                                set resX to getTextItem(thisText, {"<", ">"}, -1) / 10000
                        else if thisText contains "<tiff:YResolution>" then
                                set resY to getTextItem(thisText, {"<", ">"}, -1) / 10000
                        end if
                        if 0.0 is not in {dimX, dimY, resX, resY} then exit repeat
                end repeat
        else if (imagePath ends with ".jpg") or (imagePath ends with ".jpeg") then
                set txt to read f
                close access f
                set AppleScript's text item delimiters to {" exif:", " tiff:"}
                set parts to text items 2 thru -2 of txt
                set AppleScript's text item delimiters to ""
                repeat with i from 1 to (length of parts)
                        set thisText to item i of parts
                        if thisText starts with "PixelXDimension=" then
                                set dimX to getTextItem(thisText, {"\""}, 2)
                        else if thisText starts with "PixelYDimension=" then
                                set dimY to getTextItem(thisText, {"\""}, 2)
                        else if thisText starts with "XResolution=" then
                                set resX to getTextItem(thisText, {"\"", "/"}, 2) / 10000
                        else if thisText starts with "YResolution=" then
                                set resY to getTextItem(thisText, {"\"", "/"}, 2) / 10000
                        end if
                        if 0.0 is not in {dimX, dimY, resX, resY} then exit repeat
                end repeat
        end if
        if resX is not resY then error "Asymmetrical image resolution." number 300 from {resX, resY}
        return {dimX, dimY, resX}

        on getTextItem(theText, theDelims, itemNumber)
                set AppleScript's text item delimiters to theDelims
                set textItem to text item itemNumber of theText
                set AppleScript's text item delimiters to ""
                return textItem
        end getTextItem
Obfuscation of Hard-Coded Data in an AppleScript
16 Nov 2012 11:00 AM

If you, like me, are hesitant to put hard-coded user IDs, passwords, or whatever into your scripts, this bit of obfuscation may be of interest. The bad news is that the information is still in the script and decipherable by an astute intruder. The good news is that the typical computer user will find the ciphertext stored in your scripts to be utterly mysterious and incomprehensible.

Let me state clearly that this method is one of "security by obscurity." It is NOT going to provide genuine security, so use it at your own risk. I suppose one could slightly improve the pseudosecurity of this technique by writing the ciphertext to a text file, thus separating the information from the deciphering handlers.

As to the methodology employed, the code is my own spin on the ROT18 cipher with some extra twists thrown in. One twist is that spaces, periods, commas, and question marks are enciphered, along with letters and numbers (making it ROT20, I guess). Other characters, such as parentheses, will appear in the ciphertext as is. Another twist is the breaking up of the text into five chunks (or three, if the text is short) and shuffling the order of those chunks. If the text is really short, the order of the characters is simply reversed. Like ROT 18, this logic is self-inverting. That is, the same logic both enciphers plaintext and deciphers ciphertext.

Maybe you won't use it for passwords. You and a friend could generate coded messages to email to back and forth. Anyway, for whatever good it is, here is the code. Enjoy!

        set theText to "Quick Brown Fox, 73, Jumps(?) Over Lazy Dog 129."
        set cipherText to applyCodec(theText)
        set plaintextAgain to applyCodec(cipherText)
        return {cipherText, plaintextAgain}

        on applyCodec(theText) -- mutant ROT20 retains case of plaintext
                set lowerCharSet to "abcdefghijklmnopqrstuvwxyz.,? 0123456789"
                set lowerCipherSet to "uvwxyz.,? 0123456789abcdefghijklmnopqrst"
                set upperCharSet to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                set upperCipherSet to "NOPQRSTUVWXYZABCDEFGHIJKLM"
                set cipherText to {}
                repeat with i from 1 to (length of theText)
                        set thisChar to character i of theText
                        considering case
                                if thisChar is in lowerCharSet then
                                        set x to offset of thisChar in lowerCharSet
                                        set end of cipherText to character x of lowerCipherSet
                                else if thisChar is in upperCharSet then
                                        set x to offset of thisChar in upperCharSet
                                        set end of cipherText to character x of upperCipherSet
                                else
                                        set end of cipherText to thisChar
                                end if
                        end considering
                end repeat
                return shuffle(cipherText as text)
        end applyCodec

        on shuffle(theText)
                set len to count of theText
                if len < 3 then
                        set shuffledChars to reverse of characters of theText
                else
                        set usingFifths to len > 29
                        if usingFifths then
                                set x to 5
                                set {a, b, c, d, e} to splitIntoFifths(len)
                        else -- using thirds
                                set x to 3
                                set {a, b, c} to splitIntoThirds(len)
                        end if
                        set shuffledChars to {}
                        set end of shuffledChars to reverse of characters (a + 1) thru (a + b) of theText
                        set end of shuffledChars to reverse of characters 1 thru a of theText
                        set end of shuffledChars to reverse of characters ¬
                                (a + b + 1) thru (a + b + c) of theText
                        if usingFifths then
                                set end of shuffledChars to reverse of characters ¬
                                        (a + b + c + d + 1) thru (a + b + c + d + e) of theText
                                set end of shuffledChars to reverse of characters ¬
                                        (a + b + c + 1) thru (a + b + c + d) of theText
                        end if
                end if
                return shuffledChars as text
        end shuffle

        on splitIntoFifths(n)
                set a to round (n / 5) rounding up
                set b to round ((n - a) / 4)
                set c to round (n - a - b) / 3
                set d to round (n - a - b - c) / 2
                set e to round (n - a - b - c - d)
                return {a, b, c, d, e}
        end splitIntoFifths

        on splitIntoThirds(n)
                set a to round (n / 3) rounding up
                set b to round ((n - a) / 2)
                set c to round (n - a - b)
                return {a, b, c}
        end splitIntoThirds
Checking an AppleScript for Unused and Undefined Variables
16 Jan 2012 06:00 PM

Thanks to a recent thread on the AppleScript User's mailing list, I learned of a very useful function in Smile, the free AppleScript development software from Satimage Software, available here. Smile is not just a script editor, it's a complex development and working environment. My ignorance shows when I say that I've never quite "made sense" of the Smile environment. But then, I've never needed the advanced capabilities found in Smile—until now.

The following script uses the functionality of Smile's validate command to check for both unused and undefined variables. It also uses the scriptability of Script Debugger, my editor of choice, to determine the line number of each problem's occurrence, making it easy to locate within the script window. [If you don't use Debugger, you should be able to rework the script for use with AppleScript Editor or Smile.]

I've added this script to Script Debugger's script menu so, when invoked, it will check the frontmost script window. The result is returned by a dialog box and, from there, can be copied to the clipboard or written to a text file.

        -- get script text
        tell application "Script Debugger 4.5"
                set {scriptName} to name of windows whose index is 1
                set scriptSource to script source of document of window scriptName
                set scriptFilePath to (file spec of document of window scriptName) as text
        end tell
        set dialogMessage to "Validate script \"" & scriptName & "\"?"
        displayDialog(dialogMessage, {"Cancel", "OK"}, 2, 1)
        tell application "System Events"
                set fileExt to name extension of alias scriptFilePath
        end tell
        set baseName to text 1 thru -((length of fileExt) + 2) of scriptName
        -- hide Smile and its windows
        tell application "System Events"
                if not (exists application process "Smile") then
                        activate application "Smile"
                end if
                set visible of application process "Smile" to false
        end tell
        -- do validation of script
        tell application "Smile"
                set background to true
                set scriptWindow to make new script window
                set text of scriptWindow to scriptSource
                validate scriptWindow
                set textWindow to last text window whose name is "Script errors: "
                set validationResult to text of textWindow -- validation result
                close textWindow saving no
                close scriptWindow saving no
        end tell
        -- determine affected line numbers
        set affectedLineNumbers to {}
        tid({"show window \"\" selection "})
        set blah to text items of validationResult
        tid("")
        repeat with i from 1 to (length of blah)
                set thisInstance to item i of blah
                if thisInstance starts with "{" then
                        tid(", ")
                        set x to text item 1 of (text 2 thru -1 of thisInstance)
                        tid("")
                        set scriptChunk to text 1 thru x of scriptSource
                        set end of affectedLineNumbers to length of (paragraphs of scriptChunk)
                end if
        end repeat
        set defaultAnswer to convertAnythingToString({validationResult:validationResult, ¬
                affectedLineNumbers:affectedLineNumbers})
        set action to button returned of displayDialogWithText("Validation result:", ¬
                defaultAnswer, {"Write to Text File", "OK"}, 2, 1)
        if action is "Write to Text File" then
                set filePath to ((path to desktop) as text)
                set fileRef to open for access ((filePath & "temp_validation.txt") ¬
                        as file specification) with write permission
                set eof of fileRef to 0
                write defaultAnswer to fileRef as «class utf8»
                close access fileRef
                tell application "System Events"
                        if exists alias (filePath & baseName & "_validation.applescript") then
                                delete alias (filePath & baseName & "_validation.applescript")
                        end if
                        set name of alias (filePath & "temp_validation.txt") to ¬
                                (baseName & "_validation.applescript")
                end tell
                set dialogMessage to "Text file \"" & baseName & ¬
                        "_validation.applescript\" is on the Desktop."
                displayDialog(dialogMessage, "OK", 1, 1)
        end if

        -- handlers follow
        on convertAnythingToString(something)
                if class of something is in {string, text, Unicode text} then
                        set listString to ("\"" & something & "\"")
                else if something is {} then
                        set listString to "{}"
                else if something is in {missing value, null, true, false} then
                        set listString to something as text
                else
                        try
                                something as integer
                                set listString to something as text
                        on error errText
                                set opening to offset of "{" in errText
                                set closing to offset of "}" in "" & (reverse of (characters of errText))
                                set listString to text opening thru -closing of errText
                        end try
                end if
                return listString
        end convertAnythingToString

        on displayDialog(dialogMessage, buttonList, defButton, iconNum)
                tell application "Script Debugger 4.5"
                        activate
                        with timeout of 17700 seconds
                                display dialog dialogMessage buttons buttonList ¬
                                        default button defButton with icon iconNum
                        end timeout
                end tell
        end displayDialog

        on displayDialogWithText(dialogMessage, defaultAnswer, buttonList, defButton, iconNum)
                tell application "Script Debugger 4.5"
                        activate
                        with timeout of 17700 seconds
                                display dialog dialogMessage default answer defaultAnswer ¬
                                        buttons buttonList default button defButton with icon iconNum
                        end timeout
                end tell
        end displayDialogWithText

        on tid(d)
                set AppleScript's text item delimiters to d
        end tid

This has found problems in my scripts, saving me some potential headaches. Enjoy!

Using 'load script' With Scripts Saved In Text Format
20 Jan 2011 06:30 PM

Often I use the 'load script' command to bring in the code from another script file, so its handlers are available in the current script. It works something like this:

         load script alias "MacHD:Users:stanc:repository:Some Project:Some Script.app"

This works great unless the script you want to load is saved in text format (with the ".applescript" extension). In that case, you get error number -1752 and the message "Script doesn’t seem to belong to AppleScript." Great! So now what? This is a problem for me, because I store all my AppleScript source code in text format, which makes things much easier for version control and dealing with absent applications.

Just use the following handler, which will attempt to load the script file normally and, if that fails, will read the file as text and turn that into the desired script object.

        on loadScript(scriptFileToLoad)
                set scriptFileToLoad to scriptFileToLoad as text -- to be safe
                try
                        set scriptObject to load script alias scriptFileToLoad
                on error number -1752 -- text format script
                        set scriptObject to run script ("script s" & return & ¬
                                (read alias scriptFileToLoad as «class utf8») & ¬
                                return & "end script " & return & "return s")
                end try
                return scriptObject
        end loadScript

Just pass the script's path to the handler as a string or alias. Like so:

        set scriptObject to loadScript("MacHD:Users:stanc:MyScript.applescript")

Hope you can use this little bit of trickery.

Happy New Year, 2011!
31 Dec 2010 09:30 AM

Our plan is to find more time in the coming year to devote to sharing some of our thoughts and knowledge about AppleScript and programming. Though time is never actually "found," improvements in organizing our schedule should free up some time from other relatively unimportant activities.

Greetings!
30 Aug 2010 02:00 PM

This is a fine example of minimalism, no? But we're just getting started (and we're slow, not to mention lazy), so don't hold your breath or anything. Okay?