Tutorial

Once Upon A Time…

Once upon a time, there was a sysadmin who wanted to make sure her website was always online. However, she figured that she was pretty good at compiling, installing, and configuring software, but that her programming skills were a bit rusty.

Oh, sure, she remembered her days at university where she learned a bit of Java, and C++, and the cool mind-bending exercises in LISP, but today she felt like trying something new. She followed the installation instructions carefully, and then jumped in without waiting.

After fishing online for information, she decided to start with the following program:

"The website may or may not be online." println()

Saving it as watchcorgi.ooc and running rock -v watchcorgi sure produced a lot of output. And - as a token of its appreciation, the compiler even left an executable on the hard drive. What a promising relationship, she thought.

However, not one to be overly chatty, she decided that instead of having to type rock watchcorgi every time she wanted to compile her new program, she was going to write a usefile for it, and put them both together in a directory.

Name: Watch Corgi
Description: Tells on the bad websites that go down
Version: 0.1.0
SourcePath: source
Main: watchcorgi

Saving it as watchcorgi.use, she realized that, if she wanted her usefile to be valid, she needed to move her ooc module into source/watchcorgi.ooc. So then, her folder hierarchy now looked like:

.
├── source
│   └── watchcorgi.ooc
└── watchcorgi.use

Now, all she had to do was type rock to have her program compiled. If she felt like reading a bit, all she had to do was rock -v - it reminded her of the countless hours spent installing packages on Gentoo.

The Great Illusion

However, that program was not quite as useful as she had hoped so far. While it was technically correct — the best kind of correct — it did not, in fact, bring any new information to the table.

That was not meant to last, though, as she quickly devised a devious scheme. Before the era of watchcorgi, she was using curl, a command-line utility, to check if the website was still online. It looked a little something like this:

curl -I http://example.org/

(Of course, that wasn’t her website’s actual URL, which we have sneakingly substituted with something a tad more common, in order to protect our heroine’s privacy.)

Running that simple command was enough to let her know, with a simple look, whether the website was up and running or down in the ground — in which case prompt maintenance was needed as soon as humanly possible.

She decided that if she could run that command herself, there was no reason why her program couldn’t do so as well. After a second documentation hunt, she quickly jotted down a few more lines, replacing the contents of source/watchcorgi.ooc with this:

import os/Process

exitCode := Process new(["curl", "-I", "http://example.org/"]) execute()
"Sir Curl exited with: #{exitCode}" println()

And sure enough, after a quick recompilation, she saw the expected result: Sir Curl exited with: 0. Curious, she disconnected from the internet, and tried launching ./watchcorgi again. This time, she saw: Sir Curl exited with: 6.

“It’s just like it always is with Unix-like tools” she thought. “An exit code of 0 is a good sign, anything else… not so much. It sure is convenient to be able to import another ooc module for almost everything. Apparently, this Process class takes an array with the command arguments. And this execute method returns the exit code. Neato!” And so it was.

Form Follows Function

She was starting to be happy with her program. For all intents and purposes, was doing its job, and it was doing its job well. However, she could not deny that her program could have put a little more effort in the presentation. Just because a program does not have a will of its own, doesn’t mean it’s okay for it to be rude.

“Time to get to work”, she said out loud, forgetting that it was past 2 in the morning, and that nobody could probably hear her - and even if they could, there was no certainty that they would agree. While she thought about that, her fingers had kept tapping on the keyboard. Her program now looked a little bit like that:

import os/[Process, Terminal]

exitCode := Process new(["curl", "-I", "http://example.org/"]) execute()

match (exitCode) {
  case 0 =>
    "Everything is fine." println()

  case =>
    Terminal setFgColor(Color red)
    "[ERROR] The website is down!" println()
    Terminal reset()
}

It didn’t blink, and there were no 3D effects: disappointing maybe for a sci-fi fan like her little brother, but having alerts in red, and a human-readable message was fine enough for her.

While carefully proofreading her code to check if she hadn’t missed anything, she thought about the syntax of the match construct. “It’s pretty smart, in fact. Cases are tested from top to bottom - the first that matches, wins. And a case with no value simply matches everything”. It just made sense.

She was also particularly happy with the way she was able to import both os/Process and os/Terminal from the same line. Sure, she could have written two different import directives, but she had been promised a concise programming language and it was about time it delivered.

Corgi Ever Watching

Now that the program was polite, our programmer felt good enough to take a small break. As she was looking out the window, waiting for her 3rd cup of nocturnal coffee to brew, it came to her: “Wait a minute… what good is my program if I have to keep running it manually?”

