AppRunner: Build next version in the background to reduce downtime

Next step would be to make this point of transition configurable.

Change-Id: Ibf6504a02b2d1c376e70e944e1aaada0f2dea589
diff --git a/apps/app-runner/server.go b/apps/app-runner/server.go
index ffc5709..1779a76 100644
--- a/apps/app-runner/server.go
+++ b/apps/app-runner/server.go
@@ -29,6 +29,7 @@
 	self        string
 	managerAddr string
 	logs        *Log
+	currDir     string
 }
 
 func NewServer(port int, appId string, repoAddr, branch string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
@@ -45,6 +46,7 @@
 		self:        self,
 		managerAddr: manager,
 		logs:        &Log{},
+		currDir:     "",
 	}
 }
 
@@ -78,20 +80,6 @@
 	s.l.Lock()
 	s.ready = false
 	s.l.Unlock()
-	if s.cmd != nil {
-		err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		// NOTE(gio): No need to check err as we just killed the process
-		s.cmd.Wait()
-		s.cmd = nil
-	}
-	if err := os.RemoveAll(s.appDir); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
 	if err := s.run(); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -102,7 +90,11 @@
 }
 
 func (s *Server) run() error {
-	if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.signer, s.appDir); err != nil {
+	newDir, err := os.MkdirTemp(s.appDir, "code-*")
+	if err != nil {
+		return err
+	}
+	if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.signer, newDir); err != nil {
 		return err
 	}
 	logM := io.MultiWriter(os.Stdout, s.logs)
@@ -110,7 +102,7 @@
 		args := []string{c.Bin}
 		args = append(args, c.Args...)
 		cmd := &exec.Cmd{
-			Dir:    *appDir,
+			Dir:    newDir,
 			Path:   c.Bin,
 			Args:   args,
 			Env:    append(os.Environ(), c.Env...),
@@ -124,12 +116,22 @@
 				return err
 			}
 		} else {
+			if s.cmd != nil {
+				// TODO(gio): make this point configurable
+				if err := s.kill(); err != nil {
+					return err
+				}
+				if err := os.RemoveAll(s.currDir); err != nil {
+					return err
+				}
+			}
 			if err := cmd.Start(); err != nil {
 				return err
 			}
 			s.cmd = cmd
 		}
 	}
+	s.currDir = newDir
 	return nil
 }
 
@@ -155,3 +157,18 @@
 	registerWorkerAddr := fmt.Sprintf("%s/api/apps/%s/workers", s.managerAddr, s.appId)
 	http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
 }
+
+func (s *Server) kill() error {
+	if s.cmd == nil {
+		return nil
+	}
+
+	err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
+	if err != nil {
+		return err
+	}
+	// NOTE(gio): No need to check err as we just killed the process
+	s.cmd.Wait()
+	s.cmd = nil
+	return nil
+}