Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

NSTask Tutorial for OS X

$
0
0
See a practical example of using NSTask!

See a practical example of using NSTask!

Update note: This NSTask Tutorial for OS X has been updated to Swift by Warren Burton. The original tutorial was written by Andy Pereira.

Your Mac has a fully-fledged version of UNIX as its operating system, which means it has a massive amount of pre-installed command line utilities and scripting languages. Swift, Perl, Python, Bash, Ruby, plus anything you can install. Wouldn’t it be nice to use to that power to create awesome apps?

NSTask allows you to execute another program on your machine as a subprocess and monitor its execution state while your main program continues to run. For example, you could run the ls command to display a directory listing — from right inside your app!

A good analogy for NSTask is a parent-child relationship. A parent can create a child and tell it to do certain things, and (theoretically) the child must obey. NSTask behaves in a similar fashion; you start a “child” program, give it instructions and tell it where to report any output or errors. Better yet — it’s way more obedient than your average toddler :]

A great use for NSTask is to provide a front-end GUI to command line programs. Command line programs are powerful, but they require you to remember exactly where they live on your machine, how to call them and what options or arguments you can provide to the program. Adding a GUI on the front end can provide the user with a great deal of control — without having to be a command line guru!

This tutorial includes an NSTask example that shows you how to execute a simple command program with arguments and display its standard output as it runs in a text field. By the end, you’ll be ready to use NSTasks in your own apps!

Note: This tutorial assumes you have some basic familiarity with Mac OS X development and the Terminal. If you are completely new to programming for the Mac, check out our beginning Mac OS X development tutorial series.

Getting Started

To keep the focus squarely on NSTask, I’ve created a starter project for you that contains a basic user interface. Download the project, open it in Xcode, and build and run.

The starter app has one window, as shown:

initial_view

This window has the title “TasksProject”. It has a simple GUI that will, by invoking a shell script, let you build an iOS project, create an ipa and observe what is happening.

Creating Your First NSTask

The NSTask example will be to build and package an iOS app into an ipa file by using NSTask to run some command line tools in the background. Most of the basic UI functionality is in place — your job is do the heavy lifting with NSTask.

Note: It’s recommended that you have an iOS Developer account with Apple, as you’ll need the proper certificates and provisioning profile to create an ipa file that can be installed on one of your devices. Don’t worry if you don’t, though, as you’ll be able to follow the entire tutorial even without an account.

You are now going to work on the embedded View Controller titled “Tasks View Controller”. The first section in the window asks the user to select an Xcode project directory. To save time, rather than having to select a directory manually every time you run this app while testing, you’ll hard-code it to one of your own Xcode project directories.

To do this, head back to XCode and open TasksViewController.swift. Take a look at the properties and methods under the comment “Window Outlets”:

//View Controller Outlets
@IBOutlet var outputText:NSTextView!
@IBOutlet var spinner:NSProgressIndicator!
@IBOutlet var projectPath:NSPathControl!
@IBOutlet var repoPath:NSPathControl!
@IBOutlet var buildButton:NSButton!
@IBOutlet var targetName:NSTextField!

All of these properties correspond to the Tasks View Controller Scene in Main.storyboard. Notice the projectPath property — this is the one you want to change.

Open Main.storyboard and click on the Project Location item. You’ll find it 4 levels deep in the object hierarchy; it’s a child of the Stack View. In the Attributes Inspector, under Path Control, find the Path element:

set_path_1b

Set Path to a directory on your machine that contains an iOS project. Make sure you use the parent directory of a project, not the .xcodeproj file itself.

Note: If you don’t have any iOS projects on your machine, download a simple project here and unzip it to a location on your machine. Then set the Path property in your application using the instructions above. For example, if you unzip the package on your desktop, you would set Path to
/Users/YOUR_USERNAME_HERE/Desktop/SuperDuperApp
.

Now that you have a default source project path in your app to facilitate testing, you will also need a default destination path. Open Main.storyboard and click on the Build Repository item.

In the Attributes Inspector, find Path item under Path Control:

set_path_2b

Set the Path entry to a directory on your machine that’s easy to find, like the Desktop. This is where the .ipa file created by this application will be placed.

There are two additional fields in the Tasks View Controller Scene you need to know about: the Target Name and an associated text field.

