github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/test/appserver.go (about) 1 // Copyright 2021 Daniel Erat. 2 // All rights reserved. 3 4 package test 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "log" 11 "net" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "strconv" 16 "time" 17 18 "github.com/derat/nup/server/config" 19 20 "golang.org/x/sys/unix" 21 ) 22 23 // When tethering over a cellular connection, I saw dev_appserver.py block for 4+ minutes at startup 24 // after logging "devappserver2.py:316] Skipping SDK update check." lsof suggested that it was 25 // blocked on an HTTP connection in SYN_SENT to some random IPv6 address owned by Akamai (?). 26 // When I increased this timeout to 10 minutes, it eventually proceeded (without logging anything). 27 // 28 // Not having any network connection apparently also adds a delay, but 40 seconds seems to be enough 29 // to compensate for it (in practice, startup seems to take around 20-30 seconds when offline). 30 const appserverTimeout = 40 * time.Second 31 32 // DevAppserver wraps a dev_appserver.py process. 33 type DevAppserver struct { 34 appPort int // app's port for HTTP requests 35 cmd *exec.Cmd // dev_appserver.py process 36 createIndexes bool // update index.yaml 37 watchForChanges bool // rebuild app when files changed 38 } 39 40 // NewDevAppserver starts a dev_appserver.py process using the supplied configuration. 41 // 42 // storageDir is used to hold Datastore data and will be created if it doesn't exist. 43 // dev_appserver.py's noisy output will be sent to out (which may be nil). 44 // Close must be called later to kill the process. 45 func NewDevAppserver(cfg *config.Config, storageDir string, out io.Writer, 46 opts ...DevAppserverOption) (*DevAppserver, error) { 47 libDir, err := CallerDir() 48 if err != nil { 49 return nil, err 50 } 51 cfgData, err := json.Marshal(cfg) 52 if err != nil { 53 return nil, err 54 } 55 if err := os.MkdirAll(storageDir, 0755); err != nil { 56 return nil, err 57 } 58 59 var srv DevAppserver 60 for _, o := range opts { 61 o(&srv) 62 } 63 64 // Prevent multiple instances from trying to bind to the same ports. 65 ports, err := FindUnusedPorts(2) 66 if err != nil { 67 return nil, err 68 } 69 if srv.appPort <= 0 { 70 srv.appPort = ports[0] 71 } 72 adminPort := ports[1] 73 74 args := []string{ 75 "--application=nup-test", 76 "--port=" + strconv.Itoa(srv.appPort), 77 "--admin_port=" + strconv.Itoa(adminPort), 78 "--storage_path=" + storageDir, 79 "--env_var", "NUP_CONFIG=" + string(cfgData), 80 "--datastore_consistency_policy=consistent", 81 // TODO: This is a hack to work around forceUpdateFailures in server/main.go. 82 "--max_module_instances=1", 83 } 84 if !srv.createIndexes { 85 args = append(args, "--require_indexes=yes") 86 } 87 if !srv.watchForChanges { 88 args = append(args, "--watcher_ignore_re=.*") 89 } 90 args = append(args, ".") 91 92 cmd := exec.Command("dev_appserver.py", args...) 93 cmd.Dir = filepath.Join(libDir, "..") // directory containing app.yaml 94 cmd.Stdout = out 95 cmd.Stderr = out 96 if err := cmd.Start(); err != nil { 97 return nil, err 98 } 99 100 srv.cmd = cmd 101 102 // Wait for the server to accept connections. 103 start := time.Now() 104 for { 105 if conn, err := net.DialTimeout("tcp", srv.Addr(), time.Second); err == nil { 106 conn.Close() 107 break 108 } else if time.Now().Sub(start) > appserverTimeout { 109 srv.Close() 110 return nil, fmt.Errorf("couldn't connect: %v", err) 111 } 112 time.Sleep(100 * time.Millisecond) 113 } 114 115 // I was seeing occasional hangs in response to the first request: 116 // .. 117 // INFO 2021-12-06 01:48:34,122 instance.py:294] Instance PID: 18017 118 // INFO 2021-12-06 01:48:34,128 instance.py:294] Instance PID: 18024 119 // 2021/12/06 01:48:34 http.ListenAndServe: listen tcp 127.0.0.1:20020: bind: address already in use 120 // INFO 2021-12-06 01:48:34,143 module.py:883] default: "POST /config HTTP/1.1" 200 2 121 // 122 // I think the http.ListenAndServer call comes from the appengine.Main call in the app's main 123 // function. Oddly, the port is always already bound by the app itself. My best guess is that 124 // there's a race in dev_appserver.py that can be triggered when a request comes in soon after 125 // it starts handling requests. It happens infrequently, and I've still seen the error at least 126 // once with a 1-second delay. It didn't happen across dozens of runs with a 3-second delay, 127 // though. 128 time.Sleep(3 * time.Second) 129 130 return &srv, nil 131 } 132 133 // Close stops dev_appserver.py and cleans up its resources. 134 func (srv *DevAppserver) Close() error { 135 // I struggled with reliably cleaning up processes on exit. In the normal-exit case, this seems 136 // pretty straightforward (at least for processes that we exec ourselves): start each process in 137 // its own process group, and at exit, send SIGTERM to the process group, which will kill all of 138 // its members. 139 // 140 // Things are harder when the test process is killed via SIGINT or SIGTERM. I don't think 141 // there's an easy way to run normal cleanup code from the signal handler: the main goroutine 142 // will be blocked doing initialization or running tests, and trying to explicitly kill each 143 // process group from the signal-handling goroutine seems inherently racy. 144 // 145 // I experimented with using unix.Setsid to put the test process (and all of its children) into 146 // a new session and then iterating through each process and checking its SID, but that prevents 147 // the test process from receiving SIGINT when Ctrl+C is typed into the terminal. I also think 148 // we can't put each child process into its own session, since setsid automatically creates a 149 // new process group as well (leaving us with no easy way to kill all processes). 150 // 151 // What I've settled on here is letting dev_appserver.py and its child processes inherit our 152 // process group (which appears to be rooted at the main test process), and then just sending 153 // SIGINT to the root dev_appserver.py process here, which appears to make it exit cleanly. The 154 // signal handler (see HandleSignals) sends SIGTERM to the process group, which also seems to 155 // make dev_appserver.py exit (possibly less-cleanly). 156 // 157 // It's also possible to do something like this earlier to make the root dev_appserver.py 158 // process receive SIGINT when the test process dies: 159 // 160 // cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGINT} 161 // 162 // PR_SET_PDEATHSIG is Linux-specific and won't help with processes that are started by other 163 // packages, though, and it doesn't seem to be necessary here. 164 const sig = unix.SIGINT 165 if err := unix.Kill(srv.cmd.Process.Pid, sig); err != nil { 166 log.Printf("Failed sending %v to %v: %v", sig, srv.cmd.Process.Pid, err) 167 } 168 return srv.cmd.Wait() 169 } 170 171 // URL returns the app's slash-terminated URL. 172 func (srv *DevAppserver) URL() string { 173 return fmt.Sprintf("http://%v/", srv.Addr()) 174 } 175 176 // Addr returns the address of app's HTTP server, e.g. "localhost:8080". 177 func (srv *DevAppserver) Addr() string { 178 return net.JoinHostPort("localhost", strconv.Itoa(srv.appPort)) 179 } 180 181 // DevAppserverOption can be passed to NewDevAppserver to configure dev_appserver.py. 182 type DevAppserverOption func(*DevAppserver) 183 184 // DevAppserverPort sets the port that the app will listen on for HTTP requests. 185 // If this option is not supplied, an arbitrary open port will be used. 186 func DevAppserverPort(port int) DevAppserverOption { 187 return func(srv *DevAppserver) { srv.appPort = port } 188 } 189 190 // DevAppserverCreateIndexes specifies whether dev_appserver.py should automatically 191 // create datastore indexes in index.yaml. By default, queries fail if they can not 192 // be satisfied using the existing indexes. 193 func DevAppserverCreateIndexes(create bool) DevAppserverOption { 194 return func(srv *DevAppserver) { srv.createIndexes = create } 195 } 196 197 // DevAppserverWatchForChanges specifies whether dev_appserver.py should watch for 198 // changes to the app and rebuild it automatically. Defaults to false. 199 func DevAppserverWatchForChanges(watch bool) DevAppserverOption { 200 return func(srv *DevAppserver) { srv.watchForChanges = watch } 201 }