blob: 682a5ab552087ce6ebfb06b37231e6baaedfc473 [file] [log] [blame]
Bogdan Caprita4d67c042014-08-19 10:41:19 -07001package impl
2
3// The app invoker is responsible for managing the state of applications on the
4// node manager. The node manager manages the applications it installs and runs
5// using the following directory structure:
6//
7// TODO(caprita): Not all is yet implemented.
8//
9// <config.Root>/
Robert Kroegerdd07b362014-09-18 17:34:42 -070010// helper - the setuidhelper binary to invoke an application as a specified user.
Bogdan Caprita4d67c042014-08-19 10:41:19 -070011// app-<hash 1>/ - the application dir is named using a hash of the application title
12// installation-<id 1>/ - installations are labelled with ids
Bogdan Caprita8c776b22014-08-28 17:29:07 -070013// <status> - one of the values for installationState enum
Bogdan Capritabce0a632014-09-03 16:15:26 -070014// origin - object name for application envelope
Bogdan Caprita4d67c042014-08-19 10:41:19 -070015// <version 1 timestamp>/ - timestamp of when the version was downloaded
16// bin - application binary
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -070017// previous - symbolic link to previous version directory
Bogdan Caprita4d67c042014-08-19 10:41:19 -070018// envelope - application envelope (JSON-encoded)
19// <version 2 timestamp>
20// ...
21// current - symbolic link to the current version
22// instances/
23// instance-<id a>/ - instances are labelled with ids
24// root/ - workspace that the instance is run from
25// logs/ - stderr/stdout and log files generated by instance
26// info - app manager name and process id for the instance (if running)
27// version - symbolic link to installation version for the instance
Bogdan Caprita268b4192014-08-28 10:04:44 -070028// <status> - one of the values for instanceState enum
Bogdan Caprita4d67c042014-08-19 10:41:19 -070029// instance-<id b>
30// ...
Bogdan Caprita4d67c042014-08-19 10:41:19 -070031// installation-<id 2>
32// ...
33// app-<hash 2>
34// ...
35//
36// When node manager starts up, it goes through all instances and resumes the
37// ones that are not suspended. If the application was still running, it
38// suspends it first. If an application fails to resume, it stays suspended.
39//
40// When node manager shuts down, it suspends all running instances.
41//
42// Start starts an instance. Suspend kills the process but leaves the workspace
43// untouched. Resume restarts the process. Stop kills the process and prevents
44// future resumes (it also eventually gc's the workspace).
45//
46// If the process dies on its own, it stays dead and is assumed suspended.
47// TODO(caprita): Later, we'll add auto-restart option.
48//
49// Concurrency model: installations can be created independently of one another;
50// installations can be removed at any time (any running instances will be
51// stopped). The first call to Uninstall will rename the installation dir as a
Bogdan Caprita268b4192014-08-28 10:04:44 -070052// first step; subsequent Uninstall's will fail. Instances can be created
Bogdan Caprita4d67c042014-08-19 10:41:19 -070053// independently of one another, as long as the installation exists (if it gets
Bogdan Caprita268b4192014-08-28 10:04:44 -070054// Uninstall'ed during an instance Start, the Start may fail).
55//
56// The status file present in each instance is used to flag the state of the
57// instance and prevent concurrent operations against the instance:
58//
59// - when an instance is created with Start, it is placed in state 'suspended'.
60// To run the instance, Start transitions 'suspended' to 'starting' and then
61// 'started' (upon success) or the instance is deleted (upon failure).
62//
63// - Suspend attempts to transition from 'started' to 'suspending' (if the
64// instance was not in 'started' state, Suspend fails). From 'suspending', the
65// instance transitions to 'suspended' upon success or back to 'started' upon
66// failure.
67//
68// - Resume attempts to transition from 'suspended' to 'starting' (if the
69// instance was not in 'suspended' state, Resume fails). From 'starting', the
70// instance transitions to 'started' upon success or back to 'suspended' upon
71// failure.
72//
73// - Stop attempts to transition from 'started' to 'stopping' and then to
74// 'stopped' (upon success) or back to 'started' (upon failure); or from
75// 'suspended' to 'stopped'. If the initial state is neither 'started' or
76// 'suspended', Stop fails.
Bogdan Caprita4d67c042014-08-19 10:41:19 -070077//
78// TODO(caprita): There is room for synergy between how node manager organizes
79// its own workspace and that for the applications it runs. In particular,
80// previous, origin, and envelope could be part of a single config. We'll
81// refine that later.
82
83import (
84 "crypto/md5"
85 "encoding/base64"
86 "encoding/binary"
87 "encoding/json"
88 "fmt"
89 "hash/crc64"
90 "io/ioutil"
91 "os"
92 "os/exec"
Robert Kroegerdd07b362014-09-18 17:34:42 -070093 "os/user"
Bogdan Caprita4d67c042014-08-19 10:41:19 -070094 "path/filepath"
Bogdan Capritabce0a632014-09-03 16:15:26 -070095 "reflect"
Bogdan Caprita4d67c042014-08-19 10:41:19 -070096 "strings"
97 "time"
98
Jiri Simsa519c5072014-09-17 21:37:57 -070099 "veyron.io/veyron/veyron/lib/config"
100 vexec "veyron.io/veyron/veyron/services/mgmt/lib/exec"
101 iconfig "veyron.io/veyron/veyron/services/mgmt/node/config"
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700102
Jiri Simsa519c5072014-09-17 21:37:57 -0700103 "veyron.io/veyron/veyron2/context"
104 "veyron.io/veyron/veyron2/ipc"
105 "veyron.io/veyron/veyron2/mgmt"
106 "veyron.io/veyron/veyron2/naming"
107 "veyron.io/veyron/veyron2/rt"
108 "veyron.io/veyron/veyron2/services/mgmt/appcycle"
109 "veyron.io/veyron/veyron2/services/mgmt/application"
110 "veyron.io/veyron/veyron2/vlog"
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700111)
112
113// instanceInfo holds state about a running instance.
114type instanceInfo struct {
115 AppCycleMgrName string
116 Pid int
117}
118
119func saveInstanceInfo(dir string, info *instanceInfo) error {
120 jsonInfo, err := json.Marshal(info)
121 if err != nil {
122 vlog.Errorf("Marshal(%v) failed: %v", info, err)
123 return errOperationFailed
124 }
125 infoPath := filepath.Join(dir, "info")
126 if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil {
127 vlog.Errorf("WriteFile(%v) failed: %v", infoPath, err)
128 return errOperationFailed
129 }
130 return nil
131}
132
133func loadInstanceInfo(dir string) (*instanceInfo, error) {
134 infoPath := filepath.Join(dir, "info")
135 info := new(instanceInfo)
136 if infoBytes, err := ioutil.ReadFile(infoPath); err != nil {
137 vlog.Errorf("ReadFile(%v) failed: %v", infoPath, err)
138 return nil, errOperationFailed
139 } else if err := json.Unmarshal(infoBytes, info); err != nil {
140 vlog.Errorf("Unmarshal(%v) failed: %v", infoBytes, err)
141 return nil, errOperationFailed
142 }
143 return info, nil
144}
145
146// appInvoker holds the state of an application-related method invocation.
147type appInvoker struct {
148 callback *callbackState
149 config *iconfig.State
150 // suffix contains the name components of the current invocation name
151 // suffix. It is used to identify an application, installation, or
152 // instance.
153 suffix []string
154}
155
156func saveEnvelope(dir string, envelope *application.Envelope) error {
157 jsonEnvelope, err := json.Marshal(envelope)
158 if err != nil {
159 vlog.Errorf("Marshal(%v) failed: %v", envelope, err)
160 return errOperationFailed
161 }
Bogdan Capritabce0a632014-09-03 16:15:26 -0700162 path := filepath.Join(dir, "envelope")
163 if err := ioutil.WriteFile(path, jsonEnvelope, 0600); err != nil {
164 vlog.Errorf("WriteFile(%v) failed: %v", path, err)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700165 return errOperationFailed
166 }
167 return nil
168}
169
170func loadEnvelope(dir string) (*application.Envelope, error) {
Bogdan Capritabce0a632014-09-03 16:15:26 -0700171 path := filepath.Join(dir, "envelope")
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700172 envelope := new(application.Envelope)
Bogdan Capritabce0a632014-09-03 16:15:26 -0700173 if envelopeBytes, err := ioutil.ReadFile(path); err != nil {
174 vlog.Errorf("ReadFile(%v) failed: %v", path, err)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700175 return nil, errOperationFailed
176 } else if err := json.Unmarshal(envelopeBytes, envelope); err != nil {
177 vlog.Errorf("Unmarshal(%v) failed: %v", envelopeBytes, err)
178 return nil, errOperationFailed
179 }
180 return envelope, nil
181}
182
183func saveOrigin(dir, originVON string) error {
184 path := filepath.Join(dir, "origin")
185 if err := ioutil.WriteFile(path, []byte(originVON), 0600); err != nil {
186 vlog.Errorf("WriteFile(%v) failed: %v", path, err)
187 return errOperationFailed
188 }
189 return nil
190}
191
Bogdan Capritabce0a632014-09-03 16:15:26 -0700192func loadOrigin(dir string) (string, error) {
193 path := filepath.Join(dir, "origin")
194 if originBytes, err := ioutil.ReadFile(path); err != nil {
195 vlog.Errorf("ReadFile(%v) failed: %v", path, err)
196 return "", errOperationFailed
197 } else {
198 return string(originBytes), nil
199 }
200}
201
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700202// generateID returns a new unique id string. The uniqueness is based on the
203// current timestamp. Not cryptographically secure.
204func generateID() string {
205 timestamp := fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano))
206 h := crc64.New(crc64.MakeTable(crc64.ISO))
207 h.Write([]byte(timestamp))
208 b := make([]byte, 8)
209 binary.LittleEndian.PutUint64(b, uint64(h.Sum64()))
210 return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
211}
212
213// TODO(caprita): Nothing prevents different applications from sharing the same
214// title, and thereby being installed in the same app dir. Do we want to
215// prevent that for the same user or across users?
216
217// applicationDirName generates a cryptographic hash of the application title,
218// to be used as a directory name for installations of the application with the
219// given title.
220func applicationDirName(title string) string {
221 h := md5.New()
222 h.Write([]byte(title))
223 hash := strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=")
224 return "app-" + hash
225}
226
227func installationDirName(installationID string) string {
228 return "installation-" + installationID
229}
230
231func instanceDirName(instanceID string) string {
232 return "instance-" + instanceID
233}
234
Bogdan Caprita268b4192014-08-28 10:04:44 -0700235func mkdir(dir string) error {
236 perm := os.FileMode(0700)
237 if err := os.MkdirAll(dir, perm); err != nil {
238 vlog.Errorf("MkdirAll(%v, %v) failed: %v", dir, perm, err)
239 return err
240 }
241 return nil
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700242}
243
Bogdan Capritabce0a632014-09-03 16:15:26 -0700244func fetchAppEnvelope(ctx context.T, origin string) (*application.Envelope, error) {
245 envelope, err := fetchEnvelope(ctx, origin)
246 if err != nil {
247 return nil, err
248 }
249 if envelope.Title == application.NodeManagerTitle {
250 // Disallow node manager apps from being installed like a
251 // regular app.
252 return nil, errInvalidOperation
253 }
254 return envelope, nil
255}
256
257// newVersion sets up the directory for a new application version.
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700258func newVersion(installationDir string, envelope *application.Envelope, oldVersionDir string) (string, error) {
Bogdan Capritabce0a632014-09-03 16:15:26 -0700259 versionDir := filepath.Join(installationDir, generateVersionDirName())
260 if err := mkdir(versionDir); err != nil {
261 return "", errOperationFailed
262 }
263 // TODO(caprita): Share binaries if already existing locally.
264 if err := generateBinary(versionDir, "bin", envelope, true); err != nil {
265 return versionDir, err
266 }
267 if err := saveEnvelope(versionDir, envelope); err != nil {
268 return versionDir, err
269 }
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700270 if oldVersionDir != "" {
271 previousLink := filepath.Join(versionDir, "previous")
272 if err := os.Symlink(oldVersionDir, previousLink); err != nil {
273 vlog.Errorf("Symlink(%v, %v) failed: %v", oldVersionDir, previousLink, err)
274 return versionDir, errOperationFailed
275 }
276 }
Bogdan Capritabce0a632014-09-03 16:15:26 -0700277 // updateLink should be the last thing we do, after we've ensured the
278 // new version is viable (currently, that just means it installs
279 // properly).
280 return versionDir, updateLink(versionDir, filepath.Join(installationDir, "current"))
281}
282
283func (i *appInvoker) Install(_ ipc.ServerContext, applicationVON string) (string, error) {
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700284 if len(i.suffix) > 0 {
285 return "", errInvalidSuffix
286 }
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700287 ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
288 defer cancel()
Bogdan Capritabce0a632014-09-03 16:15:26 -0700289 envelope, err := fetchAppEnvelope(ctx, applicationVON)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700290 if err != nil {
291 return "", err
292 }
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700293 installationID := generateID()
294 installationDir := filepath.Join(i.config.Root, applicationDirName(envelope.Title), installationDirName(installationID))
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700295 deferrer := func() {
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700296 if err := os.RemoveAll(installationDir); err != nil {
Bogdan Capritabce0a632014-09-03 16:15:26 -0700297 vlog.Errorf("RemoveAll(%v) failed: %v", installationDir, err)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700298 }
299 }
300 defer func() {
301 if deferrer != nil {
302 deferrer()
303 }
304 }()
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700305 if _, err := newVersion(installationDir, envelope, ""); err != nil {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700306 return "", err
307 }
Bogdan Capritabce0a632014-09-03 16:15:26 -0700308 if err := saveOrigin(installationDir, applicationVON); err != nil {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700309 return "", err
310 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700311 if err := initializeInstallation(installationDir, active); err != nil {
312 return "", err
313 }
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700314 deferrer = nil
315 return naming.Join(envelope.Title, installationID), nil
316}
317
318func (*appInvoker) Refresh(ipc.ServerContext) error {
319 // TODO(jsimsa): Implement.
320 return nil
321}
322
323func (*appInvoker) Restart(ipc.ServerContext) error {
324 // TODO(jsimsa): Implement.
325 return nil
326}
327
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700328func openWriteFile(path string) (*os.File, error) {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700329 perm := os.FileMode(0600)
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700330 file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, perm)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700331 if err != nil {
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700332 vlog.Errorf("OpenFile(%v) failed: %v", path, err)
333 return nil, errOperationFailed
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700334 }
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700335 return file, nil
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700336}
337
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700338// installationDir returns the path to the directory containing the app
339// installation referred to by the invoker's suffix. Returns an error if the
340// suffix does not name an installation or if the named installation does not
341// exist.
342func (i *appInvoker) installationDir() (string, error) {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700343 components := i.suffix
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700344 if nComponents := len(components); nComponents != 2 {
345 return "", errInvalidSuffix
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700346 }
347 app, installation := components[0], components[1]
348 installationDir := filepath.Join(i.config.Root, applicationDirName(app), installationDirName(installation))
349 if _, err := os.Stat(installationDir); err != nil {
350 if os.IsNotExist(err) {
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700351 return "", errNotExist
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700352 }
353 vlog.Errorf("Stat(%v) failed: %v", installationDir, err)
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700354 return "", errOperationFailed
355 }
356 return installationDir, nil
357}
358
Bogdan Caprita268b4192014-08-28 10:04:44 -0700359// newInstance sets up the directory for a new application instance.
360func (i *appInvoker) newInstance() (string, string, error) {
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700361 installationDir, err := i.installationDir()
362 if err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700363 return "", "", err
364 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700365 if !installationStateIs(installationDir, active) {
366 return "", "", errInvalidOperation
367 }
Bogdan Caprita268b4192014-08-28 10:04:44 -0700368 instanceID := generateID()
369 instanceDir := filepath.Join(installationDir, "instances", instanceDirName(instanceID))
370 if mkdir(instanceDir) != nil {
371 return "", instanceID, errOperationFailed
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700372 }
373 currLink := filepath.Join(installationDir, "current")
Bogdan Caprita268b4192014-08-28 10:04:44 -0700374 versionDir, err := filepath.EvalSymlinks(currLink)
375 if err != nil {
376 vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err)
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700377 return instanceDir, instanceID, errOperationFailed
Bogdan Caprita268b4192014-08-28 10:04:44 -0700378 }
379 versionLink := filepath.Join(instanceDir, "version")
380 if err := os.Symlink(versionDir, versionLink); err != nil {
381 vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)
382 return instanceDir, instanceID, errOperationFailed
383 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700384 if err := initializeInstance(instanceDir, suspended); err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700385 return instanceDir, instanceID, err
386 }
387 return instanceDir, instanceID, nil
388}
389
Robert Kroegerdd07b362014-09-18 17:34:42 -0700390// TODO(rjkroege): Turning on the setuid feature of the suidhelper
391// requires an installer with root permissions to install it in <config.Root>/helper
392func genCmd(instanceDir string, helperPath string) (*exec.Cmd, error) {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700393 versionLink := filepath.Join(instanceDir, "version")
394 versionDir, err := filepath.EvalSymlinks(versionLink)
395 if err != nil {
396 vlog.Errorf("EvalSymlinks(%v) failed: %v", versionLink, err)
397 return nil, errOperationFailed
398 }
399 envelope, err := loadEnvelope(versionDir)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700400 if err != nil {
401 return nil, err
402 }
Bogdan Caprita268b4192014-08-28 10:04:44 -0700403 binPath := filepath.Join(versionDir, "bin")
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700404 if _, err := os.Stat(binPath); err != nil {
405 vlog.Errorf("Stat(%v) failed: %v", binPath, err)
406 return nil, errOperationFailed
407 }
Robert Kroegerdd07b362014-09-18 17:34:42 -0700408
409 helperStat, err := os.Stat(helperPath)
410 if err != nil {
411 vlog.Errorf("Stat(%v) failed: %v. helper is required.", helperPath, err)
412 return nil, errOperationFailed
413 }
414 cmd := exec.Command(helperPath)
415
416 cmd.Args = append(cmd.Args, "--username")
417 if helperStat.Mode()&os.ModeSetuid == 0 {
418 vlog.Errorf("helper not setuid. Node manager will invoke app with its own userid")
419 user, err := user.Current()
420 if err != nil {
421 vlog.Errorf("user.Current() failed: %v", err)
422 return nil, errOperationFailed
423 }
424 cmd.Args = append(cmd.Args, user.Username)
425 } else {
426 // TODO(rjkroege): Use the username associated with the veyron identity.
427 return nil, errOperationFailed
428 }
429
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700430 // TODO(caprita): Also pass in configuration info like NAMESPACE_ROOT to
431 // the app (to point to the device mounttable).
432 cmd.Env = envelope.Env
433 rootDir := filepath.Join(instanceDir, "root")
434 if err := mkdir(rootDir); err != nil {
435 return nil, err
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700436 }
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700437 cmd.Dir = rootDir
Robert Kroegerdd07b362014-09-18 17:34:42 -0700438 cmd.Args = append(cmd.Args, "--workspace")
439 cmd.Args = append(cmd.Args, rootDir)
440
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700441 logDir := filepath.Join(instanceDir, "logs")
442 if err := mkdir(logDir); err != nil {
443 return nil, err
444 }
445 timestamp := time.Now().UnixNano()
Robert Kroegerdd07b362014-09-18 17:34:42 -0700446 stdoutLog := filepath.Join(logDir, fmt.Sprintf("STDOUT-%d", timestamp))
447 if cmd.Stdout, err = openWriteFile(stdoutLog); err != nil {
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700448 return nil, err
449 }
Robert Kroegerdd07b362014-09-18 17:34:42 -0700450 cmd.Args = append(cmd.Args, "--stdoutlog")
451 cmd.Args = append(cmd.Args, stdoutLog)
452
453 stderrLog := filepath.Join(logDir, fmt.Sprintf("STDERR-%d", timestamp))
454 if cmd.Stderr, err = openWriteFile(stderrLog); err != nil {
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700455 return nil, err
456 }
Robert Kroegerdd07b362014-09-18 17:34:42 -0700457 cmd.Args = append(cmd.Args, "--stderrlog")
458 cmd.Args = append(cmd.Args, stderrLog)
459
460 cmd.Args = append(cmd.Args, "--run")
461 cmd.Args = append(cmd.Args, binPath)
462 cmd.Args = append(cmd.Args, "--")
463
Bogdan Caprita25d4faa2014-08-28 10:21:23 -0700464 // Set up args and env.
465 cmd.Args = append(cmd.Args, "--log_dir=../logs")
466 cmd.Args = append(cmd.Args, envelope.Args...)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700467 return cmd, nil
468}
469
470func (i *appInvoker) startCmd(instanceDir string, cmd *exec.Cmd) error {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700471 // Setup up the child process callback.
472 callbackState := i.callback
Bogdan Caprita78b62162014-08-21 15:35:08 -0700473 listener := callbackState.listenFor(mgmt.AppCycleManagerConfigKey)
474 defer listener.cleanup()
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700475 cfg := config.New()
Bogdan Caprita78b62162014-08-21 15:35:08 -0700476 cfg.Set(mgmt.ParentNodeManagerConfigKey, listener.name())
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700477 handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
Bogdan Caprita268b4192014-08-28 10:04:44 -0700478 defer func() {
479 if handle != nil {
480 if err := handle.Clean(); err != nil {
481 vlog.Errorf("Clean() failed: %v", err)
482 }
483 }
484 }()
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700485 // Start the child process.
486 if err := handle.Start(); err != nil {
487 vlog.Errorf("Start() failed: %v", err)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700488 return errOperationFailed
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700489 }
490 // Wait for the child process to start.
Bogdan Caprita78b62162014-08-21 15:35:08 -0700491 timeout := 10 * time.Second
492 if err := handle.WaitForReady(timeout); err != nil {
493 vlog.Errorf("WaitForReady(%v) failed: %v", timeout, err)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700494 return errOperationFailed
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700495 }
Bogdan Caprita78b62162014-08-21 15:35:08 -0700496 childName, err := listener.waitForValue(timeout)
497 if err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700498 return errOperationFailed
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700499 }
Bogdan Caprita78b62162014-08-21 15:35:08 -0700500 instanceInfo := &instanceInfo{
501 AppCycleMgrName: childName,
502 Pid: handle.Pid(),
503 }
504 if err := saveInstanceInfo(instanceDir, instanceInfo); err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700505 return err
Bogdan Caprita78b62162014-08-21 15:35:08 -0700506 }
507 // TODO(caprita): Spin up a goroutine to reap child status upon exit and
508 // transition it to suspended state if it exits on its own.
Bogdan Caprita268b4192014-08-28 10:04:44 -0700509 handle = nil
510 return nil
511}
512
513func (i *appInvoker) run(instanceDir string) error {
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700514 if err := transitionInstance(instanceDir, suspended, starting); err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700515 return err
516 }
Robert Kroegerdd07b362014-09-18 17:34:42 -0700517 cmd, err := genCmd(instanceDir, filepath.Join(i.config.Root, "helper"))
Bogdan Caprita268b4192014-08-28 10:04:44 -0700518 if err == nil {
519 err = i.startCmd(instanceDir, cmd)
520 }
521 if err != nil {
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700522 transitionInstance(instanceDir, starting, suspended)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700523 return err
524 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700525 return transitionInstance(instanceDir, starting, started)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700526}
527
528func (i *appInvoker) Start(ipc.ServerContext) ([]string, error) {
529 instanceDir, instanceID, err := i.newInstance()
530 if err == nil {
531 err = i.run(instanceDir)
532 }
533 if err != nil {
Bogdan Capritabce0a632014-09-03 16:15:26 -0700534 if err := os.RemoveAll(instanceDir); err != nil {
535 vlog.Errorf("RemoveAll(%v) failed: %v", instanceDir, err)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700536 }
537 return nil, err
538 }
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700539 return []string{instanceID}, nil
540}
541
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700542// instanceDir returns the path to the directory containing the app instance
543// referred to by the invoker's suffix, as well as the corresponding stopped
544// instance dir. Returns an error if the suffix does not name an instance.
Bogdan Caprita268b4192014-08-28 10:04:44 -0700545func (i *appInvoker) instanceDir() (string, error) {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700546 components := i.suffix
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700547 if nComponents := len(components); nComponents != 3 {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700548 return "", errInvalidSuffix
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700549 }
550 app, installation, instance := components[0], components[1], components[2]
551 instancesDir := filepath.Join(i.config.Root, applicationDirName(app), installationDirName(installation), "instances")
552 instanceDir := filepath.Join(instancesDir, instanceDirName(instance))
Bogdan Caprita268b4192014-08-28 10:04:44 -0700553 return instanceDir, nil
554}
555
556func (i *appInvoker) Resume(ipc.ServerContext) error {
557 instanceDir, err := i.instanceDir()
558 if err != nil {
559 return err
560 }
561 return i.run(instanceDir)
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700562}
563
564func stopAppRemotely(appVON string) error {
565 appStub, err := appcycle.BindAppCycle(appVON)
566 if err != nil {
567 vlog.Errorf("BindAppCycle(%v) failed: %v", appVON, err)
568 return errOperationFailed
569 }
570 ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
571 defer cancel()
572 stream, err := appStub.Stop(ctx)
573 if err != nil {
574 vlog.Errorf("%v.Stop() failed: %v", appVON, err)
575 return errOperationFailed
576 }
577 rstream := stream.RecvStream()
578 for rstream.Advance() {
579 vlog.VI(2).Infof("%v.Stop() task update: %v", appVON, rstream.Value())
580 }
581 if err := rstream.Err(); err != nil {
582 vlog.Errorf("Advance() failed: %v", err)
583 return errOperationFailed
584 }
585 if err := stream.Finish(); err != nil {
586 vlog.Errorf("Finish() failed: %v", err)
587 return errOperationFailed
588 }
589 return nil
590}
591
Bogdan Caprita268b4192014-08-28 10:04:44 -0700592func stop(instanceDir string) error {
593 info, err := loadInstanceInfo(instanceDir)
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700594 if err != nil {
595 return err
596 }
Bogdan Caprita2968f4b2014-08-22 14:11:58 -0700597 return stopAppRemotely(info.AppCycleMgrName)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700598}
599
Bogdan Caprita268b4192014-08-28 10:04:44 -0700600// TODO(caprita): implement deadline for Stop.
601
602func (i *appInvoker) Stop(_ ipc.ServerContext, deadline uint32) error {
603 instanceDir, err := i.instanceDir()
604 if err != nil {
605 return err
606 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700607 if err := transitionInstance(instanceDir, suspended, stopped); err == errOperationFailed || err == nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700608 return err
609 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700610 if err := transitionInstance(instanceDir, started, stopping); err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700611 return err
612 }
613 if err := stop(instanceDir); err != nil {
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700614 transitionInstance(instanceDir, stopping, started)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700615 return err
616 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700617 return transitionInstance(instanceDir, stopping, stopped)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700618}
619
620func (i *appInvoker) Suspend(ipc.ServerContext) error {
621 instanceDir, err := i.instanceDir()
622 if err != nil {
623 return err
624 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700625 if err := transitionInstance(instanceDir, started, suspending); err != nil {
Bogdan Caprita268b4192014-08-28 10:04:44 -0700626 return err
627 }
628 if err := stop(instanceDir); err != nil {
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700629 transitionInstance(instanceDir, suspending, started)
Bogdan Caprita268b4192014-08-28 10:04:44 -0700630 return err
631 }
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700632 return transitionInstance(instanceDir, suspending, suspended)
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700633}
634
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700635func (i *appInvoker) Uninstall(ipc.ServerContext) error {
636 installationDir, err := i.installationDir()
637 if err != nil {
638 return err
639 }
640 return transitionInstallation(installationDir, active, uninstalled)
641}
642
Bogdan Capritabce0a632014-09-03 16:15:26 -0700643func (i *appInvoker) Update(ipc.ServerContext) error {
644 installationDir, err := i.installationDir()
645 if err != nil {
646 return err
647 }
648 if !installationStateIs(installationDir, active) {
649 return errInvalidOperation
650 }
651 originVON, err := loadOrigin(installationDir)
652 if err != nil {
653 return err
654 }
655 ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
656 defer cancel()
657 newEnvelope, err := fetchAppEnvelope(ctx, originVON)
658 if err != nil {
659 return err
660 }
661 currLink := filepath.Join(installationDir, "current")
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700662 oldVersionDir, err := filepath.EvalSymlinks(currLink)
663 if err != nil {
664 vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err)
665 return errOperationFailed
666 }
Bogdan Capritabce0a632014-09-03 16:15:26 -0700667 // NOTE(caprita): A race can occur between two competing updates, where
668 // both use the old version as their baseline. This can result in both
669 // updates succeeding even if they are updating the app installation to
670 // the same new envelope. This will result in one of the updates
671 // becoming the new 'current'. Both versions will point their
672 // 'previous' link to the old version. This doesn't appear to be of
673 // practical concern, so we avoid the complexity of synchronizing
674 // updates.
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700675 oldEnvelope, err := loadEnvelope(oldVersionDir)
Bogdan Capritabce0a632014-09-03 16:15:26 -0700676 if err != nil {
677 return err
678 }
679 if oldEnvelope.Title != newEnvelope.Title {
680 return errIncompatibleUpdate
681 }
682 if reflect.DeepEqual(oldEnvelope, newEnvelope) {
683 return errUpdateNoOp
684 }
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700685 versionDir, err := newVersion(installationDir, newEnvelope, oldVersionDir)
Bogdan Capritabce0a632014-09-03 16:15:26 -0700686 if err != nil {
687 if err := os.RemoveAll(versionDir); err != nil {
688 vlog.Errorf("RemoveAll(%v) failed: %v", versionDir, err)
689 }
690 return err
691 }
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700692 return nil
693}
694
Bogdan Caprita8c776b22014-08-28 17:29:07 -0700695func (*appInvoker) UpdateTo(_ ipc.ServerContext, von string) error {
Bogdan Caprita4d67c042014-08-19 10:41:19 -0700696 // TODO(jsimsa): Implement.
697 return nil
698}
Bogdan Caprita53b7b7e2014-09-03 20:51:16 -0700699
700func (i *appInvoker) Revert(ipc.ServerContext) error {
701 installationDir, err := i.installationDir()
702 if err != nil {
703 return err
704 }
705 if !installationStateIs(installationDir, active) {
706 return errInvalidOperation
707 }
708 // NOTE(caprita): A race can occur between an update and a revert, where
709 // both use the same current version as their starting point. This will
710 // render the update inconsequential. This doesn't appear to be of
711 // practical concern, so we avoid the complexity of synchronizing
712 // updates and revert operations.
713 currLink := filepath.Join(installationDir, "current")
714 currVersionDir, err := filepath.EvalSymlinks(currLink)
715 if err != nil {
716 vlog.Errorf("EvalSymlinks(%v) failed: %v", currLink, err)
717 return errOperationFailed
718 }
719 previousLink := filepath.Join(currVersionDir, "previous")
720 if _, err := os.Lstat(previousLink); err != nil {
721 if os.IsNotExist(err) {
722 // No 'previous' link -- must be the first version.
723 return errUpdateNoOp
724 }
725 vlog.Errorf("Lstat(%v) failed: %v", previousLink, err)
726 return errOperationFailed
727 }
728 prevVersionDir, err := filepath.EvalSymlinks(previousLink)
729 if err != nil {
730 vlog.Errorf("EvalSymlinks(%v) failed: %v", previousLink, err)
731 return errOperationFailed
732 }
733 return updateLink(prevVersionDir, currLink)
734}