Saturday, July 13, 2013

Hop Stopping a Server


Although still early in its development, I enjoyed getting started with Hop last night. But I left off half way through my introduction to task management (think Makefiles) in Dart. As of last night I can start a test server with Hop:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
But I have to manually lookup the the process ID (PID) so that I can kill it from the shell:
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server
chris      902     1  0 Jul12 pts/23   00:00:00 /bin/sh -c 'dart' 'test/test_server.dart'
chris      903   902  0 Jul12 pts/23   00:00:00 dart test/test_server.dart
chris     7745  1937  0 22:50 pts/23   00:00:00 grep test_ser
➜  hipster-mvc git:(http-test-server) ✗ kill 903
Let's see if I can get stopping the server under Hop as well. Currently the test_server-start task resolves to a Dart function that starts a shell process:
Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();

  Process.
    start('dart', ['test/test_server.dart'], runInShell: true).
    then((_)=> started.complete(true));

  return started.future;
}
To obtain the PID of my test server, I need to use the value supplied to the then() method. So instead of ignoring it with the _ variable, I assign it to the a parameter named process. A Dart Process, which is what is supplied by the process's Future, has a pid process that I ought to be able to use. For now, I just print it out:
Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();

  Process.
    start('dart', ['test/test_server.dart'], runInShell: true).
    then((process) {
      print(process.pid);
      started.complete(true);
    });

  return started.future;
}
But when I try that out, I find:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
8267
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server  
chris     8267     1  0 23:00 pts/23   00:00:00 /bin/sh -c 'dart' 'test/test_server.dart'
chris     8268  8267  2 23:00 pts/23   00:00:00 dart test/test_server.dart
chris     8293  1937  0 23:00 pts/23   00:00:00 grep test_server
The pid process refers to the PID of the shell process which is responsible for starting my server. That ain't gonna work. If I kill the shell process that starts the server (PID of 8267 in this case), then the actual test server continues to run:
➜  hipster-mvc git:(http-test-server) ✗ kill 8267
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server
chris     8268     1  0 23:00 pts/23   00:00:00 dart test/test_server.dart
chris     8337  1937  0 23:00 pts/23   00:00:00 grep test_server
I suppose that I could kill pid + 1, but there is the edge case in which the shell process could be 32768 and the child process would be the next unused PID after 1.

No, what I need is something a bit more reliable. Now that I think about it, there is really no need to run my test server in a shell. I like shelling out and all, but in this case it seems to be causing more trouble than it is worth. So I remove the runInShell optional parameter from the Process.start() method (it defaults to false):
Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();

  Process.
    start('dart', ['test/test_server.dart']).
    then((process) {
      print(process.pid);
      started.complete(true);
    });

  return started.future;
}
With that, I have the PID of the actual test server and I can kill it nice and proper:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
8445
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server  
chris     8445     1  3 23:05 pts/23   00:00:00 dart test/test_server.dart
chris     8470  1937  0 23:05 pts/23   00:00:00 grep test_server
➜  hipster-mvc git:(http-test-server) ✗ kill 8445
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server
chris     8514  1937  0 23:05 pts/23   00:00:00 grep test_server
Unfortunately, this does me little immediate good in my efforts to easily kill the server for two reasons. First, once the Hop command successfully starts my test server, it exits. At this point, I no longer have access to the process to send it the kill message. My second problem is that, even if I write the PID to the file system so that hop test_server-stop could read it, Dart does not support killing arbitrary processes—I can only kill the current process or one that I start with Process.start.

First things first—I write the PID out to the filesystem:
Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();

  Process.
    start('dart', ['test/test_server.dart']).
    then((process) {
      new File('test_server.pid')
        ..writeAsStringSync('${process.pid}');
      started.complete(true);
    });

  return started.future;
}
Now I need to figure out how to use that PID to kill off my running process. Well, I suppose if I cannot kill it off natively in Dart, I can run a process to do it for me.

As with my start-server process, this is asynchronous in nature—Process.run() returns a future that will complete when the process successfully returns. This means that I have to go through the same dance in my _stopTestServer() function that I did in _startTestServer(). I need to instantiate a Completer and return its future so that Hop will know when the task finishes. Then I run my kill process and, when its future completes, then I complete my own:
Future<bool> _stopTestServer(TaskContext context) {
  var killed = new Completer();

  var pid = new File('test_server.pid').
    readAsStringSync();

  Process.
    run('kill', [pid]).
    then((_)=> killed.complete(true));

  return killed.future;
}
And that actually seems to do the trick. I can start my test server process, examine the resultant PID file to verify that it corresponds to the running test server:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
➜  hipster-mvc git:(http-test-server) ✗ cat test_server.pid        
8847%
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server 
chris     8847     1  0 23:25 pts/23   00:00:00 dart test/test_server.dart
chris     8894  1937  0 23:25 pts/23   00:00:00 grep test_server
And, when I Hop the test_server-stop task, the test server process is, indeed, stopped:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-stop 
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server 
chris     8991  1937  0 23:30 pts/23   00:00:00 grep test_server
That is not rock solid as server start and stop goes. I am doing absolutely no error handling for when I start a server when one is already running, stop a server when one is not running, etc. I am also leaving a mess behind by not deleting the test_server.pid file. Cleaning up the PID file is simple enough that I implement it tonight:
Future<bool> _stopTestServer(TaskContext context) {
  var killed = new Completer();

  var pid_file = new File('test_server.pid');
  var pid = pid_file.readAsStringSync();
  pid_file.deleteSync();

  Process.
    run('kill', [pid]).
    then((_)=> killed.complete(true));

  return killed.future;
}
But I will leave error handling for another day.

With that, I can hop start and stop my test server. Up tomorrow, I will look into replacing my current bash script that runs my test suite with Hop.


Day #811

No comments:

Post a Comment