{"action":"created","comment":{"url":"https://api.github.com/repos/m-lab/ndt/pulls/comments/32473463","id":32473463,"diff_hunk":"@@ -1527,647 +1326,364 @@ void child_process(int parent_pipe, SSL_CTX *ssl_context, int ctlsockfd) {\n     log_println(3, \"Successfully returned from run_test() routine\");\n   } else {\n     log_println(3,\n-                \"Child %d returned non-zero (%d) from \"\n-                \"run_test() results some test failed!\",\n+                \"Child %d returned non-zero (%d) from run_test() results some \"\n+                \"test failed!\",\n                 getpid(), retcode);\n-    child_sig(0);\n   }\n-  SSL_free(ctl.ssl);\n-  close(ctl.socket);\n+  close_connection(&ctl);\n #if USE_WEB100\n   web100_detach(agent);\n #elif USE_WEB10G\n   estats_nl_client_destroy(&agent);\n #endif\n-  // log_free(); // Don't free the log we use it all the time\n-  // log_println()\n-  // Also makes valgrind angry\n \n   if (cputime && workerThreadId) {\n     cputimeworkerLoop = 0;\n     pthread_join(workerThreadId, NULL);\n   }\n-\n-  // At this point the tests have been run and we need to clean up\n-  // and handle and child processes that might still be lying\n-  // around.  If we don't we get zombies. The pkt-pair handling\n-  // created 2 childern and we need to get rid of them. To know\n-  // what PIDs to look for on 1/19/10 the run_test() routine was\n-  // modified to pass around these values. The values are set in\n-  // the proper routine in the testoptions.c file.\n-  // Then add call to child_sig() routine and pass in these PID's\n-  // so we handle each child in sequence\n-\n-  log_println(6, \"remove pkt-pair children c2s=%d, s2c=%d\", testopt.child1,\n-              testopt.child2);\n-  if (testopt.child1 != 0) child_sig(testopt.child1);\n-  if (testopt.child2 != 0) child_sig(testopt.child2);\n   exit(0);\n }\n \n+int max(int a, int b) { return (a > b) ? a : b; }\n+\n /**\n- * The server's main loop.  This is the function that, once all arguments are\n- * processed and the server environment has been set up, will keep waiting for\n- * new connections and then forking off children to handle those connections.\n+ * Waits for a new client to connect, for one of the child processes to die, or\n+ * (optionally) until a timeout occurs.  If the select() is woken up by a new\n+ * client connecting, then this routine will allocate and initialize a new\n+ * ndtchild struct, and fork off the child process.\n+ * @param listenfd The file descriptor which listens to the network\n+ * @param signalfd The pipe which will be written to upon receipt of SIGCHILD.\n+ * @param waitforever True if the client should wait forever, false otherwise\n+ * @param ssl_context The ssl_context for any new connections\n+ * @returns NULL or a newly initialized ndtchild struct. The calling function\n+ *          owns the struct.\n  */\n-void NDT_server_main_loop(SSL_CTX *ssl_context, int listenfd) {\n+struct ndtchild *wait_for_new_child(int listenfd, int signalfd, int waitforever,\n+                                    SSL_CTX *ssl_context) {\n   int child_pipe[2];\n-  int tpid, mwaiting = 0;\n+  pid_t child_pid;\n+  enum PROCESS_STATUS_INT procstatusenum = UNKNOWN;\n+  enum PROCESS_TYPE_INT proctypeenum = PROCESS_TYPE;\n+  int ctlsockfd;\n+  struct ndtchild *new_child = NULL;\n   fd_set rfd;\n+  struct timeval sel_tv;\n   int retcode;\n-  time_t tt;\n-  int ctlsockfd;\n+  char signal_value;\n   char tmpstr[256];\n-  struct timeval sel_tv;\n-  struct ndtchild *tmp_ptr = NULL, *new_child = NULL, *mchild = NULL;\n-  int tmp_pipe;\n-  int child_signal;\n+  I2Addr tmp_addr;\n   socklen_t clilen;\n   struct sockaddr_storage cli_addr;\n-  enum PROCESS_STATUS_INT procstatusenum = UNKNOWN;\n-  enum PROCESS_TYPE_INT proctypeenum = PROCESS_TYPE;\n-  int chld_pipe[2];\n-  pid_t chld_pid = -1;\n-  int i, j;\n-  for (;;) {\n-  mainloop:\n-    if (head_ptr == NULL)\n-      log_println(3, \"nothing in queue\");\n-    else\n-      log_println(3,\n-                  \"Queue pointer=%d, testing=%d, waiting=%d, mclients=%d, \"\n-                  \"zombie_check=%d\",\n-                  head_ptr->pid, testing, waiting, mclients, zombie_check);\n-\n-    if (sig17 > 0) {\n-      log_println(5, \"Handle pending SIGCHLD signal, count=%d\", sig17);\n-      child_sig(0);\n-    }\n+  size_t rmt_host_strlen;\n \n-    if ((multiple == 1) && (mclients < max_clients) &&\n-        (waiting >= max_clients)) {\n-      // This condition means that there are clients waiting and there\n-      // are open slots in the test queue, so dispatch another client.\n-      //   RAC 12/11/09\n-\n-      log_println(5, \"Empty slot in test queue, find new client to dispatch\");\n-      /* tmp_ptr = head_ptr; */\n-      mchild = head_ptr;\n-      i = 0;\n-      while (mchild != NULL) {\n-        i++;  // Keep count of how many times we go through this loop\n-        log_println(2,\n-                    \"walking queue look for non-running client current=%d, \"\n-                    \"running=%d, next=0x%x\",\n-                    mchild->pid, mchild->running, mchild->next);\n-        if (mchild->running == 0) {\n-          /* mchild = tmp_ptr; */\n-          log_println(6,\n-                      \"found non-running client %d, update queue and dispatch \"\n-                      \"this client\",\n-                      mchild->pid);\n-          break;\n-        }\n-        mchild = mchild->next;\n-      }\n-      if (i > max_clients) {\n-        log_println(6,\n-                    \"walked through running client list, no empty \"\n-                    \"slots!\");\n-        continue;\n-      }\n-\n-      tmp_ptr = mchild;\n-\n-      // Update queued clients, send message to client when it moves\n-      //  up in the queue enough to get closer to running a test.  This\n-      //  happens when the client falls into the next lower maxqueue bin\n-\n-      if (waiting > (2 * max_clients)) {\n-        int wait_data = -1;\n-        int num_secs = 0;\n-        for (i = max_clients; i <= waiting; i++) {\n-          if (tmp_ptr == NULL) break;\n-          if (i == (2 * max_clients)) {\n-            wait_data = 1;\n-            num_secs = 45;\n-          }\n-          if (i == (3 * max_clients)) {\n-            wait_data = 2;\n-            num_secs = 90;\n-          }\n-          if (wait_data != -1) {\n-            child_signal = wait_data;\n-            write(tmp_ptr->pipe, &child_signal, sizeof(int));\n-            log_println(6,\n-                        \"sent %d sec update message to client %d on fd=%d, \"\n-                        \"send_msg() returned %d\",\n-                        num_secs, tmp_ptr->pid, tmp_ptr->pipe);\n-          }\n-          tmp_ptr = tmp_ptr->next;\n-        }\n-      }\n-      goto dispatch_client;\n-    }\n-\n-    if ((waiting < 0) || (mclients < 0)) {\n-      log_println(6,\n-                  \"Fault: Negative number of clents waiting=%d, mclients=%d\"\n-                  \", nuke them\",\n-                  waiting, mclients);\n-      while (head_ptr != NULL) {\n-        // give message to server waiting in queue\n-        child_signal = SRV_QUEUE_SERVER_BUSY;\n-        write(head_ptr->pipe, &child_signal, sizeof(int));\n-        close(head_ptr->pipe);\n-        tpid = head_ptr->pid;\n-        child_sig(-1);\n-        kill(tpid, SIGTERM);\n-        child_sig(tpid);\n-      }\n-      waiting = 0;\n-      mclients = 0;\n-      continue;\n-    }\n-\n-    if ((waiting == 0) && (head_ptr != NULL)) {\n-      log_println(6, \"Fault: Something [%d] in queue, but no waiting clients\",\n-                  head_ptr->pid);\n-      while (head_ptr != NULL) {\n-        /* send_msg(head_ptr->ctlsockfd, SRV_QUEUE, \"9977\", 4); */\n-        child_signal = SRV_QUEUE_SERVER_BUSY;\n-        write(head_ptr->pipe, &child_signal, sizeof(int));\n-        close(head_ptr->pipe);\n-        tpid = head_ptr->pid;\n-        child_sig(-1);\n-        kill(tpid, SIGTERM);\n-        child_sig(tpid);\n-      }\n-      waiting = 0;\n-      mclients = 0;\n-      continue;\n+  FD_ZERO(&rfd);\n+  FD_SET(listenfd, &rfd);\n+  FD_SET(signalfd, &rfd);\n+  if (waitforever) {\n+    retcode = select(max(listenfd, signalfd) + 1, &rfd, NULL, NULL, NULL);\n+  } else {\n+    sel_tv.tv_sec = 3;  // 3 seconds == WAIT_TIME_SRVR\n+    sel_tv.tv_usec = 0;\n+    retcode = select(max(listenfd, signalfd) + 1, &rfd, NULL, NULL, &sel_tv);\n+  }\n+  if (retcode == -1) {\n+    if (errno != EINTR) {\n+      log_println(0, \"Error in server's select call: %d (%s)\", errno,\n+                  strerror(errno));\n     }\n+    return NULL;\n+  }\n+  // If we woke up due to data on the signalfd, then read the data.\n+  if (FD_ISSET(signalfd, &rfd)) {\n+    read(signalfd, &signal_value, sizeof(char));\n+  }\n+  // If no new child is trying to connect, return\n+  if (!FD_ISSET(listenfd, &rfd)) return NULL;\n+  // Accept the connection, initialize variables, fire up the new child.\n+  ctlsockfd = 0;\n+  ctlsockfd = accept(listenfd, (struct sockaddr *)&cli_addr, &clilen);\n+  if (ctlsockfd <= 0) return NULL;\n+  // get meta test details copied into results\n+  memcpy(&meta.c_addr, &cli_addr, clilen);\n+  meta.family = ((struct sockaddr *)&cli_addr)->sa_family;\n+\n+  memset(rmt_addr, 0, sizeof(rmt_addr));\n+  // get addr details based on socket info available\n+  tmp_addr = I2AddrBySockFD(get_errhandle(), ctlsockfd, False);\n+  addr2a(&cli_addr, rmt_addr, sizeof(rmt_addr));\n+\n+  rmt_host_strlen = sizeof(rmt_host);\n+  memset(rmt_host, 0, rmt_host_strlen);\n+  I2AddrNodeName(tmp_addr, rmt_host, &rmt_host_strlen);\n+  log_println(4, \"New connection received from 0x%x [%s] sockfd=%d.\", tmp_addr,\n+              tmpstr, ctlsockfd);\n+  procstatusenum = PROCESS_STARTED;\n+  proctypeenum = CONNECT_TYPE;\n+  protolog_procstatus(getpid(), getCurrentTest(), proctypeenum, procstatusenum,\n+                      ctlsockfd);\n+  new_child = (struct ndtchild *)calloc(1, sizeof(struct ndtchild));\n+  if (pipe(child_pipe) == -1) {\n+    log_println(6, \"pipe() failed errno=%d\", errno);\n+    close(ctlsockfd);\n+    free(new_child);\n+    return NULL;\n+  }\n \n-    if (head_ptr != NULL) {\n-      if ((time(0) - head_ptr->stime) > 70) {  // 70 => WAIT-TIME-THRESH\n-        log_println(6,\n-                    \"Fault: Something in queue, but child %d (fd=%d) has \"\n-                    \"exceeded wait time\",\n-                    head_ptr->pid, head_ptr->pipe);\n-        // Should send new 9977 'test aborted' signal to client.  Using\n-        // this for now\n-        // rac 3/26/10\n-\n-        log_println(6, \"pid=%d, client='%s', stime=%ld, qtime=%ld now=%ld\",\n-                    head_ptr->pid, head_ptr->addr, head_ptr->stime,\n-                    head_ptr->qtime, time(0));\n-        log_println(6, \"pipe-fd=%d, running=%d\", head_ptr->pipe,\n-                    head_ptr->running);\n-        // boot the client\n-        child_signal = SRV_QUEUE_SERVER_BUSY;\n-        write(head_ptr->pipe, &child_signal, sizeof(int));\n-        close(head_ptr->pipe);\n-        tpid = head_ptr->pid;\n-        child_sig(-1);\n-        kill(tpid, SIGTERM);\n-        child_sig(tpid);\n-        continue;\n-      }\n-    }\n+  // At this point we have received a connection from a client, meaning that\n+  // a test is being requested.  At this point we should apply any policy or\n+  // AAA functions to the incoming connection.  If we don't like the client,\n+  // we can refuse the connection and loop back to the begining.  There\n+  // would need to be some additional logic installed if this AAA test\n+  // relied on more than the client's IP address.  The client would also\n+  // require modification to allow more credentials to be created/passed\n+  // between the user and this application.\n+\n+  child_pid = fork();\n+  if (child_pid == -1) {\n+    // an error occured, log it and return\n+    log_println(0, \"fork() failed, errno = %d (%s)\", errno, strerror(errno));\n+    close(child_pipe[0]);\n+    close(child_pipe[1]);\n+    close(ctlsockfd);\n+    free(new_child);\n+    return NULL;\n+  } else if (child_pid == 0) {\n+    // This is the child. Clean up and close the resources the child does not\n+    // need, then call the child_process routine which will initialize the\n+    // connection and then wait for the go/queue/nogo signals from the parent\n+    // process.\n+    log_println(4, \"Child thinks pipe() returned fd0=%d, fd1=%d for pid=%d\",\n+                child_pipe[0], child_pipe[1], child_pid);\n+    close(listenfd);\n+    close(child_pipe[1]);\n+    child_process(child_pipe[0], ssl_context, ctlsockfd);\n+    log_println(1, \"The child returned! This should never happen.\");\n+    exit(1);\n+  }\n \n-    if ((multiple == 1) && (mclients > waiting)) {\n-      log_println(\n-          5,\n-          \"Multi-client mode has uncaught terminated clients mclient=%d, \"\n-          \"waiting=%d\",\n-          mclients, waiting);\n-      while (mclients > waiting) {\n-        child_sig(0);\n-        mclients--;\n-        log_println(5, \"Removed terminated client from count mclients=%d\",\n-                    mclients);\n-      }\n-    }\n+  // this is the parent process, handle scheduling and\n+  // queuing of multiple incoming clients\n+  log_println(5, \"Parent process spawned child = %d\", child_pid);\n+  log_println(5, \"Parent thinks pipe() returned fd0=%d, fd1=%d\", child_pipe[0],\n+              child_pipe[1]);\n+\n+  // Close the open resources that should only be used by the child.\n+  close(child_pipe[0]);\n+  close(ctlsockfd);\n+\n+  // Initialize the members of new_child\n+  new_child->pid = child_pid;\n+  strlcpy(new_child->addr, rmt_addr, sizeof(new_child->addr));\n+  strlcpy(new_child->host, rmt_host, sizeof(new_child->host));\n+  new_child->qtime = time(0);\n+  new_child->pipe = child_pipe[1];\n+  new_child->running = 0;\n+  new_child->next = NULL;\n+\n+  return new_child;\n+}\n \n-    FD_ZERO(&rfd);\n-    FD_SET(listenfd, &rfd);\n-    if (waiting > 0) {\n-      // there are clients waiting\n-      sel_tv.tv_sec = 3;  // 3 seconds == WAIT_TIME_SRVR\n-      sel_tv.tv_usec = 0;\n-      log_println(3, \"Waiting for new connection, timer running\");\n-      do {\n-        retcode = select(listenfd + 1, &rfd, NULL, NULL, &sel_tv);\n-        // retry if a signal caused select() to exit.\n-      } while ((retcode == -1) && (errno == EINTR));\n-      tt = time(0);\n+/**\n+ * Enqueues a new ndtchild struct onto the queue of clients to be run.  Will\n+ * not enqueue the child unless the server has spare capacity.  This function\n+ * takes ownership of the memory pointed to by new_child, and either frees that\n+ * memory or passes ownership to the linked list that goes from head to tail.\n+ * @param new_child The new client\n+ * @param head The head of the client queue\n+ * @param tail The tail of the client queue\n+ */\n+void enqueue_new_child(struct ndtchild *new_child, struct ndtchild **head,\n+                       struct ndtchild **tail) {\n+  ParentChildSignal child_signal;\n+  struct ndtchild *tmp_ptr;\n+  int client_count = 0;\n+  // Count the number of connected clients\n+  tmp_ptr = *head;\n+  while (tmp_ptr != NULL) {\n+    client_count++;\n+    tmp_ptr = tmp_ptr->next;\n+  }\n \n+  // Check whether the server is overloaded\n+  if ((!queue && client_count >= 1) ||\n+      (queue && !multiple && client_count >= max_clients) ||\n+      (queue && multiple && client_count >= 4 * max_clients)) {\n+    // The server is overloaded. Reject the client.\n+    log_println(0,\n+                \"Too many clients/mclients (%d) waiting to be served, \"\n+                \"Please try again later.\",\n+                new_child->pid);\n+    child_signal = SRV_QUEUE_SERVER_BUSY;\n+    write(new_child->pipe, &child_signal, sizeof(ParentChildSignal));\n+    // Whether the write succeeds or not, we are done with this child. It\n+    // will either kill itself in response to this message, or it will kill\n+    // itself with its own watchdog timer. Either way, not our problem\n+    // anymore. Send the message, close the pipe, and move on.\n+    close(new_child->pipe);\n+    log_println(6, \"Too many clients, freeing child=0x%x\", new_child);\n+    free(new_child);\n+    new_child = NULL;\n+  } else {\n+    // The server is not overloaded, so queue up the new client\n+    if (*head == NULL) {\n+      *head = *tail = new_child;\n     } else {\n-      // Nothing is in the queue, so wait forever until a new connection\n-      // request arrives\n-      log_println(3, \"Timer not running, waiting for new connection\");\n-      mclients = 0;\n-      do {\n-        retcode = select(listenfd + 1, &rfd, NULL, NULL, NULL);\n-        // retry if a signal caused the select() to exit\n-      } while ((retcode == -1) && (errno == EINTR));\n+      (*tail)->next = new_child;\n+      *tail = new_child;\n     }\n+  }\n+}\n \n-    if (retcode < 0) {\n-      // an interrupt or signal caused the select() to exit, go back and\n-      // start over\n-      log_println(5, \"Select exited with return code = %d\", retcode);\n-      continue;\n-    }\n+/**\n+ * Returns true or false, depending on whether the client's process is dead.\n+ */\n+int is_live_client(struct ndtchild *client) {","path":"src/web100srv.c","position":1034,"original_position":1034,"commit_id":"1d09c1e7e415364e6134671f9ae0525fd9dafb69","original_commit_id":"1d09c1e7e415364e6134671f9ae0525fd9dafb69","user":{"login":"mtlynch","id":7783288,"avatar_url":"https://avatars.githubusercontent.com/u/7783288?v=3","gravatar_id":"","url":"https://api.github.com/users/mtlynch","html_url":"https://github.com/mtlynch","followers_url":"https://api.github.com/users/mtlynch/followers","following_url":"https://api.github.com/users/mtlynch/following{/other_user}","gists_url":"https://api.github.com/users/mtlynch/gists{/gist_id}","starred_url":"https://api.github.com/users/mtlynch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mtlynch/subscriptions","organizations_url":"https://api.github.com/users/mtlynch/orgs","repos_url":"https://api.github.com/users/mtlynch/repos","events_url":"https://api.github.com/users/mtlynch/events{/privacy}","received_events_url":"https://api.github.com/users/mtlynch/received_events","type":"User","site_admin":false},"body":"Suggest rename to `is_client_process_alive`","created_at":"2015-06-15T22:16:41Z","updated_at":"2015-06-15T22:16:41Z","html_url":"https://github.com/m-lab/ndt/pull/13#discussion_r32473463","pull_request_url":"https://api.github.com/repos/m-lab/ndt/pulls/13","_links":{"self":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/comments/32473463"},"html":{"href":"https://github.com/m-lab/ndt/pull/13#discussion_r32473463"},"pull_request":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/13"}}},"pull_request":{"url":"https://api.github.com/repos/m-lab/ndt/pulls/13","id":37513599,"html_url":"https://github.com/m-lab/ndt/pull/13","diff_url":"https://github.com/m-lab/ndt/pull/13.diff","patch_url":"https://github.com/m-lab/ndt/pull/13.patch","issue_url":"https://api.github.com/repos/m-lab/ndt/issues/13","number":13,"state":"open","locked":false,"title":"Rewrote the queuing code to actually, y'know, work.","user":{"login":"pboothe","id":143798,"avatar_url":"https://avatars.githubusercontent.com/u/143798?v=3","gravatar_id":"","url":"https://api.github.com/users/pboothe","html_url":"https://github.com/pboothe","followers_url":"https://api.github.com/users/pboothe/followers","following_url":"https://api.github.com/users/pboothe/following{/other_user}","gists_url":"https://api.github.com/users/pboothe/gists{/gist_id}","starred_url":"https://api.github.com/users/pboothe/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pboothe/subscriptions","organizations_url":"https://api.github.com/users/pboothe/orgs","repos_url":"https://api.github.com/users/pboothe/repos","events_url":"https://api.github.com/users/pboothe/events{/privacy}","received_events_url":"https://api.github.com/users/pboothe/received_events","type":"User","site_admin":false},"body":"Queuing now no longer uses semaphores, no longer has any (or not nearly as many) race conditions, and basically everything is better and more readable.","created_at":"2015-06-11T22:02:03Z","updated_at":"2015-06-15T22:16:41Z","closed_at":null,"merged_at":null,"merge_commit_sha":"97db555865234e1b17a15edb8d6d00617a9afd59","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/m-lab/ndt/pulls/13/commits","review_comments_url":"https://api.github.com/repos/m-lab/ndt/pulls/13/comments","review_comment_url":"https://api.github.com/repos/m-lab/ndt/pulls/comments{/number}","comments_url":"https://api.github.com/repos/m-lab/ndt/issues/13/comments","statuses_url":"https://api.github.com/repos/m-lab/ndt/statuses/1d09c1e7e415364e6134671f9ae0525fd9dafb69","head":{"label":"m-lab:queuing","ref":"queuing","sha":"1d09c1e7e415364e6134671f9ae0525fd9dafb69","user":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"repo":{"id":33625942,"name":"ndt","full_name":"m-lab/ndt","owner":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/m-lab/ndt","description":"Network Diagnostic Tool","fork":true,"url":"https://api.github.com/repos/m-lab/ndt","forks_url":"https://api.github.com/repos/m-lab/ndt/forks","keys_url":"https://api.github.com/repos/m-lab/ndt/keys{/key_id}","collaborators_url":"https://api.github.com/repos/m-lab/ndt/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/m-lab/ndt/teams","hooks_url":"https://api.github.com/repos/m-lab/ndt/hooks","issue_events_url":"https://api.github.com/repos/m-lab/ndt/issues/events{/number}","events_url":"https://api.github.com/repos/m-lab/ndt/events","assignees_url":"https://api.github.com/repos/m-lab/ndt/assignees{/user}","branches_url":"https://api.github.com/repos/m-lab/ndt/branches{/branch}","tags_url":"https://api.github.com/repos/m-lab/ndt/tags","blobs_url":"https://api.github.com/repos/m-lab/ndt/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/m-lab/ndt/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/m-lab/ndt/git/refs{/sha}","trees_url":"https://api.github.com/repos/m-lab/ndt/git/trees{/sha}","statuses_url":"https://api.github.com/repos/m-lab/ndt/statuses/{sha}","languages_url":"https://api.github.com/repos/m-lab/ndt/languages","stargazers_url":"https://api.github.com/repos/m-lab/ndt/stargazers","contributors_url":"https://api.github.com/repos/m-lab/ndt/contributors","subscribers_url":"https://api.github.com/repos/m-lab/ndt/subscribers","subscription_url":"https://api.github.com/repos/m-lab/ndt/subscription","commits_url":"https://api.github.com/repos/m-lab/ndt/commits{/sha}","git_commits_url":"https://api.github.com/repos/m-lab/ndt/git/commits{/sha}","comments_url":"https://api.github.com/repos/m-lab/ndt/comments{/number}","issue_comment_url":"https://api.github.com/repos/m-lab/ndt/issues/comments{/number}","contents_url":"https://api.github.com/repos/m-lab/ndt/contents/{+path}","compare_url":"https://api.github.com/repos/m-lab/ndt/compare/{base}...{head}","merges_url":"https://api.github.com/repos/m-lab/ndt/merges","archive_url":"https://api.github.com/repos/m-lab/ndt/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/m-lab/ndt/downloads","issues_url":"https://api.github.com/repos/m-lab/ndt/issues{/number}","pulls_url":"https://api.github.com/repos/m-lab/ndt/pulls{/number}","milestones_url":"https://api.github.com/repos/m-lab/ndt/milestones{/number}","notifications_url":"https://api.github.com/repos/m-lab/ndt/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/m-lab/ndt/labels{/name}","releases_url":"https://api.github.com/repos/m-lab/ndt/releases{/id}","created_at":"2015-04-08T18:54:07Z","updated_at":"2015-05-06T21:54:03Z","pushed_at":"2015-06-11T22:02:03Z","git_url":"git://github.com/m-lab/ndt.git","ssh_url":"git@github.com:m-lab/ndt.git","clone_url":"https://github.com/m-lab/ndt.git","svn_url":"https://github.com/m-lab/ndt","homepage":null,"size":4401,"stargazers_count":0,"watchers_count":0,"language":"C","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":0,"default_branch":"master"}},"base":{"label":"m-lab:imaginary_middle2","ref":"imaginary_middle2","sha":"97cd364eb1ed2aede2f7fd00d18ed02db1122a02","user":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"repo":{"id":33625942,"name":"ndt","full_name":"m-lab/ndt","owner":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/m-lab/ndt","description":"Network Diagnostic Tool","fork":true,"url":"https://api.github.com/repos/m-lab/ndt","forks_url":"https://api.github.com/repos/m-lab/ndt/forks","keys_url":"https://api.github.com/repos/m-lab/ndt/keys{/key_id}","collaborators_url":"https://api.github.com/repos/m-lab/ndt/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/m-lab/ndt/teams","hooks_url":"https://api.github.com/repos/m-lab/ndt/hooks","issue_events_url":"https://api.github.com/repos/m-lab/ndt/issues/events{/number}","events_url":"https://api.github.com/repos/m-lab/ndt/events","assignees_url":"https://api.github.com/repos/m-lab/ndt/assignees{/user}","branches_url":"https://api.github.com/repos/m-lab/ndt/branches{/branch}","tags_url":"https://api.github.com/repos/m-lab/ndt/tags","blobs_url":"https://api.github.com/repos/m-lab/ndt/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/m-lab/ndt/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/m-lab/ndt/git/refs{/sha}","trees_url":"https://api.github.com/repos/m-lab/ndt/git/trees{/sha}","statuses_url":"https://api.github.com/repos/m-lab/ndt/statuses/{sha}","languages_url":"https://api.github.com/repos/m-lab/ndt/languages","stargazers_url":"https://api.github.com/repos/m-lab/ndt/stargazers","contributors_url":"https://api.github.com/repos/m-lab/ndt/contributors","subscribers_url":"https://api.github.com/repos/m-lab/ndt/subscribers","subscription_url":"https://api.github.com/repos/m-lab/ndt/subscription","commits_url":"https://api.github.com/repos/m-lab/ndt/commits{/sha}","git_commits_url":"https://api.github.com/repos/m-lab/ndt/git/commits{/sha}","comments_url":"https://api.github.com/repos/m-lab/ndt/comments{/number}","issue_comment_url":"https://api.github.com/repos/m-lab/ndt/issues/comments{/number}","contents_url":"https://api.github.com/repos/m-lab/ndt/contents/{+path}","compare_url":"https://api.github.com/repos/m-lab/ndt/compare/{base}...{head}","merges_url":"https://api.github.com/repos/m-lab/ndt/merges","archive_url":"https://api.github.com/repos/m-lab/ndt/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/m-lab/ndt/downloads","issues_url":"https://api.github.com/repos/m-lab/ndt/issues{/number}","pulls_url":"https://api.github.com/repos/m-lab/ndt/pulls{/number}","milestones_url":"https://api.github.com/repos/m-lab/ndt/milestones{/number}","notifications_url":"https://api.github.com/repos/m-lab/ndt/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/m-lab/ndt/labels{/name}","releases_url":"https://api.github.com/repos/m-lab/ndt/releases{/id}","created_at":"2015-04-08T18:54:07Z","updated_at":"2015-05-06T21:54:03Z","pushed_at":"2015-06-11T22:02:03Z","git_url":"git://github.com/m-lab/ndt.git","ssh_url":"git@github.com:m-lab/ndt.git","clone_url":"https://github.com/m-lab/ndt.git","svn_url":"https://github.com/m-lab/ndt","homepage":null,"size":4401,"stargazers_count":0,"watchers_count":0,"language":"C","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/13"},"html":{"href":"https://github.com/m-lab/ndt/pull/13"},"issue":{"href":"https://api.github.com/repos/m-lab/ndt/issues/13"},"comments":{"href":"https://api.github.com/repos/m-lab/ndt/issues/13/comments"},"review_comments":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/13/comments"},"review_comment":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/m-lab/ndt/pulls/13/commits"},"statuses":{"href":"https://api.github.com/repos/m-lab/ndt/statuses/1d09c1e7e415364e6134671f9ae0525fd9dafb69"}}}}