Browse code

engine/spawn: run an engine in a subprocess, remote-controlled by Beam

Docker-DCO-1.1-Signed-off-by: Solomon Hykes <solomon@docker.com> (github: shykes)

Solomon Hykes authored on 2014/04/27 10:47:20
Showing 2 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,119 @@
0
+package spawn
1
+
2
+import (
3
+	"fmt"
4
+	"github.com/dotcloud/docker/engine"
5
+	"github.com/dotcloud/docker/pkg/beam"
6
+	"github.com/dotcloud/docker/utils"
7
+	"os"
8
+	"os/exec"
9
+)
10
+
11
+var initCalled bool
12
+
13
+// Init checks if the current process has been created by Spawn.
14
+//
15
+// If no, it returns nil and the original program can continue
16
+// unmodified.
17
+//
18
+// If no, it hijacks the process to run as a child worker controlled
19
+// by its parent over a beam connection, with f exposed as a remote
20
+// service. In this case Init never returns.
21
+//
22
+// The hijacking process takes place as follows:
23
+//	- Open file descriptor 3 as a beam endpoint. If this fails,
24
+//	terminate the current process.
25
+//	- Start a new engine.
26
+//	- Call f.Install on the engine. Any handlers registered
27
+//	will be available for remote invocation by the parent.
28
+//	- Listen for beam messages from the parent and pass them to
29
+//	the handlers.
30
+//	- When the beam endpoint is closed by the parent, terminate
31
+//	the current process.
32
+//
33
+// NOTE: Init must be called at the beginning of the same program
34
+// calling Spawn. This is because Spawn approximates a "fork" by
35
+// re-executing the current binary - where it expects spawn.Init
36
+// to intercept the control flow and execute the worker code.
37
+func Init(f engine.Installer) error {
38
+	initCalled = true
39
+	if os.Getenv("ENGINESPAWN") != "1" {
40
+		return nil
41
+	}
42
+	fmt.Printf("[%d child]\n", os.Getpid())
43
+	// Hijack the process
44
+	childErr := func() error {
45
+		fd3 := os.NewFile(3, "beam-introspect")
46
+		introsp, err := beam.FileConn(fd3)
47
+		if err != nil {
48
+			return fmt.Errorf("beam introspection error: %v", err)
49
+		}
50
+		fd3.Close()
51
+		defer introsp.Close()
52
+		eng := engine.NewReceiver(introsp)
53
+		if err := f.Install(eng.Engine); err != nil {
54
+			return err
55
+		}
56
+		if err := eng.Run(); err != nil {
57
+			return err
58
+		}
59
+		return nil
60
+	}()
61
+	if childErr != nil {
62
+		os.Exit(1)
63
+	}
64
+	os.Exit(0)
65
+	return nil // Never reached
66
+}
67
+
68
+// Spawn starts a new Engine in a child process and returns
69
+// a proxy Engine through which it can be controlled.
70
+//
71
+// The commands available on the child engine are determined
72
+// by an earlier call to Init. It is important that Init be
73
+// called at the very beginning of the current program - this
74
+// allows it to be called as a re-execution hook in the child
75
+// process.
76
+//
77
+// Long story short, if you want to expose `myservice` in a child
78
+// process, do this:
79
+//
80
+// func main() {
81
+//     spawn.Init(myservice)
82
+//     [..]
83
+//     child, err := spawn.Spawn()
84
+//     [..]
85
+//     child.Job("dosomething").Run()
86
+// }
87
+func Spawn() (*engine.Engine, error) {
88
+	if !initCalled {
89
+		return nil, fmt.Errorf("spawn.Init must be called at the top of the main() function")
90
+	}
91
+	cmd := exec.Command(utils.SelfPath())
92
+	cmd.Env = append(cmd.Env, "ENGINESPAWN=1")
93
+	local, remote, err := beam.SocketPair()
94
+	if err != nil {
95
+		return nil, err
96
+	}
97
+	child, err := beam.FileConn(local)
98
+	if err != nil {
99
+		local.Close()
100
+		remote.Close()
101
+		return nil, err
102
+	}
103
+	local.Close()
104
+	cmd.ExtraFiles = append(cmd.ExtraFiles, remote)
105
+	// FIXME: the beam/engine glue has no way to inform the caller
106
+	// of the child's termination. The next call will simply return
107
+	// an error.
108
+	if err := cmd.Start(); err != nil {
109
+		child.Close()
110
+		return nil, err
111
+	}
112
+	eng := engine.New()
113
+	if err := engine.NewSender(child).Install(eng); err != nil {
114
+		child.Close()
115
+		return nil, err
116
+	}
117
+	return eng, nil
118
+}
0 119
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"github.com/dotcloud/docker/engine"
5
+	"github.com/dotcloud/docker/engine/spawn"
6
+	"log"
7
+	"os"
8
+	"os/exec"
9
+	"strings"
10
+)
11
+
12
+func main() {
13
+	fmt.Printf("[%d] MAIN\n", os.Getpid())
14
+	spawn.Init(&Worker{})
15
+	fmt.Printf("[%d parent] spawning\n", os.Getpid())
16
+	eng, err := spawn.Spawn()
17
+	if err != nil {
18
+		log.Fatal(err)
19
+	}
20
+	fmt.Printf("[parent] spawned\n")
21
+	job := eng.Job(os.Args[1], os.Args[2:]...)
22
+	job.Stdout.Add(os.Stdout)
23
+	job.Stderr.Add(os.Stderr)
24
+	job.Run()
25
+	// FIXME: use the job's status code
26
+	os.Exit(0)
27
+}
28
+
29
+type Worker struct {
30
+}
31
+
32
+func (w *Worker) Install(eng *engine.Engine) error {
33
+	eng.Register("exec", w.Exec)
34
+	eng.Register("cd", w.Cd)
35
+	eng.Register("echo", w.Echo)
36
+	return nil
37
+}
38
+
39
+func (w *Worker) Exec(job *engine.Job) engine.Status {
40
+	fmt.Printf("--> %v\n", job.Args)
41
+	cmd := exec.Command(job.Args[0], job.Args[1:]...)
42
+	cmd.Stdout = job.Stdout
43
+	cmd.Stderr = os.Stderr
44
+	if err := cmd.Run(); err != nil {
45
+		return job.Errorf("%v\n", err)
46
+	}
47
+	return engine.StatusOK
48
+}
49
+
50
+func (w *Worker) Cd(job *engine.Job) engine.Status {
51
+	if err := os.Chdir(job.Args[0]); err != nil {
52
+		return job.Errorf("%v\n", err)
53
+	}
54
+	return engine.StatusOK
55
+}
56
+
57
+func (w *Worker) Echo(job *engine.Job) engine.Status {
58
+	fmt.Fprintf(job.Stdout, "%s\n", strings.Join(job.Args, " "))
59
+	return engine.StatusOK
60
+}