blob: 6a98e3a7275eb6043a25b6d78a06e23b0e4491fe [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"
giofc441e32024-11-11 16:26:14 +040011 "path/filepath"
giobcd25e92025-05-03 19:14:10 +040012 "strings"
gio0eaf2712024-04-14 13:08:46 +040013 "sync"
gioff0ee0f2024-10-15 23:11:54 +040014 "syscall"
gio0eaf2712024-04-14 13:08:46 +040015 "time"
16
17 "golang.org/x/crypto/ssh"
18)
19
20type Server struct {
21 l sync.Locker
22 port int
gio266c04f2024-07-03 14:18:45 +040023 appId string
gio0eaf2712024-04-14 13:08:46 +040024 ready bool
25 cmd *exec.Cmd
26 repoAddr string
gio2b1157a2024-10-24 08:45:07 +040027 branch string
giofc441e32024-11-11 16:26:14 +040028 rootDir string
gio0eaf2712024-04-14 13:08:46 +040029 signer ssh.Signer
30 appDir string
31 runCommands []Command
32 self string
gioa60f0de2024-07-08 10:49:48 +040033 managerAddr string
gio183e8342024-08-20 06:01:24 +040034 logs *Log
gio45c31822024-10-24 10:58:02 +040035 currDir string
gio0eaf2712024-04-14 13:08:46 +040036}
37
giofc441e32024-11-11 16:26:14 +040038func NewServer(port int, appId string, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
gio0eaf2712024-04-14 13:08:46 +040039 return &Server{
40 l: &sync.Mutex{},
41 port: port,
42 ready: false,
gio266c04f2024-07-03 14:18:45 +040043 appId: appId,
gio0eaf2712024-04-14 13:08:46 +040044 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040045 branch: branch,
giofc441e32024-11-11 16:26:14 +040046 rootDir: rootDir,
gio0eaf2712024-04-14 13:08:46 +040047 signer: signer,
48 appDir: appDir,
49 runCommands: runCommands,
50 self: self,
gioa60f0de2024-07-08 10:49:48 +040051 managerAddr: manager,
gio183e8342024-08-20 06:01:24 +040052 logs: &Log{},
gio45c31822024-10-24 10:58:02 +040053 currDir: "",
gio0eaf2712024-04-14 13:08:46 +040054 }
55}
56
57func (s *Server) Start() error {
58 http.HandleFunc("/update", s.handleUpdate)
59 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040060 http.HandleFunc("/logs", s.handleLogs)
gio0eaf2712024-04-14 13:08:46 +040061 if err := s.run(); err != nil {
62 return err
63 }
64 go s.pingManager()
65 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
66}
67
gio183e8342024-08-20 06:01:24 +040068func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
69 fmt.Fprint(w, s.logs.Contents())
70}
71
gio0eaf2712024-04-14 13:08:46 +040072func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
73 s.l.Lock()
74 defer s.l.Unlock()
75 if s.ready {
76 fmt.Fprintln(w, "ok")
77 } else {
78 http.Error(w, "not ready", http.StatusInternalServerError)
79 }
80}
81
82func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
83 fmt.Println("update")
84 s.l.Lock()
85 s.ready = false
86 s.l.Unlock()
gio0eaf2712024-04-14 13:08:46 +040087 if err := s.run(); err != nil {
88 http.Error(w, err.Error(), http.StatusInternalServerError)
89 return
90 }
91 s.l.Lock()
92 s.ready = true
93 s.l.Unlock()
94}
95
96func (s *Server) run() error {
gio45c31822024-10-24 10:58:02 +040097 newDir, err := os.MkdirTemp(s.appDir, "code-*")
98 if err != nil {
99 return err
100 }
giofc441e32024-11-11 16:26:14 +0400101 if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400102 return err
103 }
gio183e8342024-08-20 06:01:24 +0400104 logM := io.MultiWriter(os.Stdout, s.logs)
gio0eaf2712024-04-14 13:08:46 +0400105 for i, c := range s.runCommands {
106 args := []string{c.Bin}
107 args = append(args, c.Args...)
108 cmd := &exec.Cmd{
giofc441e32024-11-11 16:26:14 +0400109 Dir: filepath.Join(newDir, s.rootDir),
giobcd25e92025-05-03 19:14:10 +0400110 Path: "/bin/sh",
111 Args: []string{"/bin/sh", "-c", strings.Join(args, " ")},
gio7fbd4ad2024-08-27 10:06:39 +0400112 Env: append(os.Environ(), c.Env...),
gio183e8342024-08-20 06:01:24 +0400113 Stdout: logM,
114 Stderr: logM,
gio0eaf2712024-04-14 13:08:46 +0400115 }
gioff0ee0f2024-10-15 23:11:54 +0400116 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gio5e4d1a72024-10-09 15:25:29 +0400117 fmt.Printf("Running: %s %s\n", c.Bin, c.Args)
gio0eaf2712024-04-14 13:08:46 +0400118 if i < len(s.runCommands)-1 {
119 if err := cmd.Run(); err != nil {
120 return err
121 }
122 } else {
gio45c31822024-10-24 10:58:02 +0400123 if s.cmd != nil {
124 // TODO(gio): make this point configurable
125 if err := s.kill(); err != nil {
126 return err
127 }
128 if err := os.RemoveAll(s.currDir); err != nil {
129 return err
130 }
131 }
gio0eaf2712024-04-14 13:08:46 +0400132 if err := cmd.Start(); err != nil {
133 return err
134 }
135 s.cmd = cmd
136 }
137 }
gio45c31822024-10-24 10:58:02 +0400138 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400139 return nil
140}
141
142type pingReq struct {
143 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400144 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400145}
146
147func (s *Server) pingManager() {
gioc555e0a2025-04-17 11:03:45 +0400148 // TODO(gio): do we need runnert -> manager communication?
149 return
gio0eaf2712024-04-14 13:08:46 +0400150 defer func() {
151 go func() {
152 time.Sleep(5 * time.Second)
153 s.pingManager()
154 }()
155 }()
gioa60f0de2024-07-08 10:49:48 +0400156 buf, err := json.Marshal(pingReq{
157 Address: fmt.Sprintf("%s:%d", s.self, s.port),
gio183e8342024-08-20 06:01:24 +0400158 Logs: s.logs.Contents(),
gioa60f0de2024-07-08 10:49:48 +0400159 })
gio0eaf2712024-04-14 13:08:46 +0400160 if err != nil {
161 return
162 }
gioa60f0de2024-07-08 10:49:48 +0400163 registerWorkerAddr := fmt.Sprintf("%s/api/apps/%s/workers", s.managerAddr, s.appId)
164 http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
gio0eaf2712024-04-14 13:08:46 +0400165}
gio45c31822024-10-24 10:58:02 +0400166
167func (s *Server) kill() error {
168 if s.cmd == nil {
169 return nil
170 }
171
172 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
173 if err != nil {
174 return err
175 }
176 // NOTE(gio): No need to check err as we just killed the process
177 s.cmd.Wait()
178 s.cmd = nil
179 return nil
180}