initial_view_2

  1. Target Name is designated for the name of the iOS Target you want to build.
  2. The text area below Target Name will display output from the NSTask object in your project as it runs.

Don’t know the target name of your iOS project? To find it, select your project in Xcode’s project navigator and look under TARGETS in the Info tab. The screenshot below shows where to find this for a sample project called “SuperDuperApp”:

target_name

Remember the target name — you’ll be entering it into the running app later on.

Let’s start fleshing out the bits of code that will run when the “Build” button is pressed.

Preparing the Spinner

Open TasksViewController.swift and add the following code to startTask:

//1.
outputText.string = ""
 
if let projectURL = projectPath.URL, let repositoryURL = repoPath.URL {
 
  //2. we can use ! safely here because we know the URL exists
  let projectLocation = projectURL.path!
  let finalLocation = repositoryURL.path!
 
  //3.
  let projectName = projectURL.lastPathComponent!
  let xcodeProjectFile = projectLocation + "/\(projectName).xcodeproj"
 
  //4.
  let buildLocation = projectLocation + "/build"
 
  //5.
  var arguments:[String] = []
  arguments.append(xcodeProjectFile)
  arguments.append(targetName.stringValue)
  arguments.append(buildLocation)
  arguments.append(projectName)
  arguments.append(finalLocation)
 
  //6. 
  buildButton.enabled = false
  spinner.startAnimation(self)
 
}

Here’s a step-by-step explanation of the code above:

  1. outputText is the large text box in the window; it will contain all the output from the script that you will be running. If you run the script multiple times, you’ll want to clear it out between each run, so this first line sets the string property (contents of the text box) to an empty string.
  2. The projectURL and repositoryURL objects are NSURL objects, and this gets the string representations of these objects in order to pass them as arguments to your NSTask.
  3. By convention, the name of the folder and the name of the project file are the same. Getting the lastPathComponent property of the project folder contained in projectURL and adding an “.xcodeproj” extension gets the path to the project file.
  4. Defines the subdirectory where your task will store intermediate build files while it’s creating the ipa file as build.
  5. Stores the arguments in an array. This array will be passed to NSTask to be used when launching the command line tools to build your .ipa file.
  6. Disables the “Build” button and starts a spinner animation.

Why disable the “Build” button? The NSTask will run each time the button is pressed, and as the app will be busy for an amount of time while the NSTask does its work, the user could impatiently press it many times — each time spawning a new build process. This action prevents the user from creating button click events while the app is busy.

Build and run your application, then hit the Build button. You should see the “Build” button disable and the spinner animation start:

busy1

Your app looks pretty busy, but you know right now it’s not really doing anything. Time to add some NSTask magic.

Adding an NSTask to TasksProject

Open TasksViewController.swift and add the following method:

func runScript(arguments:[String]) {
 
  //1.
  isRunning = true
 
  let taskQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
 
  //2.
  dispatch_async(taskQueue) {
 
    //TESTING CODE
 
    //3.
    NSThread.sleepForTimeInterval(2.0)
 
    //4.
    dispatch_async(dispatch_get_main_queue(), {
      self.buildButton.enabled = true
      self.spinner.stopAnimation(self)
      self.isRunning = false
 
    })
 
   //TESTING CODE
 
  }
 
}

If you look at the method step-by-step, you’ll see that the code does the following:

  1. Sets isRunning to true. This enables the Stop button, since it’s bound to the TasksViewController‘s isRunning property via Cocoa Bindings. You want this to happen on the main thread.
  2. Uses dispatch_async to run the heavy lifting on a background thread. The application will continue to process things like button clicks on the main thread, but the NSTask will run on the background thread until it is complete.
  3. This is a temporary line of code that causes the current thread to sleep for 2 seconds, simulating a long-running task.
  4. Once the job has finished, re-enables the Build button, stops the spinner animation, and sets isRunning to false which disables the “Stop” button. This needs to be done in the main thread, as you are manipulating UI elements.

Now that you have a method that will run your task in a separate thread, you need to call it from somewhere in your app.

Still in TasksViewController.swift, add the following code to the end of startTask just after spinner.startAnimation(self):

runScript(arguments)

This calls runScript with the array of arguments you built in startTask.

Build and run your application and hit the Build button. You’ll notice that the Build button will become disabled, the Stop button will become enabled and the spinner will start animating:

busy2

While the spinner is animating, you’ll still be able to interact with the application. Try it yourself — for example, you should be able to type in the Target Name field while the spinner is active.

After two seconds have elapsed, the spinner will disappear, Stop will become disabled and Build will become enabled.

Note: If you have trouble interacting with the application before it’s done sleeping, increase the number of seconds in your call to sleepForTimeInterval.

Now that you’ve solved the UI responsiveness issues, you can finally implement your call to NSTask.

In TasksViewController.swift, find the lines in runScript that are bracketed by the comment //TESTING CODE. Replace that entire section of code inside the dispatch_async(taskQueue) block with the following:

//1.
guard let path = NSBundle.mainBundle().pathForResource("BuildScript",ofType:"command") else {
  print("Unable to locate BuildScript.command")
  return
}
 
//2.
self.buildTask = NSTask()
self.buildTask.launchPath = path
self.buildTask.arguments = arguments
 
//3.
self.buildTask.terminationHandler = {
 
  task in
  dispatch_async(dispatch_get_main_queue(), {
    self.buildButton.enabled = true
    self.spinner.stopAnimation(self)
    self.isRunning = false
  })
 
}
 
//TODO Output Handling
 
//4.
self.buildTask.launch()
 
//5.
self.buildTask.waitUntilExit()

The above code:

  1. Gets the path to a script named BuildScript.command, included in your application’s bundle. That script doesn’t exist right now — you’ll be adding it shortly.
  2. Creates a new NSTask object and assigns it to the TasksViewController‘s buildTask property. The launchPath property is the path to the executable you want to run. Assigns the BuildScript.command‘s path to the NSTask‘s launchPath, then assigns the arguments that were passed to runScript:to NSTask‘s arguments property. NSTask will pass the arguments to the executable, as though you had typed them into terminal.
  3. NSTask has a terminationHandler property that contains a block which is executed when the task is finished. This updates the UI to reflect that finished status as you did before.
  4. In order to run the task and execute the script, calls launch on the NSTask object. There are also methods to terminate, interrupt, suspend or resume an NSTask.
  5. Calls waitUntilExit, which tells the NSTask object to block any further activity on the current thread until the task is complete. Remember, this code is running on a background thread. Your UI, which is running on the main thread, will still respond to user input.

Build and run your project; you won’t notice that anything looks different, but hit the Build button and check the output console. You should see an error like the following:

Unable to locate BuildScript.command

This is the log from the guard statement at the start of the code you just added. Since you haven’t added the script yet, the guard is triggered.

Looks like it’s time to write that script! :]

Writing a Build Shell Script

In Xcode, choose File\New\File… and select the Other category under OS X. Choose Shell Script and hit Next:

script1

Name the file BuildScript.command. Before you hit Create, be sure TasksProject is selected under Targets, as shown below:

target_choice

Open BuildScript.command and add the following commands at the end of the file:

echo "*********************************"
echo "Build Started"
echo "*********************************"
 
echo "*********************************"
echo "Beginning Build Process"
echo "*********************************"
 
xcodebuild -project "${1}" -target "${2}" -sdk iphoneos -verbose CONFIGURATION_BUILD_DIR="${3}"
 
echo "*********************************"
echo "Creating IPA"
echo "*********************************"
 
/usr/bin/xcrun -verbose -sdk iphoneos PackageApplication -v "${3}/${4}.app" -o "${5}/app.ipa"

This is the entire build script that your NSTask calls.

The echo commands that you see throughout your script will send whatever text is passed to them to standard output, which you capture as part of the return values from your NSTask object and display in your outputText field. echo statments are handy statements to let you know what your script is doing, since many commands don’t provide much, if any, output when run from the command line.

You’ll notice that besides all of the echo commands, there are two other commands: xcodebuild, and xcrun.

xcodebuild builds your application, creates an .app file, and places it in the subdirectory /build. Recall that you created an argument that references this directory way back in startTask, since you needed a place for the intermediate build files to live during the build and packaging process.

xcrun runs the developer tools from the command line. Here you use it to call PackageApplication, which packages the .app file into an .ipa file. By setting the verbose flag, you’ll get a lot of details in the standard output, which you’ll be able to view in your outputText field.

