blob: 1779a762ca4f22038b6ea132ec0693d0d9405f0e [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
gio183e8342024-08-20 06:01:24 +04007 "io"
gio0eaf2712024-04-14 13:08:46 +04008 "net/http"
9 "os"
10 "os/exec"
11 "sync"
gioff0ee0f2024-10-15 23:11:54 +040012 "syscall"
gio0eaf2712024-04-14 13:08:46 +040013 "time"
14
15 "golang.org/x/crypto/ssh"
16)
17
18type Server struct {
19 l sync.Locker
20 port int
gio266c04f2024-07-03 14:18:45 +040021 appId string
gio0eaf2712024-04-14 13:08:46 +040022 ready bool
23 cmd *exec.Cmd
24 repoAddr string
gio2b1157a2024-10-24 08:45:07 +040025 branch string
gio0eaf2712024-04-14 13:08:46 +040026 signer ssh.Signer
27 appDir string
28 runCommands []Command
29 self string
gioa60f0de2024-07-08 10:49:48 +040030 managerAddr string
gio183e8342024-08-20 06:01:24 +040031 logs *Log
gio45c31822024-10-24 10:58:02 +040032 currDir string
gio0eaf2712024-04-14 13:08:46 +040033}
34
gio2b1157a2024-10-24 08:45:07 +040035func NewServer(port int, appId string, repoAddr, branch string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
gio0eaf2712024-04-14 13:08:46 +040036 return &Server{
37 l: &sync.Mutex{},
38 port: port,
39 ready: false,
gio266c04f2024-07-03 14:18:45 +040040 appId: appId,
gio0eaf2712024-04-14 13:08:46 +040041 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040042 branch: branch,
gio0eaf2712024-04-14 13:08:46 +040043 signer: signer,
44 appDir: appDir,
45 runCommands: runCommands,
46 self: self,
gioa60f0de2024-07-08 10:49:48 +040047 managerAddr: manager,
gio183e8342024-08-20 06:01:24 +040048 logs: &Log{},
gio45c31822024-10-24 10:58:02 +040049 currDir: "",
gio0eaf2712024-04-14 13:08:46 +040050 }
51}
52
53func (s *Server) Start() error {
54 http.HandleFunc("/update", s.handleUpdate)
55 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040056 http.HandleFunc("/logs", s.handleLogs)
gio0eaf2712024-04-14 13:08:46 +040057 if err := s.run(); err != nil {
58 return err
59 }
60 go s.pingManager()
61 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
62}
63
gio183e8342024-08-20 06:01:24 +040064func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
65 fmt.Fprint(w, s.logs.Contents())
66}
67
gio0eaf2712024-04-14 13:08:46 +040068func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
69 s.l.Lock()
70 defer s.l.Unlock()
71 if s.ready {
72 fmt.Fprintln(w, "ok")
73 } else {
74 http.Error(w, "not ready", http.StatusInternalServerError)
75 }
76}
77
78func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
79 fmt.Println("update")
80 s.l.Lock()
81 s.ready = false
82 s.l.Unlock()
gio0eaf2712024-04-14 13:08:46 +040083 if err := s.run(); err != nil {
84 http.Error(w, err.Error(), http.StatusInternalServerError)
85 return
86 }
87 s.l.Lock()
88 s.ready = true
89 s.l.Unlock()
90}
91
92func (s *Server) run() error {
gio45c31822024-10-24 10:58:02 +040093 newDir, err := os.MkdirTemp(s.appDir, "code-*")
94 if err != nil {
95 return err
96 }
97 if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.signer, newDir); err != nil {
gio0eaf2712024-04-14 13:08:46 +040098 return err
99 }
gio183e8342024-08-20 06:01:24 +0400100 logM := io.MultiWriter(os.Stdout, s.logs)
gio0eaf2712024-04-14 13:08:46 +0400101 for i, c := range s.runCommands {
102 args := []string{c.Bin}
103 args = append(args, c.Args...)
104 cmd := &exec.Cmd{
gio45c31822024-10-24 10:58:02 +0400105 Dir: newDir,
gio0eaf2712024-04-14 13:08:46 +0400106 Path: c.Bin,
107 Args: args,
gio7fbd4ad2024-08-27 10:06:39 +0400108 Env: append(os.Environ(), c.Env...),
gio183e8342024-08-20 06:01:24 +0400109 Stdout: logM,
110 Stderr: logM,
gio0eaf2712024-04-14 13:08:46 +0400111 }
gioff0ee0f2024-10-15 23:11:54 +0400112 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gio5e4d1a72024-10-09 15:25:29 +0400113 fmt.Printf("Running: %s %s\n", c.Bin, c.Args)
gio0eaf2712024-04-14 13:08:46 +0400114 if i < len(s.runCommands)-1 {
115 if err := cmd.Run(); err != nil {
116 return err
117 }
118 } else {
gio45c31822024-10-24 10:58:02 +0400119 if s.cmd != nil {
120 // TODO(gio): make this point configurable
121 if err := s.kill(); err != nil {
122 return err
123 }
124 if err := os.RemoveAll(s.currDir); err != nil {
125 return err
126 }
127 }
gio0eaf2712024-04-14 13:08:46 +0400128 if err := cmd.Start(); err != nil {
129 return err
130 }
131 s.cmd = cmd
132 }
133 }
gio45c31822024-10-24 10:58:02 +0400134 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400135 return nil
136}
137
138type pingReq struct {
139 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400140 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400141}
142
143func (s *Server) pingManager() {
144 defer func() {
145 go func() {
146 time.Sleep(5 * time.Second)
147 s.pingManager()
148 }()
149 }()
gioa60f0de2024-07-08 10:49:48 +0400150 buf, err := json.Marshal(pingReq{
151 Address: fmt.Sprintf("%s:%d", s.self, s.port),
gio183e8342024-08-20 06:01:24 +0400152 Logs: s.logs.Contents(),
gioa60f0de2024-07-08 10:49:48 +0400153 })
gio0eaf2712024-04-14 13:08:46 +0400154 if err != nil {
155 return
156 }
gioa60f0de2024-07-08 10:49:48 +0400157 registerWorkerAddr := fmt.Sprintf("%s/api/apps/%s/workers", s.managerAddr, s.appId)
158 http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
gio0eaf2712024-04-14 13:08:46 +0400159}
gio45c31822024-10-24 10:58:02 +0400160
161func (s *Server) kill() error {
162 if s.cmd == nil {
163 return nil
164 }
165
166 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
167 if err != nil {
168 return err
169 }
170 // NOTE(gio): No need to check err as we just killed the process
171 s.cmd.Wait()
172 s.cmd = nil
173 return nil
174}