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	appserverArgs = append(appserverArgs, filepath.Join(i.appDir, "app"))
205
206	i.child = exec.Command(python,
207		appserverArgs...,
208	)
209	i.child.Stdout = os.Stdout
210	var stderr io.Reader
211	stderr, err = i.child.StderrPipe()
212	if err != nil {
213		return err
214	}
215	stderr = io.TeeReader(stderr, os.Stderr)
216	if err = i.child.Start(); err != nil {
217		return err
218	}
219
220	// Read stderr until we have read the URLs of the API server and admin interface.
221	errc := make(chan error, 1)
222	go func() {
223		s := bufio.NewScanner(stderr)
224		for s.Scan() {
225			if match := apiServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
226				u, err := url.Parse(match[1])
227				if err != nil {
228					errc <- fmt.Errorf("failed to parse API URL %q: %v", match[1], err)
229					return
230				}
231				i.apiURL = u
232			}
233			if match := adminServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
234				i.adminURL = match[1]
235			}
236			if i.adminURL != "" && i.apiURL != nil {
237				break
238			}
239		}
240		errc <- s.Err()
241	}()
242
243	select {
244	case <-time.After(i.startupTimeout):
245		if p := i.child.Process; p != nil {
246			p.Kill()
247		}
248		return errors.New("timeout starting child process")
249	case err := <-errc:
250		if err != nil {
251			return fmt.Errorf("error reading child process stderr: %v", err)
252		}
253	}
254	if i.adminURL == "" {
255		return errors.New("unable to find admin server URL")
256	}
257	if i.apiURL == nil {
258		return errors.New("unable to find API server URL")
259	}
260	return nil
261}
262
263func (i *instance) appYAML() string {
264	return fmt.Sprintf(appYAMLTemplate, i.appID)
265}
266
267const appYAMLTemplate = `
268application: %s
269version: 1
270runtime: go
271api_version: go1
272
273handlers:
274- url: /.*
275  script: _go_app
276`
277
278const appSource = `
279package main
280import "google.golang.org/appengine"
281func main() { appengine.Main() }
282`
283