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-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.
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:
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:
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:
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 :)
The author does not allow comments to this entry
Comments
Display comments as Linear | Threaded