A quick sip out of her coffee cup finished to clear her mind completely. “I am going to need some sort of loop. And I think watchcorgi should shut up if everything is fine, and only complain if something goes wrong.”

As she looked at her timer, waiting for it to run out and allow her to go back to hacking (self-discipline is important, after all), she came to a second realization: that there were two main tasks in her program - the checking, and the notifying. Surely there must be some way to write that in a more modular way?

She decided to go for broke, and split her program into three different ooc modules. She started with source/watchcorgi/checker.ooc:

import os/[Process]

Checker: class {
  url: String

  init: func (=url)

  /**
   * @return true if the url is reachable, false otherwise
   */
  check: func -> Bool {
    0 == Process new(["curl", "-I", url]) execute()
  }
}

Then went on with source/watchcorgi/notifier.ooc:

import os/[Terminal]

Notifier: class {
  quiet: Bool

  init: func

  notify: func (online: Bool, url: String) {
    if (online) {
      if (quiet) return

      Terminal setFgColor(Color green)
      "[ OK  ] %s is online." printfln(url)
      Terminal reset()
    } else {
      Terminal setFgColor(Color red)
      "[ERROR] %s is not reachable! You may panic now." printfln(url)
      Terminal reset()
    }
  }
}

And finally, thought it was better to rewrite source/watchcorgi.ooc from scratch using those new modules:

import watchcorgi/[checker, notifier]
import os/Time

notifier := Notifier new()
notifier quiet = true // only bother me if something goes wrong
checker := Checker new("http://example.org/")

while (true) {
  notifier notify(checker check(), checker url)
  Time sleepSec(5)
}

There. All good. Not only was her program now constantly vigilant, checking for potential problems every five seconds, she felt that the various components were just as flexible as needed, small enough, and that it made the main program file short and sweet.

The Littlest Things

There was one area of the code she wasn’t entirely happy with - in the notifier, she was using the same pattern twice. First Terminal setFgColor, then String printfln, then Terminal reset. She decided to extract that pattern into a function instead, and added it to the end of the the Notifier class definition:

say: func (color: Color, message: String) {
  Terminal setFgColor(color)
  message println()
  Terminal reset()
}

With that new neighbor, the notify function was happy to be reduced to:

notify: (online: Bool, url: String) {
  if (online) {
    if (quiet) return
    say(Color green, "[ OK  ] %s is online" format(url))
  } else {
    say(Color red, "[ERROR] %s is not reachable! You may panic now." \
      format(url))
  }
}

While this was better, she wasn’t satisfied yet - calling format like this (she thought of it as a version of printfln that returned the formatted string instead of printing it) wasn’t particularly pretty.

Like with everything that bothered her, she decided to do something about it:

say: func (color: Color, message: String, args: ...) {
  Terminal setFgColor(color)
  message printfln(args)
  Terminal reset()
}

notify: func (online: Bool, url: String) {
  if (online) {
    if (quiet) return
    say(Color green, "[ OK  ] %s is online", url)
  } else {
    say(Color red, "[ERROR] %s is not reachable! You may panic now.", url)
  }
}

It was subtle, but for her, it made all the difference. Being able to relay any number of arguments like that? This language might actually be comfortable after all.

All Together Now

“So, that was nice. For the life of me, I can’t think of a single thing my program is missing.” Her eyes closed gently, and she leaned back, as if overwhelmed by bliss.

Wait. Her eyes, suddenly inquisitive, were perfectly open now. “What if I want to monitor several websites? Then I would need a config file so that I could modify the list of websites to monitor… and it would need to check them in parallel, so it doesn’t get stuck on any one of them.”

She decided she needed one more module: source/watchcorgi/config.ooc:

import io/File
import os/Env
import structs/List
import text/StringTokenizer

DEFAULT_PATH := Env get("HOME") + "/.config/corgirc"

Config: class {
  websites: List<String>

  init: func (path := DEFAULT_PATH) {
    content := File new(path) read()
    websites = content split('\n') \
               map(|line| line trim("\t ")) \
               filter(|line| !line empty?())
  }
}

Armed with that new weapon, checking multiple websites in parallel was just a matter of making threads behave. Since she didn’t have much experience in the domain, and the documentation seemed a little bit obscure, she decided to ask for help in the ooc discussion group

Almost immediately, a response sprung with numerous code examples she could use as inspiration for her own endeavor. And so she embarked courageously, rewriting source/watchcorgi.ooc once again:

import watchcorgi/[config, checker, notifier]
import os/Time
import threading/Thread
import structs/[ArrayList]

threads := ArrayList<Thread> new()
config := Config new()

