teaching machines

Running a Mac Bundle Synchronously

December 16, 2013 by . Filed under code, public.

My holiday wishlist is pretty simple this year:

  1. I want to be able to run Mac bundles from the Terminal, such that standard in and out stay in Terminal and that the application window is frontmost.

OS X provides the utility open, but open runs the bundle asynchronously, and we lose standard in and standard out from Terminal. Historically, standard out used to go to Console.app, but that’s not true anymore. Alternatively, we can run the executable inside the bundle directly to retain standard in and out, but then the application window is not brought to the foreground.

Instead of waiting for a myth to come and fix the operating system for me, I took matters into my own hands. Here’s a Ruby script that replaces the open utility, but keeps standard in and standard out in the Terminal and puts the application window in front:

#!/usr/bin/env ruby

# ---------------------------------------------------------------------------- 
# FILE:   runx                                                                 
# AUTHOR: Chris Johnson                                                        
# DATE:   Dec 16 2013                                                          
#                                                                              
# A script for starting a GUI app in Mac OS X from Terminal. Compared to the
# traditional "open myapp.app", this script runs the app synchronously, keeps
# its output and input in the terminal, and foregrounds the application
# window.
#
# Usage:
#   runx path/to/app/bundle.app [args]
# ---------------------------------------------------------------------------- 

require 'pathname'

DELAY_BETWEEN_CHECKS = 1.0

if ARGV.length == 0 || ARGV[0] !~ /\.app$/
  puts "Usage: #{$0} path/to/app/bundle.app [args]"
  exit 1
end

app = ARGV.shift
exe = Pathname.new(app).basename.sub_ext('').to_s

foreground_script = <<EOF
-- Activate the application to bring it into the foreground. Returns "yes" if
-- successful and "no" otherwise.

-- A blind activate call causes the application to open if it's not opened
-- yet. We only want to communicate with an instance of the application that's
-- already open.
if application "#{app}" is running then
  try
    tell application "#{app}" to activate
    return "yes"

  -- Sadly, even if the application is open, it may not be ready to respond
  -- to activate. I was seeing these errors occasionally: "execution error:
  -- #{app} got an error: Application isn't running. (-600)" even if the
  -- condition above was met.
  on error message number errno
    return "no"
  end try
else
  return "no"
end if
EOF

# Make a side process that waits for the application to be foregroundable. Once
# it is, let's foreground it and be done.
fork do
  is_foregrounded = false

  # If the application's process died for some reason, we became a zombie. Our
  # parent process is init, which has pid 1. There's no point in continuing if
  # the parent is dead.
  while Process.ppid != 1 && !is_foregrounded
    sleep DELAY_BETWEEN_CHECKS
    IO.popen('osascript', 'r+') do |io|
      io.write(foreground_script)
      io.close_write
      is_foregrounded = io.gets.chomp == 'yes'
    end
  end
end

# Replace this process with the application. We reach inside the bundle and
# invoke the executable directly so that stdout and stdin come from the
# terminal. If we ran "open $app" instead, the process would detach from
# the terminal.
exec "#{app}/Contents/MacOS/#{exe} #{ARGV.join(' ')}"