Skip to content

Getting Started

Cătălin Stan edited this page Dec 4, 2019 · 5 revisions

This section covers creating a standalone background app (a launchd daemon) using Criollo.

Step 1: The Project

TL;DR; You can just proceed to get the skeleton project from here.

Since the final build product is an OS X application, we will start with a Cocoa Application template project.[^An Xcode project template is in the pipeline, but I really haven’t gotten around to it.] The demo below is in Objective-C but it applies to Swift as well.

Check out the HelloWorld-Swift and HelloWorld-ObjC examples for a basic project.

Open Xcode and Create a new Cocoa Application

This will give us a really good starting point. None of the options should be selected.

New project - Cocoa Application Details

Cleanup the Project

There is some ‘excess fat’ in the standard cocoa application template - which we really don’t need. So, let’s get rid of it. Delete Assets.xcassets and MainMenu.xib. We are not really using those. Of course you can leave Assets.xcassets.

Step 2: Adding Criollo

Once the basic cleanup of the project is done, we must add Criollo and configure the project to use it.

CocoaPods

Open Terminal and cd to your project folder, and type in

pod init

This will create a file called Podfile. Open it in your favourite text editor and modify it so that it looks like this:

platform :osx, '10.10'
use_frameworks!
target ‘HelloWorld’ do
    pod 'Criollo', '~> 0.5
end

After that is done, in Terminal, run:

pod install

This will install Criollo and its dependencies and create a .xcworkspace bundle. You should use this file and not the .xcodeproj file.

Open the workspace in Xcode, and build.

Principal Class - Info.plist

We have to make a small change to the info.plist file. Normally, the Principal Class of a Cocoa application is NSApplication. NSApplication takes care of a bunch of stuff like setting up a RunLoop, connecting to the graphics server, handling application events etc.

In the case of a Criollo application, these tasks are performed by the CRApplication class. We must tell the bundle that its Principal Class is CRApplication.

Here’s how to do that:

  1. Open HelloWorld.xcworkspace in Xcode, then locate and select the Info.plist file, in the Project navigator.
  2. In the editor window, locate the key labeled Principal class (or NSPrincipalClass if you’re showing raw key/values).
  3. Replace NSApplication with CRApplication.

CRApplicationMain - The Entry Point

A typical Cocoa application will call the NSApplicationMain function to set everything up. A Criollo application does this with the CRApplicationMain function. This is located in the main.m file.

A typical main.m file looks like this:

#import <Cocoa/Cocoa.h>

int main(int argc, const char * argv[]) {
    return NSApplicationMain(argc, argv);
}

In a Criollo app, the main.m file should look like this, assuming that AppDelegate is the application delegate class.

#import <Criollo/Criollo.h>
#import "AppDelegate.h"

int main(int argc, const char * argv[]) {
    return CRApplicationMain(argc, argv, [AppDelegate new]);
}

Eagle-eyed readers will observe that CRApplicationMain has one extra parameter. As you have guessed, this is the application delegate. Feel free to check out the code of CRApplicationMain.

Edit the main.m file and make the changes as indicated above.

At this point Xcode will display a warning about assigning AppDelegate * to id<CRApplicationDelegate>. We’ll take care of that next.

The AppDelegate

As in the case of Cocoa, Criollo defines the CRApplicationDelegate protocol[^Again apologies for the lack of documentation].

AppDelegate.h

The file should look like this:

#import <Criollo/Criollo.h>

@interface AppDelegate : NSObject <CRApplicationDelegate>

@end

The only two changes should be importing the Criollo header and conforming to CRApplicationDelegate instead of NSApplicationDelegate.

AppDelegate.m

Cocoa apps do more than we need. The main thing is to remove the IBOutlet declaration, since we are not using Interface Builder to design our UI (if any).

Look for the line @property (weak) IBOutlet NSWindow *window; and remove it. Everything else stays the same. The file should look like this:

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

@end

You’ll notice a few familiar methods; they do the exact same thing as in Cocoa or CocoaTouch.

You can download this skeleton project here, for future use.

Step 3: Hello World

At this point the app should build, start and hang around, doing absolutely nothing.

The next step is to spin-up a server and start serving some content.

CRHTTPServer/CRFCGIServer