for (url in config websites) {
  threads add(Thread new(||
    guard := Guard new(url, 5)
    guard run()
  ))
}

// start all the threads
for (thread in threads) {
  thread start()
}

// wait for all threads to complete
threads each(|thread| thread wait())

Guard: class {
    delay: Int
    checker: Checker
    notifier: Notifier

    init: func (url: String, =delay) {
      checker = Checker new(url)
      notifier = Notifier new()
      notifier quiet = true
    }
    
    run: func {
      while (true) {
        notifier notify(checker check(), checker url)
        Time sleepSec(delay)
      }
    }
}

As she began to write down a list of websites to check in ~/.config/corgirc, she started to list the new things she had learned during that last refactoring:

  • That classes can be used before they are defined - in order word, the order in which classes are defined does not matter!

  • That threads, while really old fashioned, were quite easy to use - all you had to do was create a new Thread object and pass a function that takes zero arguments.

  • That some functions are anonymous - and that they can be defined as an argument to a function call like this: [1, 2, 3] reduce(|a, b| a + b)

  • That using a foreach, such as for (element in iterable) { /* code */ } or using the each method, like so iterable each(|element| /* code */ ), where pretty much equivalent.

When Features Creep

As magnificent as the program was, she couldn’t shake an eerie feeling. It seemed so perfect, so concise, so damn practical - what could possibly go wrong?

“Oh, right!” she whispered. The program assumes that the curl command-line utility is installed and in the $PATH. While on most Linux distributions, that’s a safe bet, it might not be there on OSX. Or, god forbid, on Windows.

But it was almost 6AM, and rays of sunlight would soon come and disturb the oh so peaceful (yet eventful) night of coding. Obviously, she could not afford to write her own HTTP library.

Sure, in theory, a simple usage of net/TCPSocket from the SDK, writing something like

HEAD / HTTP/1.0\r\n\r\n

..and seeing if you get a non-empty response, would suffice. But what about parsing empty, yet worrying responses, like an HTTP 404, or an HTTP 502? What about HTTP 1.1 and the Host header, essential when several websites are running on the same IP address? And most importantly, what about HTTPS, which runs on a different port, and what’s more, over SSL?

No, definitely, writing an HTTP library was not part of the plan. But maybe there was something she could use… maybe curl existed also as a library. A quick search for ooc curl revealed the existence of fasterthanlime/ooc-curl. Jackpot!

A quick clone and.. wait. She knew better. Why not use sam instead? A simple sam clone curl would suffice. Or, better yet, she could add the dependency in the .use file, and run sam get from the watchcorgi folder afterwards.

Her .use file now looked a little bit like this:

Name: Watch Corgi
Description: Multi-threaded website monitoring system
Version: 0.2.0

SourcePath: source
Main: watchcorgi
Requires: curl

And sure enough, after sam get, she saw the ooc-curl folder appear in her $OOC_LIBS directory. It was time to rewrite source/watchcorgi/checker.ooc:

use curl
import curl/Highlevel

Checker: class {
    url: String

    init: func (=url)

    /**
     * @return true if the url is reachable, false otherwise
     */
    check: func -> Bool {
      200 == (HTTPRequest new(url). perform(). getResponseCode())
    }
}

This piece of code was one of her favorites yet. She had used one of the features she had just learned about - call chaining. “In fact”, she would later explain to a colleague, “you can think of the dot as a comma - it separates several method calls, but they all happen on the same object, sequentially”.

Recompiling the program after this change was exciting. There was no configuration dialog to fill out. No complicated command-line option to add when compiling. As a matter of fact, the single line added to the use file was enough to make sam happy - and rock itself seemed pretty content with the use curl directive now sitting at the top of the checker module.

A simple rock -v did the trick. And there she had it. The perfect website monitoring system. At last. Oh, sure, part of her brain fully realized that the impression of perfectness would fade out over the days, but as far as discovering a new language goes, she thought this was a pretty good run.

There was just one thing left to do…

To Give Back

At this point, she felt that watchcorgi it was worth it to publish her program somewhere. Of course, all along, she had been keeping track of it using git. In this case, she was using GitHub as a host.

She decided to make it easy for other people who might want to use watchcorgi, to get it. After a quick search, it quickly became evident that the process itself was trivial. She just had to send a pull request to the sam repository that added a formula for her new pet project.

So, after forking sam on GitHub, changing the origin of her sam repository, she opened a new file in $OOC_LIBS/sam/library/watchcorgi.yml, and wrote:

Origin: https://github.com/example/watchcorgi.git

And then, she submitted the pull request. The sun was rising. It felt warm. I think - she thought - I just might like it here.