In both the xcodebuild and xcrun commands, you’ll notice that all of the arguments are written “${1}” instead of $1. This is because the paths to your projects may contain spaces. To handle that condition, you must wrap your file paths in quotes in order to get the right location. By putting the paths in quotes and curly braces, the script will properly parse the full path, spaces and all.

What about the other parts of the script, the parts that Xcode automatically added for you? What do they mean?

The first line of the script looks like this:

#!/bin/sh

Although it looks like a comment since it’s prefixed with #, this line tells the operating system to use a specific shell when executing the remainder of the script. It’s called a shebang. The shell is the interpreter that runs your commands, either in script files or from a command line interface.

There are many different shells available, but most of them adhere to some variation of either Bourne shell syntax or C shell syntax. Your script indicates that it should use sh, which is one of the shells included with OS X.

If you wanted to specify another shell to execute your script, like bash, you would change the first line to contain the full path to the appropriate shell executable, like so:

#!/bin/bash

In scripts, any argument you pass in is accessed by a $ and a number. $0 represents the name of the program you called, with all arguments after that referenced by $1, $2 and so forth.

Note: Shell scripts have been around for about as long as computers, so you’ll find more information than you’ll ever want to know about them on the Internet. For a simple (and relevant) place to start, check out Apple’s Shell Scripting Primer.

Now you’re ready to start calling your script from NSTask, right?

Not quite. At this point, your script file doesn’t have execute permissions. That is, you can read and write the file, but you can’t execute it.

This means if you build and run right now, your app will crash when you hit the Build button. Try it if you like. It’s not a big deal while developing, and you should see the exception “launch path not accessible” in your Xcode console.

To make it executable, navigate to your project directory in Terminal. Terminal defaults to your Home directory, so if your project is in your Documents directory, you would type the command:

cd Documents/TasksProject

If your project is in another directory besides “Documents/TasksProject”, you’ll need to enter the correct path to your project folder. To do this quickly, click and drag your project folder from the Finder into Terminal. The path to your project will magically appear in the Terminal window! Now simply move your cursor to the front of that path, type cd followed by a space, and hit enter.

To make sure you’re in the right place, type the following command into Terminal:

ls

Check that BuildScript.command in the file listing produced. If you’re not in the right place, check that you’ve correctly entered your project directory in Terminal.

Once you’re assured that you’re in the correct directory, type the following command into Terminal:

chmod +x BuildScript.command

The chmod command changes the permissions of the script to allow it to be executed by your NSTask object. If you try to run your application without these permissions in place, you’d see the same “Launch path not accessible” error as before. You only need to do this once for each new script that you add to your project.

Note: Using scripts like this is simple if you are developing for yourself or shipping your app outside the Mac App Store (MAS), however when developing for the MAS the sandbox rules that apply to your app are inherited by your scripts and you’ll need to use more complex techniques to use command line programs. These techniques are beyond the scope of this tutorial. See the links at the end for more details.

Clean and run your project; the “clean” is necessary as Xcode won’t pick up on the file’s permissions change, and therefore won’t copy it into the build repository. Once the application opens up, type in the target name of your test app, ensure the “Project Location” and “Build Repository” values are set correctly, and finally hit Build.

When the spinner disappears, you should have a new .ipa file in your desired location. Success!

Using Outputs

Okay, you’re pretty well versed in passing arguments to command line programs, but what about dealing with the output of these command line programs?

To see this in action, type the following command into Terminal and hit Enter:

date

You should see a message produced that looks something like this:

Fri 19 Feb 2016 17:48:15 GMT

date tells you the current date and time. Although it isn’t immediately obvious, the results were sent back to you on a channel called standard output.

Processes generally have three default channels for input and output:

  • standard input, which accepts input from the caller;
  • standard output, which sends output from the process back to the caller; and
  • standard error, which sends errors from the process back to the caller.

Pro tip: you’ll see these commonly abbreviated as stdin, stdout, and stderr.

There is also a pipe that allows you to redirect the output of one process into the input of another process. You’ll be creating a pipe to let your application see the standard output from the process that NSTask runs.

To see a pipe in action, ensure the volume is turned up on your computer, then type the following command in Terminal:

date | say

Hit enter and you should hear your computer telling you what time it is.

Note: The pipe character “|” on your keyboard is usually located on the forward slash \ key, just above the enter/return key.

Here’s what just happened: you created a pipe that takes the standard output of date and redirects it into the standard input of say. You can also provide options to the commands that communicate with pipes, so if you would like to hear the date with an Australian accent, type the following command instead:

date | say -v karen

Its the date, mate!

You can construct some rather long chains of commands using pipes, redirecting the stdout from one command into the stdin of another. Once you get comfortable using stdin, stdout, and pipe redirects, you can do some really complicated things from the command line using tools like pipes.

Now it’s time to implement a pipe in your app.

Open TasksViewController.swift and replace the comment that reads // TODO: Output Handling in runScript with the following code:

self.captureStandardOutputAndRouteToTextView(self.buildTask)

Next, add this function to TasksViewController.swift:

func captureStandardOutputAndRouteToTextView(task:NSTask) {
 
  //1.
  outputPipe = NSPipe()
  task.standardOutput = outputPipe
 
  //2.
  outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
 
  //3.
  NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification, object: outputPipe.fileHandleForReading , queue: nil) {
 
    notification in
 
    //4.
    let output = self.outputPipe.fileHandleForReading.availableData
    let outputString = String(data: output, encoding: NSUTF8StringEncoding) ?? ""
 
    //5.
    dispatch_async(dispatch_get_main_queue(), {
      let previousOutput = self.outputText.string ?? ""
      let nextOutput = previousOutput + "\n" + outputString
      self.outputText.string = nextOutput
 
      let range = NSRange(location:nextOutput.characters.count,length:0)
      self.outputText.scrollRangeToVisible(range)
 
    })
 
    //6.
    self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
  }
}

This function collects the output from the external process and adds it to the GUI’s outputText field. It works as follows:

  1. Creates an NSPipe and attaches it to buildTask‘s standard output. NSPipe is a class representing the same kind of pipe that you created in Terminal. Anything that is written to buildTask‘s stdout will be provided to this NSPipe object.
  2. NSPipe has two properties: fileHandleForReading and fileHandleForWriting. These are NSFileHandle objects. Covering NSFileHandle is beyond the scope of this tutorial, but the fileHandleForReading is used to read the data in the pipe. You call waitForDataInBackgroundAndNotify on it to use a separate background thread to check for available data.
  3. Whenever data is available, waitForDataInBackgroundAndNotify notifies you by calling the block of code you register with NSNotificationCenter to handle NSFileHandleDataAvailableNotification.
  4. Inside your notification handler, gets the data as an NSData object and converts it to a string.
  5. On the main thread, appends the string from the previous step to the end of the text in outputText and scrolls the text area so that the user can see the latest output as it arrives. This must be on the main thread, like all UI and user interaction.
  6. Finally, repeats the call to wait for data in the background. This creates a loop that will continually wait for available data, process that data, wait for available data, and so on.

Build and run your application again; make sure the Project Location and Build Repository fields are set correctly, type in your target’s name and click Build.

You should see the output from the building process in your outputText field:

last_view

Stopping an NSTask

What happens if you start a build, then change your mind? What if it’s taking too long, or something else seems to have gone wrong and it’s just hanging there, making no progress? These are times when you’ll want to be able to stop your background task. Fortunately, this is pretty easy to do.

In TasksViewController.swift, add the following code to stopTask:

if isRunning {
  buildTask.terminate()
}

The code above simply checks if the NSTask is running, and if so, calls its terminate method. This will stop the NSTask in its tracks.

Build and run your app, ensure all fields are configured correctly and hit the Build button. Then hit the Stop button before the build is complete. You’ll see that everything stops and no new .ipa file is created in your output directory.

Where to Go From Here?

Here is the finished NSTask example project from the above tutorial.

Congratulations, you’ve begun the process of becoming an NSTask ninja!

In one short tutorial, you’ve learned:

  • How to create NSTasks with arguments and output pipes; and
  • How to create a shell script and call it from your app!

To learn more about NSTask, check out Apple’s official NSTask Class Reference.

To learn about using command line programs in a sandboxed app see Daemons and Services Programming Guide and XPC Services API Reference.

This tutorial only dealt with working with stdout with NSTask, but you can use stdin and stderr as well! To practice your new skills, try working with these.

I hope you enjoyed this NSTask tutorial and that you find it useful in your future Mac OS X apps. If you have any questions or comments, please join the forum discussion below!

The post NSTask Tutorial for OS X appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4384

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>