Depending on which way you need to go with your project, you can use either of the two (HTTP/FastCGI). This is not a decision you have to make at the start of development since the two classes are perfectly compatible. In case you need to switch just change the class you are instantiating.

First, let’s declare the server object as a property of the AppDelegate, for the purposes of retaining a strong reference to it, just to prevent ARC from prematurely dealloc-ing as soon as we exit applicationDidFinishLaunching:.

@property (nonatomic, strong, nonnull) CRServer* server;

After that, inside applicationDidFinishLaunching: we initialize the server and start listening.

self.server = [[CRHTTPServer alloc] init];
[self.server startListening];

Criollo will start an HTTP server that is bound to all interfaces on port 10781. The server is accessible at http://localhost:10781/.

At this point, the server will return a 404 Not Found response, because there are no routes defined as yet.

The info displayed to the client is provided by the built-in error handling block. The code for this is found here.

CRRouteBlock

The basic building block of Criollo’s routing system is a CRRouteBlock.

typedef void(^CRRouteBlock)(CRRequest* _Nonnull request, CRResponse* _Nonnull response, CRRouteCompletionBlock _Nonnull completionHandler);

These blocks are added to paths according to HTTP request methods and they get executed in the order in which they are added to the route.

Adding a route

CRRouter offers several ways of adding a CRRouteBlock to a route.

For the purpose of this guide we will add a block that says “Hello World”, to the path “/", for the HTTP method “GET”.

[self.server get:@"/" block:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
    [response send:@"Hello world!"];
    completionHandler();
}];

Using send:, or any of the send family of functions, causes the response to end. Subsequent calls to the write or send family of functions will throw an NSInternalInconsistencyException. Check out the rest of the CRResponse API.

At the end of the work that this block will perform, call completionHandler() in order to proceed with the execution of the rest of the blocks added to the current route.

Adding a “middleware” block

Typically a middleware is a block that gets executed on a series of paths and is not the end-point of the route.

- (void)add:(CRRouteBlock _Nonnull)block;

Calling this method of CRRouter will cause the block to be added to all paths on all HTTP methods. Of course the same result could be achieved by calling any other versions of this method with the path and HTTPMethod parameters set to nil.

Let’s add a middleware that will set the Server HTTP header on all responses. Useful for bragging rights.

We will add this before the “Hello world” block, as that one finishes the response.

[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
    [response setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"Server"];		
    completionHandler();
}];

Adding Some Logging

Even though the “Hello world” block finishes the response, additional blocks can be added to the route. The only restriction is that they cannot alter the response. It has already been sent.

Let’s add a block that simply does some NSLog-ing.

[self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
    NSUInteger statusCode = request.response.statusCode;
    NSString* contentLength = [request.response valueForHTTPHeaderField:@"Content-Length"];
    NSString* userAgent = request.env[@"HTTP_USER_AGENT"];
    NSString* remoteAddress = request.connection.remoteAddress;
    NSLog(@"%@ %@ - %lu %@ - %@", remoteAddress, request, statusCode, contentLength ? : @"-", userAgent);
    completionHandler();
}];

All this block does is log some nice info about the request and response to the console.

To Sum it Up

By this point we have a server running at http://localhost:10781/, that sets a Server header on all responses, says “Hello world” and then NSLogs what it has just done.

The AppDelegate.m file should look like this:

#import "AppDelegate.h"

@interface AppDelegate () <CRServerDelegate>

@property (nonatomic, strong, nonnull) CRServer* server;

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    self.server = [[CRHTTPServer alloc] init];

    [self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
        [response setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:@"Server"];
        completionHandler();
    }];

    [self.server get:@"/" block:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
        [response sendString:@"Hello world!"];
        completionHandler();
    }];

    [self.server add:^(CRRequest * _Nonnull request, CRResponse * _Nonnull response, CRRouteCompletionBlock  _Nonnull completionHandler) {
        NSUInteger statusCode = request.response.statusCode;
        NSString* contentLength = [request.response valueForHTTPHeaderField:@"Content-Length"];
        NSString* userAgent = request.env[@"HTTP_USER_AGENT"];
        NSString* remoteAddress = request.connection.remoteAddress;
        NSLog(@"%@ %@ - %lu %@ - %@", remoteAddress, request, statusCode, contentLength ? : @"-", userAgent);
        completionHandler();
    }];

    [self.server startListening];
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

@end