1// +build !appengine
2
3package aetest
4
5import (
6	"bufio"
7	"crypto/rand"
8	"errors"
9	"fmt"
10	"io"
11	"io/ioutil"
12	"net/http"
13	"net/url"
14	"os"
15	"os/exec"
16	"path/filepath"
17	"regexp"
18	"time"
19
20	"golang.org/x/net/context"
21	"google.golang.org/appengine/internal"
22)
23
24// NewInstance launches a running instance of api_server.py which can be used
25// for multiple test Contexts that delegate all App Engine API calls to that
26// instance.
27// If opts is nil the default values are used.
28func NewInstance(opts *Options) (Instance, error) {
29	i := &instance{
30		opts:           opts,
31		appID:          "testapp",
32		startupTimeout: 15 * time.Second,
33	}
34	if opts != nil {
35		if opts.AppID != "" {
36			i.appID = opts.AppID
37		}
38		if opts.StartupTimeout > 0 {
39			i.startupTimeout = opts.StartupTimeout
40		}
41	}
42	if err := i.startChild(); err != nil {
43		return nil, err
44	}
45	return i, nil
46}
47
48func newSessionID() string {
49	var buf [16]byte
50	io.ReadFull(rand.Reader, buf[:])
51	return fmt.Sprintf("%x", buf[:])
52}
53
54// instance implements the Instance interface.
55type instance struct {
56	opts           *Options
57	child          *exec.Cmd
58	apiURL         *url.URL // base URL of API HTTP server
59	adminURL       string   // base URL of admin HTTP server
60	appDir         string
61	appID          string
62	startupTimeout time.Duration
63	relFuncs       []func() // funcs to release any associated contexts
64}
65
66// NewRequest returns an *http.Request associated with this instance.
67func (i *instance) NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
68	req, err := http.NewRequest(method, urlStr, body)
69	if err != nil {
70		return nil, err
71	}
72
73	// Associate this request.
74	req, release := internal.RegisterTestRequest(req, i.apiURL, func(ctx context.Context) context.Context {
75		ctx = internal.WithAppIDOverride(ctx, "dev~"+i.appID)
76		return ctx
77	})
78	i.relFuncs = append(i.relFuncs, release)
79
80	return req, nil
81}
82
83// Close kills the child api_server.py process, releasing its resources.
84func (i *instance) Close() (err error) {
85	for _, rel := range i.relFuncs {
86		rel()
87	}
88	i.relFuncs = nil
89	child := i.child
90	if child == nil {
91		return nil
92	}
93	defer func() {
94		i.child = nil
95		err1 := os.RemoveAll(i.appDir)
96		if err == nil {
97			err = err1
98		}
99	}()
100
101	if p := child.Process; p != nil {
102		errc := make(chan error, 1)
103		go func() {
104			errc <- child.Wait()
105		}()
106
107		// Call the quit handler on the admin server.
108		res, err := http.Get(i.adminURL + "/quit")
109		if err != nil {
110			p.Kill()
111			return fmt.Errorf("unable to call /quit handler: %v", err)
112		}
113		res.Body.Close()
114		select {
115		case <-time.After(15 * time.Second):
116			p.Kill()
117			return errors.New("timeout killing child process")
118		case err = <-errc:
119			// Do nothing.
120		}
121	}
122	return
123}
124
125func fileExists(path string) bool {
126	_, err := os.Stat(path)
127	return err == nil
128}
129
130func findPython() (path string, err error) {
131	for _, name := range []string{"python2.7", "python"} {
132		path, err = exec.LookPath(name)
133		if err == nil {
134			return
135		}
136	}
137	return
138}
139
140func findDevAppserver() (string, error) {
141	if p := os.Getenv("APPENGINE_DEV_APPSERVER"); p != "" {
142		if fileExists(p) {
143			return p, nil
144		}
145		return "", fmt.Errorf("invalid APPENGINE_DEV_APPSERVER environment variable; path %q doesn't exist", p)
146	}
147	return exec.LookPath("dev_appserver.py")
148}
149
150var apiServerAddrRE = regexp.MustCompile(`Starting API server at: (\S+)`)
151var adminServerAddrRE = regexp.MustCompile(`Starting admin server at: (\S+)`)
152
153func (i *instance) startChild() (err error) {
154	if PrepareDevAppserver != nil {
155		if err := PrepareDevAppserver(); err != nil {
156			return err
157		}
158	}
159	python, err := findPython()
160	if err != nil {
161		return fmt.Errorf("Could not find python interpreter: %v", err)
162	}
163	devAppserver, err := findDevAppserver()
164	if err != nil {
165		return fmt.Errorf("Could not find dev_appserver.py: %v", err)
166	}
167
168	i.appDir, err = ioutil.TempDir("", "appengine-aetest")
169	if err != nil {
170		return err
171	}
172	defer func() {
173		if err != nil {
174			os.RemoveAll(i.appDir)
175		}
176	}()
177	err = os.Mkdir(filepath.Join(i.appDir, "app"), 0755)
178	if err != nil {
179		return err
180	}
181	err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "app.yaml"), []byte(i.appYAML()), 0644)
182	if err != nil {
183		return err
184	}
185	err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "stubapp.go"), []byte(appSource), 0644)
186	if err != nil {
187		return err
188	}
189
190	appserverArgs := []string{
191		devAppserver,
192		"--port=0",
193		"--api_port=0",
194		"--admin_port=0",
195		"--automatic_restart=false",
196		"--skip_sdk_update_check=true",
197		"--clear_datastore=true",
198		"--clear_search_indexes=true",
199		"--datastore_path", filepath.Join(i.appDir, "datastore"),
200	}
201	if i.opts != nil && i.opts.StronglyConsistentDatastore {
202		appserverArgs = append(appserverArgs, "--datastore_consistency_policy=consistent")
203	}
204	if i.opts != nil && i.opts.SupportDatastoreEmulator != nil {
205		appserverArgs = append(appserverArgs, fmt.Sprintf("--support_datastore_emulator=%t", *i.opts.SupportDatastoreEmulator))
206	}
207	appserverArgs = append(appserverArgs, filepath.Join(i.appDir, "app"))
208
209	i.child = exec.Command(python,
210		appserverArgs...,
211	)
212	i.child.Stdout = os.Stdout
213	var stderr io.Reader
214	stderr, err = i.child.StderrPipe()
215	if err != nil {
216		return err
217	}
218
219	if err = i.child.Start(); err != nil {
220		return err
221	}
222
223	// Read stderr until we have read the URLs of the API server and admin interface.
224	errc := make(chan error, 1)
225	go func() {
226		s := bufio.NewScanner(stderr)
227		for s.Scan() {
228			// Pass stderr along as we go so the user can see it.
229			if !(i.opts != nil && i.opts.SuppressDevAppServerLog) {
230				fmt.Fprintln(os.Stderr, s.Text())
231			}
232			if match := apiServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
233				u, err := url.Parse(match[1])
234				if err != nil {
235					errc <- fmt.Errorf("failed to parse API URL %q: %v", match[1], err)
236					return
237				}
238				i.apiURL = u
239			}
240			if match := adminServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
241				i.adminURL = match[1]
242			}
243			if i.adminURL != "" && i.apiURL != nil {
244				// Pass along stderr to the user after we're done with it.
245				if !(i.opts != nil && i.opts.SuppressDevAppServerLog) {
246					go io.Copy(os.Stderr, stderr)
247				}
248				break
249			}
250		}
251		errc <- s.Err()
252	}()
253
254	select {
255	case <-time.After(i.startupTimeout):
256		if p := i.child.Process; p != nil {
257			p.Kill()
258		}
259		return errors.New("timeout starting child process")
260	case err := <-errc:
261		if err != nil {
262			return fmt.Errorf("error reading child process stderr: %v", err)
263		}
264	}
265	if i.adminURL == "" {
266		return errors.New("unable to find admin server URL")
267	}
268	if i.apiURL == nil {
269		return errors.New("unable to find API server URL")
270	}
271	return nil
272}
273
274func (i *instance) appYAML() string {
275	return fmt.Sprintf(appYAMLTemplate, i.appID)
276}
277
278const appYAMLTemplate = `
279application: %s
280version: 1
281runtime: go111
282
283handlers:
284- url: /.*
285  script: _go_app
286`
287
288const appSource = `
289package main
290import "google.golang.org/appengine"
291func main() { appengine.Main() }
292`
293