Skip to content

Starting CLI Services In Sequence

Running a couple of scripts in a row is no problem for Bash, but running them in parallel is more tricky. It gets even more ridiculous when on script needs to reach a certain state before another can start. This post illustrates how I solved this work using AppleScript.

The Problem

Before we get to the solution, let's look at the problem first. The concrete task at hand (rather sequence thereof) looks like this:

  • open an SSH tunnel, let's call it Fred
  • open an SSH tunnel through Fred, let's call it Bob
  • download a couple of things through Bob
  • start a service that logs to the TTY
  • invoke a client that does something with the service,

Every task is dependent upon the one previous to it. It's a classic, linear dependency chain. Were these tasks finite, a bash script could look as simple as

./call-fred.sh
./call-bob.sh
./download-stuff.sh
./start-server.sh
./run-client.sh

But since, with the exception of download-stuff and run-client, everything is a service, the processes never end, so the next line of the bash file would never be invoked. You could "daemonize" the tasks, so they run in the background. But in the case of call-bob (where I get a remote shell) and start-server (where I get a local console) daemonizing wouldn't do much good.

Meet AppleScript

Luckily Apple has automation features built into OSX. For applications that expose themselves to this API all sorts of "funky" things are made possible. For iTerm this means that we can control the windows themselves, throw keyboard events at the terminal windows and more. They are scriptable through AppleScript - a language that does not only sound wrong, but looks like it was designed for three year olds. Have a look at this example.applescript for the basic boilerplate.

NOTE the ¬ character is the "ignore this line-break" escape like \ is in bash.

#!/usr/bin/osascript

tell application "iTerm"
  tell the current terminal
    activate current session
   
    -- required to leave the session occupied by osascript
    tell application "System Events" to keystroke "d" ¬
      using {command down, shift down}
    delay 0.5

    tell the last session
      -- run `echo 'Hello World'`
      write text "echo 'Hello World'"
    end tell
   
    -- can be repeated as often as you want
    tell application "System Events" to keystroke "d" ¬
      using {command down, shift down}
    delay 0.5

    tell the last session
      write text "echo 'Oh look, a Split Pane!'"
    end tell

  end tell
end tell

What this example doesn't show is that opening the third split pane will not wait for the first echo to finish. This is happening in parallel. But since we need to make sure that the preceding task is in a certain state, we can use the following snippet to block until we're certain we can continue:

#!/usr/bin/osascript

tell application "iTerm"
  tell the current terminal
    activate current session
   
    -- required to leave the session occupied by osascript
    tell application "System Events" to keystroke "d" ¬
      using {command down, shift down}
    delay 0.5

    tell the last session
      -- run `./call-fred.sh`
      write text "/bin/bash /path/to/call-fred.sh"
      repeat while contents does not contain ¬
        "fred is ready for business"
        delay 0.01
      end repeat
      -- won't reach this line before "fred is ready for business"
      -- was printed to the terminal
    end tell
   
  end tell
end tell

Should I Repeat Myself? has some more example loops. That doesn't look very sexy, but it gets the job done. To make the thing a bit more readable, we can introduce functions:

#!/usr/bin/osascript

tell application "iTerm"
  tell the current terminal
    activate current session
   
    -- required to leave the session occupied by osascript
    my addSplitPane()
    my runAndWait( ¬
      "/bin/bash /path/to/call-fred.sh", ¬
      "fred is ready for business" ¬
    )

  end tell
end tell

on addSplitPane()
  tell application "iTerm"
    tell the current terminal
      activate
      tell application "System Events" to keystroke "d" ¬
        using {command down, shift down}
      delay 0.5
    end tell
  end tell
end addSplitPane

on runAndWait(command, response)
  tell application "iTerm"
    tell the current terminal
      activate
      tell the last session
        write text command
        repeat while contents does not contain response
          delay 0.01
        end repeat
      end tell
    end tell
  end tell
end runAndWait

I had hoped that you could save a session to a variable and replace the tell the last session with tell mySessionThing to interleave concurrent tasks. Well, you can, but a session refers to something that is not the split pane. To cover that I'd have had to send keyboard commands to navigate the split panes - making it necessary to know which one your in. That sounded like a very dumb idea, so I dropped the little optimization and ended up with simply opening new sessions instead:

#!/usr/bin/osascript

tell application "iTerm"
  tell the current terminal
    activate current session
   
    -- open SSH tunnels
    my addSplitPane()
    my runAndWait( ¬
      "/bin/bash /path/to/call-fred.sh", ¬
      "fred is ready for business" ¬
    )
    my addSplitPane()
    my runAndWait( ¬
      "/bin/bash /path/to/call-bob.sh", ¬
      "bob here, hello?" ¬
    )

    -- update code base
    launch session "Default"
    delay 0.5
    tell the last session
      write text "/bin/bash /path/to/update.sh"
    end tell

    -- start server (in parallel to code base update)
    launch session "Default"
    delay 0.5
    my runAndWait( ¬
      "/bin/bash /path/to/update-server.sh", ¬
      "[INFO] BUILD SUCCESS" ¬
    )
    my runAndWait( ¬
      "/bin/bash /path/to/start-server.sh", ¬
      "State Change from INITIALIZING to ACTIVE" ¬
    )

    -- do something with server
    launch session "Default"
    delay 0.5
    tell the last session
      write text "/bin/bash /path/to/client.sh"
    end tell

  end tell
end tell

on addSplitPane()
  tell application "iTerm"
    tell the current terminal
      activate
      tell application "System Events" to keystroke "d" ¬
        using {command down, shift down}
      delay 0.5
    end tell
  end tell
end addSplitPane

on runAndWait(command, response)
  tell application "iTerm"
    tell the current terminal
      activate
      tell the last session
        write text command
        repeat while contents does not contain response
          delay 0.01
        end repeat
      end tell
    end tell
  end tell
end runAndWait

And run the thing with osascript the-file.applescript && exit. The exit part is required for the (successfully) finished script runner to go away. Only if there was an error, would the output of osascript remain visible.

This may not seem like much, but in this particular case I simply boot my machine and 10 minutes later have an up-to-date and fully initialized system to work with. I'm looking forward to a new morning routine :)

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry