February 1, 2016
Swiftly Idle
With the release of the open source Swift compiler, it becomes feasible to write Swift server-side apps for Linux. Server-side apps require two basic components, parallel processing (threading), and the ability to never exit.
While these two requirements are not scheduled to be fully ready for the Linux version of the compiler until Swift 3.0, I wanted to get ahead of the curve and start experimenting now. To do this I decided to design and create, using the Swift 2.2 open source compiler on OS X, an app I’m calling Courier.
Since Courier is intended to run constantly without consuming CPU resources I thought I’d share a code example that keeps the main thread in an idle state but allows the main thread to respond to dispatches from GCD. This fundamental ability is the basis for all servers and other types of monitoring apps…even GUI apps and OS’s.
Below is the code for a function I’m calling waitAndListen. It will setup listening for changes to one of the server’s directories. When a change is detected, it will report what happened.
To make this work, all the listening has to be set up early in the source code. To make the waitAndListen example easier to understand, I’ve moved the code for setting up listening into a function called watchDirectory. We’ll look at that function a little later.
1 func waitAndListen(){ 2 //call a custom function to watch for changes in the "stuff" directory. 3 let source = watchDirectory("stuff") 4 //setup the current thread's runloop in an infinite loop 5 //to keep the app going 6 while(true){ 7 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 3600.0, true) 8 print("looping") 9 } 10 }
Line 6 is where the good stuff starts. It is the beginning of an infinite loop. Don’t worry. It won’t soak up your CPU cycles. Line 7 makes sure of that. Line 7 is a special function call. What it does is make a OS blocking call that returns every hour (the second parameter) or when the ‘stuff’ directory is changed, which ever comes first. Until then it sits and waits. The first argument isn’t particularly important to this discussion, but the third parameter, a boolean, is used to indicate if CFRunLoopRunInMode should exit after it is interrupted. I’ve set it to true for this example to keep things simpler.
There are more complicated examples of using CFRunLoopRunInMode on the web. Those usually check the return value of CFRunLoopRunInMode to determine if the application should exit. Since I’m writing something that should never exit of its own choice, that kind of check is unneeded.
In short, lines 6 – 9 are all that is needed to create an application designed to never exit normally.
One nice thing about using CFRunLoopRunIn Mode to put the main thread into a waiting state is the main thread can still be used for computation. The watchDirectory function, called in line 3, makes the application to wake up the main thread and use it when changes are made to the ‘stuff’ directory. watchDirectory is longer and more complicated than the code in waitAndListen. We’ll take it a step at a time.
In line 2 below, a file descriptor is created. This descriptor refers to the ‘stuff’ directory and is used later as part of the code required to listen for changes to the ‘stuff’ directory.
Line 4 is where we get a reference to the main thread. If you don’t want to use the main thread for handling changes you can use GCD’s dispatch_get_global_queue instead. If you choose to use the global queue rather than the main thread, the main thread will never be wakened by events. Instead, the background thread will become active each time a change is made.
Grand Central Dispatch (GCD) is a dispatching engine. Line 5 adds a new, non-preexisting event source to GCD. The _VNODE constant tells GCD the type of events it should dispatch are file and directory events. The descriptor for the directory we want to watch is passed as the second parameter so GCD knows which directory to monitor.
The third parameter consists of a series of constants that are assembled using swift’s bitwise OR operator. The options used in this example sets up the dispatch source to include all types of file and directory events. You can restrict these to a smaller set if you choose. Lastly, the queue to dispatch to is passed in as the last parameter.
With a dispatch source added to GCD, GCD needs to be told what to do when a change is made to the directory being watching. That is done by adding the closure beginning on line 7 as a dispatch handler.
1 func watchDirectory(directoryPath: String) -> OS_dispatch_source{ 2 let directoryDescriptor = UInt(open(directoryPath, O_RDONLY)) 3 //attach the listener to the main thread. 4 let queue = dispatch_get_main_queue() 5 let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, directoryDescriptor, DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE | DISPATCH_VNODE_EXTEND, queue) 6 //assign a closure to be the listener for directory change events 7 dispatch_source_set_event_handler(dispatchSource){ 8 let eventType = dispatch_source_get_data(dispatchSource) 9 //detect the different types of changes and handle them 10 if((eventType & DISPATCH_VNODE_DELETE) == DISPATCH_VNODE_DELETE 11 || (eventType & DISPATCH_VNODE_REVOKE) == DISPATCH_VNODE_REVOKE 12 || (eventType & DISPATCH_VNODE_RENAME) == DISPATCH_VNODE_RENAME 13 ) 14 { 16 //handle removing and deleting 17 } 18 //when something is added or something is removed from this 19 //directory it is a WRITE 20 if((eventType & DISPATCH_VNODE_WRITE) == DISPATCH_VNODE_WRITE) 21 { 22 //handle a write 23 } 24 } 25 dispatch_source_set_cancel_handler(dispatchSource){ 26 close(Int32(directoryDescriptor)) 27 } 28 dispatch_resume(dispatchSource) 29 return dispatchSource 30 }
Lines 7 through 24 consists of the code GCD executes each time a change is made to the ‘stuff’ directory. The example above varies its behavior depending on the type of event. It handles deletions and renamings in one way and adding files and directories in another. Before it can handle the events differently, the code must determine the type of event that happened. To do this, the dispatch_get_source_data function (line 8) is called each time a change is made.
Once eventType is set to the type of event that happened, the example uses if conditional statements to handle the various types of events. The event could be directory or file creation, deletions, renaming, etc. You can see the if statements for this example on lines 10 and 20.
There are a bunch of events associated with each if statement. Since I’m preparing to run this code on both OS X and Linux, I’ll need to experiment with the event types on Linux before I can decide exactly how to break up the types across my applications’s statements. I’m assuming there will be differences in how Linux handles these events compared to OS X.
If you want to cancel watching the directory, the dispatch_source_set_cancel_handler must be called. The example shows that function cleaning up the file descriptor that refers to the directory.
Line 28 is very important. If you don’t resume/restart the dispatching engine, no events will ever be captured and dispatched to the listener you’ve areated. In other words, your code for handling the changes to the directory won’t be called when the directory is changed.
Overall the API for watching a directory or file isn’t too bad.
Now….back to creating Courier. 🙂