Merge lp:~fwereade/pyjuju/go-unit-commands into lp:pyjuju/go
- go-unit-commands
- Merge into go
Status: | Work in progress |
---|---|
Proposed branch: | lp:~fwereade/pyjuju/go-unit-commands |
Merge into: | lp:pyjuju/go |
Diff against target: |
1328 lines (+877/-88) 21 files modified
cmd/command.go (+74/-25) cmd/juju/bootstrap.go (+2/-2) cmd/juju/bootstrap_test.go (+2/-2) cmd/juju/main.go (+1/-1) cmd/jujud/agent.go (+1/-1) cmd/jujud/initzk.go (+2/-2) cmd/jujud/initzk_test.go (+1/-1) cmd/jujud/machine.go (+4/-3) cmd/jujud/main.go (+1/-1) cmd/jujud/provisioning.go (+6/-3) cmd/jujud/unit.go (+4/-3) cmd/jujud/util_test.go (+5/-5) cmd/proxy/main.go (+42/-0) cmd/proxy/proxy.go (+103/-0) cmd/proxy/proxy_test.go (+136/-0) cmd/server/protocol.go (+86/-0) cmd/server/protocol_test.go (+38/-0) cmd/server/server.go (+149/-0) cmd/server/server_test.go (+205/-0) cmd/supercommand.go (+10/-34) cmd/supercommand_test.go (+5/-5) |
To merge this branch: | bzr merge lp:~fwereade/pyjuju/go-unit-commands |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+94885@code.launchpad.net |
Commit message
Description of the change
Rough proposal for implementation of relation-get and similar commands.
In short: every command is implemented as a symlink to a single "proxy"
executable. This proxy communicates with a cmd.server.Server running in the
unit agent process, which uses a nameless SuperCommand to select and run the
command specified by the original proxy's full command line, and
communicates stderr and stdout back to the original process.
This necessitated a change to cmd, such that a new Env interface has been
added, which is responsible for the command's interactions with its
environment; cmd.DefaultEnv implements the original functionality, while
cmd.server.
back to the proxy process. Env has ended up needing to be threaded through
much of cmd, but the impact on calling code is minimal.
- 80. By William Reade
-
add tests for command server
- 81. By William Reade
-
added tests for request/response
- 82. By William Reade
-
doc tweaks
William Reade (fwereade) wrote : | # |
On Tue, 2012-02-28 at 18:22 +0000, Gustavo Niemeyer wrote:
> Can this be broken down in smaller chunks?
>
> The past changes have all been similarly large in scope. I thought they
> might be hard to break down, but I'm getting more skeptical that every
> single change needs to touch 20 files. Reviewing, applying comments, and
> getting them through the pipeline in general is a lot more traumatic
> that way.
Fair point, and this will definitely break down cleanly into 2 parts;
more might be a little tricky, but I'll see what I can do.
Gustavo Niemeyer (niemeyer) wrote : | # |
> Fair point, and this will definitely break down cleanly into 2 parts;
> more might be a little tricky, but I'll see what I can do.
I suspect there are many more independent changes packed into that stack.
--
Gustavo Niemeyer
http://
http://
http://
http://
-- I'm not absolutely sure of anything.
Unmerged revisions
- 82. By William Reade
-
doc tweaks
- 81. By William Reade
-
added tests for request/response
- 80. By William Reade
-
add tests for command server
- 79. By William Reade
-
moved packages again; added max request size check; moved InitOutput from SuperCommand to Env, to prevent potential pollution of server process settings
- 78. By William Reade
-
rearrange packages
- 77. By William Reade
-
moved response handling next to request handling
- 76. By William Reade
-
merge parent
- 75. By William Reade
-
docs
- 74. By William Reade
-
we have sometests,and apparently they work...
- 73. By William Reade
-
added testable entry point for Main
Preview Diff
1 | === modified file 'cmd/command.go' |
2 | --- cmd/command.go 2012-02-15 10:29:05 +0000 |
3 | +++ cmd/command.go 2012-02-28 17:13:19 +0000 |
4 | @@ -2,8 +2,10 @@ |
5 | |
6 | import ( |
7 | "fmt" |
8 | + "io" |
9 | "launchpad.net/gnuflag" |
10 | "launchpad.net/juju/go/log" |
11 | + stdlog "log" |
12 | "os" |
13 | "strings" |
14 | ) |
15 | @@ -32,8 +34,7 @@ |
16 | return fmt.Sprintf("%s %s", i.Name, i.Args) |
17 | } |
18 | |
19 | -// Command is implemented by types that interpret any command-line arguments |
20 | -// passed to the "juju" command. |
21 | +// Command is implemented by types that interpret command-line arguments. |
22 | type Command interface { |
23 | // Info returns information about the command. |
24 | Info() *Info |
25 | @@ -44,40 +45,85 @@ |
26 | |
27 | // ParsePositional is called by Parse to allow the Command to handle |
28 | // positional command-line arguments. |
29 | - ParsePositional(args []string) error |
30 | + ParsePositional(e Env, args []string) error |
31 | |
32 | // Run will execute the command according to the options and positional |
33 | // arguments interpreted by a call to Parse. |
34 | - Run() error |
35 | + Run(e Env) error |
36 | +} |
37 | + |
38 | +// Env represents an environment in which a Command can be run. |
39 | +type Env interface { |
40 | + Stderr() io.Writer |
41 | + Stdout() io.Writer |
42 | + Exit(int) |
43 | + InitOutput(bool, bool, string) error |
44 | +} |
45 | + |
46 | +// DefaultEnv is the environment in which normal command-line commands are run. |
47 | +type DefaultEnv struct{} |
48 | + |
49 | +func (e *DefaultEnv) Stdout() io.Writer { |
50 | + return os.Stdout |
51 | +} |
52 | + |
53 | +func (e *DefaultEnv) Stderr() io.Writer { |
54 | + return os.Stderr |
55 | +} |
56 | + |
57 | +func (e *DefaultEnv) Exit(code int) { |
58 | + os.Exit(code) |
59 | +} |
60 | + |
61 | +func (e *DefaultEnv) InitOutput(verbose bool, debug bool, logfile string) error { |
62 | + if debug { |
63 | + log.Debug = true |
64 | + } |
65 | + var target io.Writer |
66 | + if logfile != "" { |
67 | + var err error |
68 | + target, err = os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) |
69 | + if err != nil { |
70 | + return err |
71 | + } |
72 | + } else if verbose || debug { |
73 | + target = e.Stderr() |
74 | + } |
75 | + if target != nil { |
76 | + log.Target = stdlog.New(target, "", stdlog.LstdFlags) |
77 | + } |
78 | + return nil |
79 | } |
80 | |
81 | // NewFlagSet returns a FlagSet initialized for use with c. |
82 | -func NewFlagSet(c Command) *gnuflag.FlagSet { |
83 | - f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ExitOnError) |
84 | - f.Usage = func() { PrintUsage(c) } |
85 | +func NewFlagSet(e Env, c Command) *gnuflag.FlagSet { |
86 | + f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError) |
87 | + f.SetOutput(e.Stderr()) |
88 | + f.Usage = func() { PrintUsage(e, c) } |
89 | c.InitFlagSet(f) |
90 | return f |
91 | } |
92 | |
93 | // PrintUsage prints usage information for c to stderr. |
94 | -func PrintUsage(c Command) { |
95 | +func PrintUsage(e Env, c Command) { |
96 | i := c.Info() |
97 | - fmt.Fprintf(os.Stderr, "usage: %s\n", i.Usage()) |
98 | - fmt.Fprintf(os.Stderr, "purpose: %s\n", i.Purpose) |
99 | - fmt.Fprintf(os.Stderr, "\noptions:\n") |
100 | - NewFlagSet(c).PrintDefaults() |
101 | + stderr := e.Stderr() |
102 | + fmt.Fprintf(stderr, "usage: %s\n", i.Usage()) |
103 | + fmt.Fprintf(stderr, "purpose: %s\n", i.Purpose) |
104 | + fmt.Fprintf(stderr, "\noptions:\n") |
105 | + NewFlagSet(e, c).PrintDefaults() |
106 | if i.Doc != "" { |
107 | - fmt.Fprintf(os.Stderr, "\n%s\n", strings.TrimSpace(i.Doc)) |
108 | + fmt.Fprintf(stderr, "\n%s\n", strings.TrimSpace(i.Doc)) |
109 | } |
110 | } |
111 | |
112 | // Parse parses args on c. This must be called before c is Run. |
113 | -func Parse(c Command, args []string) error { |
114 | - f := NewFlagSet(c) |
115 | +func Parse(e Env, c Command, args []string) error { |
116 | + f := NewFlagSet(e, c) |
117 | if err := f.Parse(c.Info().Intersperse, args); err != nil { |
118 | return err |
119 | } |
120 | - return c.ParsePositional(f.Args()) |
121 | + return c.ParsePositional(e, f.Args()) |
122 | } |
123 | |
124 | // CheckEmpty is a utility function that returns an error if args is not empty. |
125 | @@ -89,16 +135,19 @@ |
126 | } |
127 | |
128 | // Main will Parse and Run a Command, and exit appropriately. |
129 | -func Main(c Command, args []string) { |
130 | - if err := Parse(c, args[1:]); err != nil { |
131 | - fmt.Fprintf(os.Stderr, "%v\n", err) |
132 | - PrintUsage(c) |
133 | - os.Exit(2) |
134 | +func Main(e Env, c Command, args []string) { |
135 | + stderr := e.Stderr() |
136 | + if err := Parse(e, c, args); err != nil { |
137 | + fmt.Fprintf(stderr, "%v\n", err) |
138 | + PrintUsage(e, c) |
139 | + e.Exit(2) |
140 | + return |
141 | } |
142 | - if err := c.Run(); err != nil { |
143 | + if err := c.Run(e); err != nil { |
144 | log.Debugf("%s command failed: %s\n", c.Info().Name, err) |
145 | - fmt.Fprintf(os.Stderr, "%v\n", err) |
146 | - os.Exit(1) |
147 | + fmt.Fprintf(stderr, "%v\n", err) |
148 | + e.Exit(1) |
149 | + return |
150 | } |
151 | - os.Exit(0) |
152 | + e.Exit(0) |
153 | } |
154 | |
155 | === modified file 'cmd/juju/bootstrap.go' |
156 | --- cmd/juju/bootstrap.go 2012-02-09 11:35:24 +0000 |
157 | +++ cmd/juju/bootstrap.go 2012-02-28 17:13:19 +0000 |
158 | @@ -30,13 +30,13 @@ |
159 | |
160 | // ParsePositional checks that no unexpected extra command-line arguments have |
161 | // been specified. |
162 | -func (c *BootstrapCommand) ParsePositional(args []string) error { |
163 | +func (c *BootstrapCommand) ParsePositional(e cmd.Env, args []string) error { |
164 | return cmd.CheckEmpty(args) |
165 | } |
166 | |
167 | // Run connects to the environment specified on the command line and bootstraps |
168 | // a juju in that environment if none already exists. |
169 | -func (c *BootstrapCommand) Run() error { |
170 | +func (c *BootstrapCommand) Run(e cmd.Env) error { |
171 | conn, err := juju.NewConn(c.Environment) |
172 | if err != nil { |
173 | return err |
174 | |
175 | === modified file 'cmd/juju/bootstrap_test.go' |
176 | --- cmd/juju/bootstrap_test.go 2012-02-08 11:29:32 +0000 |
177 | +++ cmd/juju/bootstrap_test.go 2012-02-28 17:13:19 +0000 |
178 | @@ -12,7 +12,7 @@ |
179 | |
180 | func checkEnv(c *C, args []string, expect string) { |
181 | bc := &main.BootstrapCommand{} |
182 | - err := cmd.Parse(bc, args) |
183 | + err := cmd.Parse(&cmd.DefaultEnv{}, bc, args) |
184 | c.Assert(err, IsNil) |
185 | c.Assert(bc.Environment, Equals, expect) |
186 | } |
187 | @@ -20,7 +20,7 @@ |
188 | func (s *BootstrapSuite) TestEnvironment(c *C) { |
189 | bc := &main.BootstrapCommand{} |
190 | c.Assert(bc.Environment, Equals, "") |
191 | - err := cmd.Parse(bc, []string{"hotdog"}) |
192 | + err := cmd.Parse(&cmd.DefaultEnv{}, bc, []string{"hotdog"}) |
193 | c.Assert(err, ErrorMatches, `unrecognised args: \[hotdog\]`) |
194 | c.Assert(bc.Environment, Equals, "") |
195 | |
196 | |
197 | === modified file 'cmd/juju/main.go' |
198 | --- cmd/juju/main.go 2012-02-14 16:42:42 +0000 |
199 | +++ cmd/juju/main.go 2012-02-28 17:13:19 +0000 |
200 | @@ -18,7 +18,7 @@ |
201 | func Main(args []string) { |
202 | jc := cmd.NewSuperCommand("juju", jujuDoc) |
203 | jc.Register(&BootstrapCommand{}) |
204 | - cmd.Main(jc, args) |
205 | + cmd.Main(&cmd.DefaultEnv{}, jc, args[1:]) |
206 | } |
207 | |
208 | func main() { |
209 | |
210 | === modified file 'cmd/jujud/agent.go' |
211 | --- cmd/jujud/agent.go 2012-02-21 15:13:47 +0000 |
212 | +++ cmd/jujud/agent.go 2012-02-28 17:13:19 +0000 |
213 | @@ -36,7 +36,7 @@ |
214 | |
215 | // ParsePositional checks that there are no unwanted arguments, and that all |
216 | // required flags have been set. |
217 | -func (c *agentConf) ParsePositional(args []string) error { |
218 | +func (c *agentConf) ParsePositional(e cmd.Env, args []string) error { |
219 | if c.jujuDir == "" { |
220 | return requiredError("juju-directory") |
221 | } |
222 | |
223 | === modified file 'cmd/jujud/initzk.go' |
224 | --- cmd/jujud/initzk.go 2012-02-21 15:13:47 +0000 |
225 | +++ cmd/jujud/initzk.go 2012-02-28 17:13:19 +0000 |
226 | @@ -32,7 +32,7 @@ |
227 | |
228 | // ParsePositional checks that there are no unwanted arguments, and that all |
229 | // required flags have been set. |
230 | -func (c *InitzkCommand) ParsePositional(args []string) error { |
231 | +func (c *InitzkCommand) ParsePositional(e cmd.Env, args []string) error { |
232 | if c.StateInfo.Addrs == nil { |
233 | return requiredError("zookeeper-servers") |
234 | } |
235 | @@ -46,7 +46,7 @@ |
236 | } |
237 | |
238 | // Run initializes zookeeper state for an environment. |
239 | -func (c *InitzkCommand) Run() error { |
240 | +func (c *InitzkCommand) Run(e cmd.Env) error { |
241 | // TODO connect to zookeeper; call State.Initialize |
242 | return fmt.Errorf("InitzkCommand.Run not implemented") |
243 | } |
244 | |
245 | === modified file 'cmd/jujud/initzk_test.go' |
246 | --- cmd/jujud/initzk_test.go 2012-02-27 08:48:13 +0000 |
247 | +++ cmd/jujud/initzk_test.go 2012-02-28 17:13:19 +0000 |
248 | @@ -12,7 +12,7 @@ |
249 | |
250 | func parseInitzkCommand(args []string) (*main.InitzkCommand, error) { |
251 | c := &main.InitzkCommand{} |
252 | - err := cmd.Parse(c, args) |
253 | + err := cmd.Parse(&cmd.DefaultEnv{}, c, args) |
254 | return c, err |
255 | } |
256 | |
257 | |
258 | === modified file 'cmd/jujud/machine.go' |
259 | --- cmd/jujud/machine.go 2012-02-15 12:28:26 +0000 |
260 | +++ cmd/jujud/machine.go 2012-02-28 17:13:19 +0000 |
261 | @@ -3,6 +3,7 @@ |
262 | import ( |
263 | "fmt" |
264 | "launchpad.net/gnuflag" |
265 | + "launchpad.net/juju/go/cmd" |
266 | ) |
267 | |
268 | // MachineAgent is a cmd.Command responsible for running a machine agent. |
269 | @@ -23,14 +24,14 @@ |
270 | |
271 | // ParsePositional checks that there are no unwanted arguments, and that all |
272 | // required flags have been set. |
273 | -func (a *MachineAgent) ParsePositional(args []string) error { |
274 | +func (a *MachineAgent) ParsePositional(e cmd.Env, args []string) error { |
275 | if a.MachineId < 0 { |
276 | return fmt.Errorf("--machine-id option must be set, and expects a non-negative integer") |
277 | } |
278 | - return a.agentConf.ParsePositional(args) |
279 | + return a.agentConf.ParsePositional(e, args) |
280 | } |
281 | |
282 | // Run runs a machine agent. |
283 | -func (a *MachineAgent) Run() error { |
284 | +func (a *MachineAgent) Run(e cmd.Env) error { |
285 | return fmt.Errorf("MachineAgent.Run not implemented") |
286 | } |
287 | |
288 | === modified file 'cmd/jujud/main.go' |
289 | --- cmd/jujud/main.go 2012-02-15 10:10:38 +0000 |
290 | +++ cmd/jujud/main.go 2012-02-28 17:13:19 +0000 |
291 | @@ -21,7 +21,7 @@ |
292 | jc.Register(NewUnitAgent()) |
293 | jc.Register(NewMachineAgent()) |
294 | jc.Register(NewProvisioningAgent()) |
295 | - cmd.Main(jc, args) |
296 | + cmd.Main(&cmd.DefaultEnv{}, jc, args[1:]) |
297 | } |
298 | |
299 | func main() { |
300 | |
301 | === modified file 'cmd/jujud/provisioning.go' |
302 | --- cmd/jujud/provisioning.go 2012-02-15 10:29:05 +0000 |
303 | +++ cmd/jujud/provisioning.go 2012-02-28 17:13:19 +0000 |
304 | @@ -1,6 +1,9 @@ |
305 | package main |
306 | |
307 | -import "fmt" |
308 | +import ( |
309 | + "fmt" |
310 | + "launchpad.net/juju/go/cmd" |
311 | +) |
312 | |
313 | // ProvisioningAgent is a cmd.Command responsible for running a provisioning agent. |
314 | type ProvisioningAgent struct { |
315 | @@ -12,6 +15,6 @@ |
316 | } |
317 | |
318 | // Run runs a provisioning agent. |
319 | -func (a *ProvisioningAgent) Run() error { |
320 | - return fmt.Errorf("MachineAgent.Run not implemented") |
321 | +func (a *ProvisioningAgent) Run(e cmd.Env) error { |
322 | + return fmt.Errorf("ProvisioningAgent.Run not implemented") |
323 | } |
324 | |
325 | === modified file 'cmd/jujud/unit.go' |
326 | --- cmd/jujud/unit.go 2012-02-17 08:03:35 +0000 |
327 | +++ cmd/jujud/unit.go 2012-02-28 17:13:19 +0000 |
328 | @@ -3,6 +3,7 @@ |
329 | import ( |
330 | "fmt" |
331 | "launchpad.net/gnuflag" |
332 | + "launchpad.net/juju/go/cmd" |
333 | "launchpad.net/juju/go/juju" |
334 | ) |
335 | |
336 | @@ -24,17 +25,17 @@ |
337 | |
338 | // ParsePositional checks that there are no unwanted arguments, and that all |
339 | // required flags have been set. |
340 | -func (a *UnitAgent) ParsePositional(args []string) error { |
341 | +func (a *UnitAgent) ParsePositional(e cmd.Env, args []string) error { |
342 | if a.UnitName == "" { |
343 | return requiredError("unit-name") |
344 | } |
345 | if !juju.ValidUnit.MatchString(a.UnitName) { |
346 | return fmt.Errorf(`--unit-name option expects "<service>/<n>" argument`) |
347 | } |
348 | - return a.agentConf.ParsePositional(args) |
349 | + return a.agentConf.ParsePositional(e, args) |
350 | } |
351 | |
352 | // Run runs a unit agent. |
353 | -func (a *UnitAgent) Run() error { |
354 | +func (a *UnitAgent) Run(e cmd.Env) error { |
355 | return fmt.Errorf("UnitAgent.Run not implemented") |
356 | } |
357 | |
358 | === modified file 'cmd/jujud/util_test.go' |
359 | --- cmd/jujud/util_test.go 2012-02-27 08:48:13 +0000 |
360 | +++ cmd/jujud/util_test.go 2012-02-28 17:13:19 +0000 |
361 | @@ -13,23 +13,23 @@ |
362 | // command pre-parsed with the always-required options and whatever others |
363 | // are necessary to allow parsing to succeed (specified in args). |
364 | func CheckAgentCommand(c *C, create acCreator, args []string) main.AgentCommand { |
365 | - err := cmd.Parse(create(), args) |
366 | + err := cmd.Parse(&cmd.DefaultEnv{}, create(), args) |
367 | c.Assert(err, ErrorMatches, "--zookeeper-servers option must be set") |
368 | args = append(args, "--zookeeper-servers", "zk1:2181,zk2:2181") |
369 | |
370 | - err = cmd.Parse(create(), args) |
371 | + err = cmd.Parse(&cmd.DefaultEnv{}, create(), args) |
372 | c.Assert(err, ErrorMatches, "--session-file option must be set") |
373 | args = append(args, "--session-file", "sf") |
374 | |
375 | ac := create() |
376 | - c.Assert(cmd.Parse(ac, args), IsNil) |
377 | + c.Assert(cmd.Parse(&cmd.DefaultEnv{}, ac, args), IsNil) |
378 | c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"}) |
379 | c.Assert(ac.SessionFile(), Equals, "sf") |
380 | c.Assert(ac.JujuDir(), Equals, "/var/lib/juju") |
381 | args = append(args, "--juju-directory", "jd") |
382 | |
383 | ac = create() |
384 | - c.Assert(cmd.Parse(ac, args), IsNil) |
385 | + c.Assert(cmd.Parse(&cmd.DefaultEnv{}, ac, args), IsNil) |
386 | c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"}) |
387 | c.Assert(ac.SessionFile(), Equals, "sf") |
388 | c.Assert(ac.JujuDir(), Equals, "jd") |
389 | @@ -44,5 +44,5 @@ |
390 | "--session-file", "sf", |
391 | "--juju-directory", "jd", |
392 | } |
393 | - return cmd.Parse(ac, append(common, args...)) |
394 | + return cmd.Parse(&cmd.DefaultEnv{}, ac, append(common, args...)) |
395 | } |
396 | |
397 | === added directory 'cmd/proxy' |
398 | === added file 'cmd/proxy/main.go' |
399 | --- cmd/proxy/main.go 1970-01-01 00:00:00 +0000 |
400 | +++ cmd/proxy/main.go 2012-02-28 17:13:19 +0000 |
401 | @@ -0,0 +1,42 @@ |
402 | +package main |
403 | + |
404 | +import ( |
405 | + "fmt" |
406 | + "os" |
407 | +) |
408 | + |
409 | +// requireEnv returns an environment variable, and calls os.Exit(2) if it's not |
410 | +// set. |
411 | +func requireEnv(name string) string { |
412 | + value := os.Getenv(name) |
413 | + if value == "" { |
414 | + fmt.Fprintf(os.Stderr, "%s not set\n", name) |
415 | + os.Exit(2) |
416 | + } |
417 | + return value |
418 | +} |
419 | + |
420 | +// Main implements every command that calls back into a juju unit agent by using |
421 | +// a Proxy to ask the unit agent to execute the actual command for us and pipe |
422 | +// back stdout/stderr. This function is not redundant with main, because it |
423 | +// provides an entry point for testing with arbitrary command line arguments. |
424 | +// Individual commands should be exposed by symlinking the command name to this |
425 | +// executable. |
426 | +func Main(args []string) { |
427 | + p := &Proxy{ |
428 | + AgentSock: requireEnv("JUJU_AGENT_SOCKET"), |
429 | + ClientId: requireEnv("JUJU_CLIENT_ID"), |
430 | + Stdout: os.Stdout, |
431 | + Stderr: os.Stderr, |
432 | + } |
433 | + result, err := p.Run(args) |
434 | + if err != nil { |
435 | + fmt.Fprintf(os.Stderr, "%s command failed: %v\n", args[0], err) |
436 | + os.Exit(1) |
437 | + } |
438 | + os.Exit(result) |
439 | +} |
440 | + |
441 | +func main() { |
442 | + Main(os.Args) |
443 | +} |
444 | |
445 | === added file 'cmd/proxy/proxy.go' |
446 | --- cmd/proxy/proxy.go 1970-01-01 00:00:00 +0000 |
447 | +++ cmd/proxy/proxy.go 2012-02-28 17:13:19 +0000 |
448 | @@ -0,0 +1,103 @@ |
449 | +package main |
450 | + |
451 | +import ( |
452 | + "fmt" |
453 | + "io" |
454 | + "io/ioutil" |
455 | + "launchpad.net/juju/go/cmd/server" |
456 | + "net" |
457 | + "os" |
458 | + "path/filepath" |
459 | +) |
460 | + |
461 | +// Proxy is responsible for asking a running Server to execute a Command, and |
462 | +// for propagating the Command's output and return code. |
463 | +type Proxy struct { |
464 | + AgentSock string |
465 | + ClientId string |
466 | + Stdout io.Writer |
467 | + Stderr io.Writer |
468 | +} |
469 | + |
470 | +// connect listens for a connection on sock, and echoes anything received to dst. |
471 | +func connect(dst io.Writer, sock string) (<-chan bool, error) { |
472 | + l, err := net.Listen("unix", sock) |
473 | + if err != nil { |
474 | + return nil, err |
475 | + } |
476 | + done := make(chan bool) |
477 | + go func() { |
478 | + defer close(done) |
479 | + defer os.Remove(sock) |
480 | + defer l.Close() |
481 | + conn, err := l.Accept() |
482 | + if err != nil { |
483 | + return |
484 | + } |
485 | + defer conn.Close() |
486 | + _, err = io.Copy(dst, conn) |
487 | + if err != nil { |
488 | + return |
489 | + } |
490 | + done <- true |
491 | + }() |
492 | + return done, nil |
493 | +} |
494 | + |
495 | +// Run connects to a Server listening on p.AgentSock, asks it to run the Command |
496 | +// specified by args, forwards the Command's stdout/stderr to p.Stdout/p.Stderr, |
497 | +// and returns the Command's result code. |
498 | +func (p *Proxy) Run(args []string) (int, error) { |
499 | + // Create tempdir for stdout/stderr sockets. |
500 | + sockDir, err := ioutil.TempDir("", "juju-proxy") |
501 | + if err != nil { |
502 | + return 0, err |
503 | + } |
504 | + defer os.RemoveAll(sockDir) |
505 | + |
506 | + // Pipe data from sockets into stdout/stderr. |
507 | + outSock := filepath.Join(sockDir, "stdout.sock") |
508 | + outDone, err := connect(p.Stdout, outSock) |
509 | + if err != nil { |
510 | + return 0, err |
511 | + } |
512 | + errSock := filepath.Join(sockDir, "stderr.sock") |
513 | + errDone, err := connect(p.Stderr, errSock) |
514 | + if err != nil { |
515 | + return 0, err |
516 | + } |
517 | + |
518 | + // Connect to the agent socket. |
519 | + conn, err := net.Dial("unix", p.AgentSock) |
520 | + if err != nil { |
521 | + return 0, err |
522 | + } |
523 | + defer conn.Close() |
524 | + |
525 | + // Tell the agent to run the command specified by args (with the command |
526 | + // name suitably trimmed) and pipe its output back to our sockets. |
527 | + args[0] = filepath.Base(args[0]) |
528 | + req := &server.Request{p.ClientId, args, outSock, errSock} |
529 | + _, err = req.WriteTo(conn) |
530 | + if err != nil { |
531 | + return 0, err |
532 | + } |
533 | + |
534 | + // Extract response code from agent connection. |
535 | + var response server.Response |
536 | + _, err = response.ReadFrom(conn) |
537 | + if err != nil { |
538 | + return 0, err |
539 | + } |
540 | + |
541 | + // Wait for streams to close before returning. |
542 | + _, ok := <-outDone |
543 | + if !ok { |
544 | + return 0, fmt.Errorf("stdout connection failed on %s", outSock) |
545 | + } |
546 | + _, ok = <-errDone |
547 | + if !ok { |
548 | + return 0, fmt.Errorf("stderr connection failed on %s", errSock) |
549 | + } |
550 | + return int(response), nil |
551 | +} |
552 | |
553 | === added file 'cmd/proxy/proxy_test.go' |
554 | --- cmd/proxy/proxy_test.go 1970-01-01 00:00:00 +0000 |
555 | +++ cmd/proxy/proxy_test.go 2012-02-28 17:13:19 +0000 |
556 | @@ -0,0 +1,136 @@ |
557 | +package main_test |
558 | + |
559 | +import ( |
560 | + "bytes" |
561 | + "fmt" |
562 | + "launchpad.net/gnuflag" |
563 | + . "launchpad.net/gocheck" |
564 | + "launchpad.net/juju/go/cmd" |
565 | + main "launchpad.net/juju/go/cmd/proxy" |
566 | + "launchpad.net/juju/go/cmd/server" |
567 | + "path/filepath" |
568 | + "strings" |
569 | + "testing" |
570 | +) |
571 | + |
572 | +func Test(t *testing.T) { TestingT(t) } |
573 | + |
574 | +type TestCommand struct { |
575 | + Value string |
576 | +} |
577 | + |
578 | +func (c *TestCommand) Info() *cmd.Info { |
579 | + return &cmd.Info{"magic", "[options]", "do magic", "blah doc", true} |
580 | +} |
581 | + |
582 | +func (c *TestCommand) InitFlagSet(f *gnuflag.FlagSet) { |
583 | + f.StringVar(&c.Value, "value", "", "doc") |
584 | +} |
585 | + |
586 | +func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error { |
587 | + if len(args) != 0 { |
588 | + return fmt.Errorf("BADARGS: %s", args) |
589 | + } |
590 | + return nil |
591 | +} |
592 | + |
593 | +func (c *TestCommand) Run(e cmd.Env) error { |
594 | + if c.Value != "zyxxy" { |
595 | + return fmt.Errorf("insufficiently magic") |
596 | + } |
597 | + e.Stdout().Write([]byte("eye of newt")) |
598 | + e.Stderr().Write([]byte("toe of frog")) |
599 | + return nil |
600 | +} |
601 | + |
602 | +type ProxySuite struct { |
603 | + sock string |
604 | + server *server.Server |
605 | +} |
606 | + |
607 | +var _ = Suite(&ProxySuite{}) |
608 | + |
609 | +func (s *ProxySuite) SetUpSuite(c *C) { |
610 | + factory := func(clientId string) (cmd.Command, error) { |
611 | + if clientId != "jeremiah" { |
612 | + return nil, fmt.Errorf("unknown client") |
613 | + } |
614 | + sc := cmd.NewSuperCommand("", "") |
615 | + sc.Register(&TestCommand{}) |
616 | + return sc, nil |
617 | + } |
618 | + s.sock = filepath.Join(c.MkDir(), "test.sock") |
619 | + var err error |
620 | + s.server, err = server.NewServer(factory, s.sock) |
621 | + c.Assert(err, IsNil) |
622 | +} |
623 | + |
624 | +func (s *ProxySuite) TearDownSuite(c *C) { |
625 | + c.Assert(s.server.Stop(), IsNil) |
626 | +} |
627 | + |
628 | +func (s *ProxySuite) AssertCall(c *C, clientId string, args []string) (int, string, string) { |
629 | + stdout := bytes.NewBuffer([]byte{}) |
630 | + stderr := bytes.NewBuffer([]byte{}) |
631 | + p := &main.Proxy{ |
632 | + AgentSock: s.sock, |
633 | + ClientId: clientId, |
634 | + Stdout: stdout, |
635 | + Stderr: stderr, |
636 | + } |
637 | + result, err := p.Run(args) |
638 | + c.Assert(err, IsNil) |
639 | + return result, stdout.String(), stderr.String() |
640 | +} |
641 | + |
642 | +func (s *ProxySuite) TestSuccess(c *C) { |
643 | + result, stdout, stderr := s.AssertCall( |
644 | + c, "jeremiah", []string{"magic", "--value", "zyxxy"}) |
645 | + c.Assert(result, Equals, 0) |
646 | + c.Assert(stdout, Equals, "eye of newt") |
647 | + c.Assert(stderr, Equals, "toe of frog") |
648 | +} |
649 | + |
650 | +func (s *ProxySuite) TestCommandPath(c *C) { |
651 | + result, stdout, stderr := s.AssertCall( |
652 | + c, "jeremiah", []string{"some/path/to/magic", "--value", "zyxxy"}) |
653 | + c.Assert(result, Equals, 0) |
654 | + c.Assert(stdout, Equals, "eye of newt") |
655 | + c.Assert(stderr, Equals, "toe of frog") |
656 | +} |
657 | + |
658 | +func (s *ProxySuite) TestBadRun(c *C) { |
659 | + result, stdout, stderr := s.AssertCall( |
660 | + c, "jeremiah", []string{"magic", "--value", "wrong"}) |
661 | + c.Assert(result, Equals, 1) |
662 | + c.Assert(stdout, Equals, "") |
663 | + errLines := strings.Split(stderr, "\n") |
664 | + c.Assert(errLines[0], Equals, "insufficiently magic") |
665 | +} |
666 | + |
667 | +func (s *ProxySuite) TestBadClient(c *C) { |
668 | + result, stdout, stderr := s.AssertCall( |
669 | + c, "methuselah", []string{"magic"}) |
670 | + c.Assert(result, Equals, 1) |
671 | + c.Assert(stdout, Equals, "") |
672 | + errLines := strings.Split(stderr, "\n") |
673 | + c.Assert(errLines[0], Equals, "command unavailable: unknown client") |
674 | +} |
675 | + |
676 | +func (s *ProxySuite) TestNonsenseArgs(c *C) { |
677 | + result, stdout, stderr := s.AssertCall( |
678 | + c, "jeremiah", []string{"magic", "--cheese"}) |
679 | + c.Assert(result, Equals, 2) |
680 | + c.Assert(stdout, Equals, "") |
681 | + errLines := strings.Split(stderr, "\n") |
682 | + c.Assert(errLines[0], Equals, "flag provided but not defined: --cheese") |
683 | +} |
684 | + |
685 | +func (s *ProxySuite) TestUnknownCommand(c *C) { |
686 | + result, stdout, stderr := s.AssertCall( |
687 | + c, "jeremiah", []string{"witchcraft"}) |
688 | + c.Assert(result, Equals, 2) |
689 | + c.Assert(stdout, Equals, "") |
690 | + errLines := strings.Split(stderr, "\n") |
691 | + c.Assert(errLines[0], Equals, "unrecognised command: witchcraft") |
692 | +} |
693 | |
694 | === added directory 'cmd/server' |
695 | === added file 'cmd/server/protocol.go' |
696 | --- cmd/server/protocol.go 1970-01-01 00:00:00 +0000 |
697 | +++ cmd/server/protocol.go 2012-02-28 17:13:19 +0000 |
698 | @@ -0,0 +1,86 @@ |
699 | +package server |
700 | + |
701 | +import ( |
702 | + "encoding/binary" |
703 | + "fmt" |
704 | + "io" |
705 | + "launchpad.net/goyaml" |
706 | +) |
707 | + |
708 | +// Request contains all the information necessary for a Server to run a Command |
709 | +// on behalf of a Proxy. |
710 | +type Request struct { |
711 | + ClientId string |
712 | + Args []string |
713 | + OutSock string |
714 | + ErrSock string |
715 | +} |
716 | + |
717 | +var ( |
718 | + bo = binary.BigEndian |
719 | + codeS = int64(binary.Size(int32(0))) |
720 | + sizeS = int64(binary.Size(uint32(0))) |
721 | + maxSize = uint32(1024 * 1024) |
722 | +) |
723 | + |
724 | +// ReadFrom fills in a Request's fields using data read from src (which is |
725 | +// expected in the format written by WriteTo). |
726 | +func (r *Request) ReadFrom(src io.Reader) (count int64, err error) { |
727 | + var size uint32 |
728 | + err = binary.Read(src, bo, &size) |
729 | + if err != nil { |
730 | + return |
731 | + } |
732 | + count += sizeS |
733 | + if size > maxSize { |
734 | + err = fmt.Errorf("unreasonably large request: %d bytes", size) |
735 | + return |
736 | + } |
737 | + data := make([]byte, size) |
738 | + n, err := io.ReadFull(src, data[:]) |
739 | + count += int64(n) |
740 | + if err != nil { |
741 | + return |
742 | + } |
743 | + err = goyaml.Unmarshal(data, r) |
744 | + return |
745 | +} |
746 | + |
747 | +// WriteTo writes a Request's data to dst, in a format suitable for |
748 | +// reconstruction by ReadFrom. |
749 | +func (r *Request) WriteTo(dst io.Writer) (int64, error) { |
750 | + yaml, err := goyaml.Marshal(r) |
751 | + if err != nil { |
752 | + return 0, err |
753 | + } |
754 | + bytes := []byte(yaml) |
755 | + err = binary.Write(dst, bo, uint32(len(bytes))) |
756 | + if err != nil { |
757 | + return 0, err |
758 | + } |
759 | + n, err := dst.Write(bytes) |
760 | + return int64(n) + sizeS, err |
761 | +} |
762 | + |
763 | +// Response is a command's return code, with extra methods to make it convenient |
764 | +// to transmit and interpret. |
765 | +type Response int32 |
766 | + |
767 | +// ReadFrom decodes a Response from src (expected in the format written by WriteTo). |
768 | +func (r *Response) ReadFrom(src io.Reader) (int64, error) { |
769 | + err := binary.Read(src, bo, r) |
770 | + if err != nil { |
771 | + return 0, err |
772 | + } |
773 | + return codeS, nil |
774 | +} |
775 | + |
776 | +// WriteTo encodes a Response to dst, in a format suitable for reconstruction by |
777 | +// ReadFrom. |
778 | +func (r *Response) WriteTo(dst io.Writer) (int64, error) { |
779 | + err := binary.Write(dst, bo, r) |
780 | + if err != nil { |
781 | + return 0, err |
782 | + } |
783 | + return codeS, nil |
784 | +} |
785 | |
786 | === added file 'cmd/server/protocol_test.go' |
787 | --- cmd/server/protocol_test.go 1970-01-01 00:00:00 +0000 |
788 | +++ cmd/server/protocol_test.go 2012-02-28 17:13:19 +0000 |
789 | @@ -0,0 +1,38 @@ |
790 | +package server_test |
791 | + |
792 | +import ( |
793 | + "bytes" |
794 | + . "launchpad.net/gocheck" |
795 | + "launchpad.net/juju/go/cmd/server" |
796 | +) |
797 | + |
798 | +type ProtocolSuite struct{} |
799 | + |
800 | +var _ = Suite(&ProtocolSuite{}) |
801 | + |
802 | +func (s *ProtocolSuite) TestWriteReadRequest(c *C) { |
803 | + buf := bytes.NewBuffer([]byte{}) |
804 | + req0 := &server.Request{"client", []string{"command", "arg"}, "out", "err"} |
805 | + count0, err := req0.WriteTo(buf) |
806 | + c.Assert(err, IsNil) |
807 | + var req1 server.Request |
808 | + count1, err := req1.ReadFrom(buf) |
809 | + c.Assert(err, IsNil) |
810 | + c.Assert(count1, Equals, count0) |
811 | + c.Assert(req1.ClientId, Equals, "client") |
812 | + c.Assert(req1.Args, DeepEquals, []string{"command", "arg"}) |
813 | + c.Assert(req1.OutSock, Equals, "out") |
814 | + c.Assert(req1.ErrSock, Equals, "err") |
815 | +} |
816 | + |
817 | +func (s *ProtocolSuite) TestWriteReadResponse(c *C) { |
818 | + buf := bytes.NewBuffer([]byte{}) |
819 | + resp0 := server.Response(-123) |
820 | + count0, err := resp0.WriteTo(buf) |
821 | + c.Assert(err, IsNil) |
822 | + var resp1 server.Response |
823 | + count1, err := resp1.ReadFrom(buf) |
824 | + c.Assert(err, IsNil) |
825 | + c.Assert(count1, Equals, count0) |
826 | + c.Assert(resp1, Equals, resp0) |
827 | +} |
828 | |
829 | === added file 'cmd/server/server.go' |
830 | --- cmd/server/server.go 1970-01-01 00:00:00 +0000 |
831 | +++ cmd/server/server.go 2012-02-28 17:13:19 +0000 |
832 | @@ -0,0 +1,149 @@ |
833 | +package server |
834 | + |
835 | +import ( |
836 | + "fmt" |
837 | + "io" |
838 | + "launchpad.net/juju/go/cmd" |
839 | + "launchpad.net/tomb" |
840 | + "net" |
841 | +) |
842 | + |
843 | +// clientEnv implements cmd.Env so as to act for the Proxy which communicated a |
844 | +// Request to a Server. |
845 | +type clientEnv struct { |
846 | + stdout io.WriteCloser |
847 | + stderr io.WriteCloser |
848 | + ret io.WriteCloser |
849 | +} |
850 | + |
851 | +// newClientEnv returns a clientEnv whose stdout/stderr are connected to the |
852 | +// sockets specified in req, and whose Exit() will write the return code to conn. |
853 | +func newClientEnv(conn net.Conn, req *Request) (*clientEnv, error) { |
854 | + stdout, err := net.Dial("unix", req.OutSock) |
855 | + if err != nil { |
856 | + return nil, err |
857 | + } |
858 | + stderr, err := net.Dial("unix", req.ErrSock) |
859 | + if err != nil { |
860 | + return nil, err |
861 | + } |
862 | + return &clientEnv{stdout, stderr, conn}, nil |
863 | +} |
864 | + |
865 | +func (e *clientEnv) Stdout() io.Writer { |
866 | + return e.stdout |
867 | +} |
868 | + |
869 | +func (e *clientEnv) Stderr() io.Writer { |
870 | + return e.stderr |
871 | +} |
872 | + |
873 | +func (e *clientEnv) Exit(code int) { |
874 | + response := Response(code) |
875 | + response.WriteTo(e.ret) |
876 | + e.ret.Close() |
877 | + e.stdout.Close() |
878 | + e.stderr.Close() |
879 | +} |
880 | + |
881 | +func (e *clientEnv) InitOutput(verbose bool, debug bool, logfile string) error { |
882 | + // TODO move away from global logging to enable this to do something |
883 | + // helpful? (We don't want to override this process's logging setup with |
884 | + // the proxy command's, but I think it's still worth exposing the flags). |
885 | + return nil |
886 | +} |
887 | + |
888 | +// CmdFactory returns an appropriate Command for the context specified by |
889 | +// clientId. |
890 | +type CmdFactory func(clientId string) (cmd.Command, error) |
891 | + |
892 | +// Server is a bare-bones "RPC" ("RCC"?) server which accepts serialized Request |
893 | +// structs and runs the specified Command, piping its output to the sockets |
894 | +// specified in the Request and writing its return code back to the original |
895 | +// connection. |
896 | +type Server struct { |
897 | + cmdFactory CmdFactory |
898 | + listener net.Listener |
899 | + reqs chan bool |
900 | + *tomb.Tomb |
901 | +} |
902 | + |
903 | +var ( |
904 | + maxReqs = 4 |
905 | +) |
906 | + |
907 | +// NewServer starts a Server listening on sock and exposing the Command |
908 | +// returned by f. |
909 | +func NewServer(f CmdFactory, sock string) (*Server, error) { |
910 | + s := &Server{f, nil, make(chan bool, maxReqs), tomb.New()} |
911 | + if err := s.serve(sock); err != nil { |
912 | + return nil, err |
913 | + } |
914 | + return s, nil |
915 | +} |
916 | + |
917 | +// serve starts listening for Requests sent on sock. |
918 | +func (s *Server) serve(sock string) error { |
919 | + var err error |
920 | + s.listener, err = net.Listen("unix", sock) |
921 | + if err != nil { |
922 | + return err |
923 | + } |
924 | + go func() { |
925 | + <-s.Dying |
926 | + s.listener.Close() |
927 | + for i := 0; i < maxReqs; i++ { |
928 | + s.reqs <- true |
929 | + } |
930 | + s.Done() |
931 | + }() |
932 | + go func() { |
933 | + for { |
934 | + s.reqs <- true |
935 | + conn, err := s.listener.Accept() |
936 | + if err != nil { |
937 | + // if err *is* tomb.Stop, it implies we Stop~ed the listener; |
938 | + // and that the error is an expected connection-lost complaint, |
939 | + // which we should quietly suppress. I feel that this bit could |
940 | + // maybe be done better..? |
941 | + if s.Err() != tomb.Stop { |
942 | + s.Fatal(err) |
943 | + } |
944 | + <-s.reqs |
945 | + return |
946 | + } |
947 | + go s.handle(conn) |
948 | + } |
949 | + }() |
950 | + return nil |
951 | +} |
952 | + |
953 | +// handle reads and handles a single Request from conn. |
954 | +func (s *Server) handle(conn net.Conn) { |
955 | + defer conn.Close() |
956 | + defer func() { <-s.reqs }() |
957 | + |
958 | + req := &Request{} |
959 | + _, err := req.ReadFrom(conn) |
960 | + if err != nil { |
961 | + return |
962 | + } |
963 | + env, err := newClientEnv(conn, req) |
964 | + if err != nil { |
965 | + return |
966 | + } |
967 | + c, err := s.cmdFactory(req.ClientId) |
968 | + if err != nil { |
969 | + fmt.Fprintf(env.Stderr(), "command unavailable: %s", err) |
970 | + env.Exit(1) |
971 | + return |
972 | + } |
973 | + cmd.Main(env, c, req.Args) |
974 | +} |
975 | + |
976 | +// Stop stops the server from accepting new requests, and returns once all |
977 | +// current requests have completed. |
978 | +func (s *Server) Stop() error { |
979 | + s.Fatal(tomb.Stop) |
980 | + return s.Wait() |
981 | +} |
982 | |
983 | === added file 'cmd/server/server_test.go' |
984 | --- cmd/server/server_test.go 1970-01-01 00:00:00 +0000 |
985 | +++ cmd/server/server_test.go 2012-02-28 17:13:19 +0000 |
986 | @@ -0,0 +1,205 @@ |
987 | +package server_test |
988 | + |
989 | +import ( |
990 | + "bytes" |
991 | + "fmt" |
992 | + "io" |
993 | + "launchpad.net/gnuflag" |
994 | + . "launchpad.net/gocheck" |
995 | + "launchpad.net/juju/go/cmd" |
996 | + proxy "launchpad.net/juju/go/cmd/proxy" |
997 | + "launchpad.net/juju/go/cmd/server" |
998 | + "path/filepath" |
999 | + "strings" |
1000 | + "testing" |
1001 | +) |
1002 | + |
1003 | +func Test(t *testing.T) { TestingT(t) } |
1004 | + |
1005 | +type TestCommand struct { |
1006 | + Value string |
1007 | +} |
1008 | + |
1009 | +func (c *TestCommand) Info() *cmd.Info { |
1010 | + return &cmd.Info{"magic", "[options]", "do magic", "blah doc", true} |
1011 | +} |
1012 | + |
1013 | +func (c *TestCommand) InitFlagSet(f *gnuflag.FlagSet) { |
1014 | + f.StringVar(&c.Value, "value", "", "doc") |
1015 | +} |
1016 | + |
1017 | +func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error { |
1018 | + return cmd.CheckEmpty(args) |
1019 | +} |
1020 | + |
1021 | +func (c *TestCommand) Run(e cmd.Env) error { |
1022 | + if c.Value != "zyxxy" { |
1023 | + return fmt.Errorf("insufficiently magic") |
1024 | + } |
1025 | + e.Stdout().Write([]byte("eye of newt")) |
1026 | + e.Stderr().Write([]byte("toe of frog")) |
1027 | + return nil |
1028 | +} |
1029 | + |
1030 | +type WaitCommand struct { |
1031 | + unblock chan bool |
1032 | +} |
1033 | + |
1034 | +func (c *WaitCommand) Info() *cmd.Info { |
1035 | + return &cmd.Info{"wait", "", "wait until unblocked", "blah doc", true} |
1036 | +} |
1037 | + |
1038 | +func (c *WaitCommand) InitFlagSet(f *gnuflag.FlagSet) {} |
1039 | + |
1040 | +func (c *WaitCommand) ParsePositional(e cmd.Env, args []string) error { |
1041 | + return cmd.CheckEmpty(args) |
1042 | +} |
1043 | + |
1044 | +func (c *WaitCommand) Run(e cmd.Env) error { |
1045 | + fmt.Fprintf(e.Stderr(), "waiting...\n") |
1046 | + _, ok := <-c.unblock |
1047 | + fmt.Fprintf(e.Stderr(), "unblocked; ok = %s\n", ok) |
1048 | + if !ok { |
1049 | + return fmt.Errorf("oh no, command failed") |
1050 | + } |
1051 | + return nil |
1052 | +} |
1053 | + |
1054 | +type ServerSuite struct{} |
1055 | + |
1056 | +var _ = Suite(&ServerSuite{}) |
1057 | + |
1058 | +func startServer(c *C, register func(*cmd.SuperCommand)) (string, *server.Server) { |
1059 | + factory := func(clientId string) (cmd.Command, error) { |
1060 | + if clientId != "jeremiah" { |
1061 | + return nil, fmt.Errorf("unknown client") |
1062 | + } |
1063 | + sc := cmd.NewSuperCommand("", "") |
1064 | + register(sc) |
1065 | + return sc, nil |
1066 | + } |
1067 | + sock := filepath.Join(c.MkDir(), "test.sock") |
1068 | + server, err := server.NewServer(factory, sock) |
1069 | + c.Assert(err, IsNil) |
1070 | + return sock, server |
1071 | +} |
1072 | + |
1073 | +func newProxy(sock string, stdout io.Writer, stderr io.Writer) *proxy.Proxy { |
1074 | + if stdout == nil { |
1075 | + stdout = bytes.NewBuffer([]byte{}) |
1076 | + } |
1077 | + if stderr == nil { |
1078 | + stderr = bytes.NewBuffer([]byte{}) |
1079 | + } |
1080 | + return &proxy.Proxy{ |
1081 | + AgentSock: sock, |
1082 | + ClientId: "jeremiah", |
1083 | + Stdout: stdout, |
1084 | + Stderr: stderr, |
1085 | + } |
1086 | +} |
1087 | + |
1088 | +func (s *ServerSuite) TestStartStop(c *C) { |
1089 | + sock, server := startServer(c, func(sc *cmd.SuperCommand) { |
1090 | + sc.Register(&TestCommand{}) |
1091 | + }) |
1092 | + |
1093 | + stdout := bytes.NewBuffer([]byte{}) |
1094 | + stderr := bytes.NewBuffer([]byte{}) |
1095 | + p := newProxy(sock, stdout, stderr) |
1096 | + args := []string{"magic", "--value", "zyxxy"} |
1097 | + result, err := p.Run(args) |
1098 | + c.Assert(err, IsNil) |
1099 | + c.Assert(result, Equals, 0) |
1100 | + c.Assert(stdout.String(), Equals, "eye of newt") |
1101 | + c.Assert(stderr.String(), Equals, "toe of frog") |
1102 | + |
1103 | + server.Stop() |
1104 | + stdout.Reset() |
1105 | + stderr.Reset() |
1106 | + _, err = p.Run(args) |
1107 | + c.Assert(err, NotNil) |
1108 | +} |
1109 | + |
1110 | +func (s *ServerSuite) TestConcurrentStop(c *C) { |
1111 | + concurrency := 4 |
1112 | + unblockOne := make(chan bool) |
1113 | + gotOneRequest := make(chan bool) |
1114 | + sock, server := startServer(c, func(sc *cmd.SuperCommand) { |
1115 | + sc.Register(&WaitCommand{unblockOne}) |
1116 | + gotOneRequest <- true |
1117 | + }) |
1118 | + |
1119 | + args := []string{"wait"} |
1120 | + oneRunComplete := make(chan bool) |
1121 | + for i := 0; i < concurrency; i++ { |
1122 | + go func() { |
1123 | + result, err := newProxy(sock, nil, nil).Run(args) |
1124 | + c.Assert(err, IsNil) |
1125 | + c.Assert(result, Equals, 0) |
1126 | + oneRunComplete <- true |
1127 | + }() |
1128 | + <-gotOneRequest |
1129 | + } |
1130 | + |
1131 | + stopComplete := make(chan bool) |
1132 | + go func() { |
1133 | + err := server.Stop() |
1134 | + c.Assert(err, IsNil) |
1135 | + stopComplete <- true |
1136 | + }() |
1137 | + |
1138 | + for i := 0; i < concurrency; i++ { |
1139 | + select { |
1140 | + case <-server.Dying: |
1141 | + // Jolly good; check it's not accepting new requests. |
1142 | + _, err := newProxy(sock, nil, nil).Run(args) |
1143 | + c.Assert(err, NotNil) |
1144 | + case <-server.Dead: |
1145 | + c.Fatal("server did not finish processing requests") |
1146 | + } |
1147 | + |
1148 | + unblockOne <- true |
1149 | + <-oneRunComplete |
1150 | + } |
1151 | + <-server.Dead |
1152 | + <-stopComplete |
1153 | +} |
1154 | + |
1155 | +func (s *ServerSuite) TestIgnoresHugeRequests(c *C) { |
1156 | + sock, server := startServer(c, func(sc *cmd.SuperCommand) { |
1157 | + c.Fatal("should never call this") |
1158 | + }) |
1159 | + defer server.Stop() |
1160 | + |
1161 | + p := newProxy(sock, nil, nil) |
1162 | + stupidArgs := [1e6]string{} |
1163 | + _, err := p.Run(stupidArgs[:]) |
1164 | + c.Assert(err, NotNil) |
1165 | +} |
1166 | + |
1167 | +func (s *ServerSuite) TestBadCommands(c *C) { |
1168 | + sock, server := startServer(c, func(sc *cmd.SuperCommand) { |
1169 | + sc.Register(&TestCommand{}) |
1170 | + }) |
1171 | + defer server.Stop() |
1172 | + |
1173 | + assertBadCmd := func(clientId string, args []string, expect int, stderr0 string) { |
1174 | + stderr := bytes.NewBuffer([]byte{}) |
1175 | + p := newProxy(sock, nil, stderr) |
1176 | + p.ClientId = clientId |
1177 | + result, err := p.Run(args) |
1178 | + c.Assert(err, IsNil) |
1179 | + c.Assert(result, Equals, expect) |
1180 | + lines := strings.Split(stderr.String(), "\n") |
1181 | + c.Assert(lines[0], Equals, stderr0) |
1182 | + } |
1183 | + assertBadCmd( |
1184 | + "jeremiah", []string{"witchcraft"}, 2, "unrecognised command: witchcraft") |
1185 | + assertBadCmd( |
1186 | + "jeremiah", []string{"magic", "--ham"}, 2, "flag provided but not defined: --ham") |
1187 | + assertBadCmd( |
1188 | + "jeremiah", []string{"magic", "--value", "wrong"}, 1, "insufficiently magic") |
1189 | + assertBadCmd( |
1190 | + "jeff", []string{"magic", "--value", "zyxxy"}, 1, "command unavailable: unknown client") |
1191 | +} |
1192 | |
1193 | === modified file 'cmd/supercommand.go' |
1194 | --- cmd/supercommand.go 2012-02-17 08:03:35 +0000 |
1195 | +++ cmd/supercommand.go 2012-02-28 17:13:19 +0000 |
1196 | @@ -2,11 +2,7 @@ |
1197 | |
1198 | import ( |
1199 | "fmt" |
1200 | - "io" |
1201 | "launchpad.net/gnuflag" |
1202 | - "launchpad.net/juju/go/log" |
1203 | - stdlog "log" |
1204 | - "os" |
1205 | "sort" |
1206 | "strings" |
1207 | ) |
1208 | @@ -72,7 +68,9 @@ |
1209 | var info *Info |
1210 | if c.subcmd != nil { |
1211 | info = c.subcmd.Info() |
1212 | - info.Name = fmt.Sprintf("%s %s", c.Name, info.Name) |
1213 | + if c.Name != "" { |
1214 | + info.Name = fmt.Sprintf("%s %s", c.Name, info.Name) |
1215 | + } |
1216 | return info |
1217 | } |
1218 | return &Info{ |
1219 | @@ -89,7 +87,7 @@ |
1220 | if c.subcmd != nil { |
1221 | c.subcmd.InitFlagSet(f) |
1222 | } |
1223 | - // SuperCommand's flags are always added to subcommands./ Note that the |
1224 | + // SuperCommand's flags are always added to subcommands'. Note that the |
1225 | // flag defaults come from the SuperCommand itself, so that ParsePositional |
1226 | // can call Parse twice on the same SuperCommand without losing information. |
1227 | f.StringVar(&c.LogFile, "log-file", c.LogFile, "path to write log to") |
1228 | @@ -101,9 +99,9 @@ |
1229 | |
1230 | // ParsePositional selects the subcommand specified by subargs and uses it to |
1231 | // Parse any remaining unconsumed command-line arguments. |
1232 | -func (c *SuperCommand) ParsePositional(subargs []string) error { |
1233 | +func (c *SuperCommand) ParsePositional(e Env, subargs []string) error { |
1234 | if c.subcmd != nil { |
1235 | - return c.subcmd.ParsePositional(subargs) |
1236 | + return c.subcmd.ParsePositional(e, subargs) |
1237 | } |
1238 | if len(subargs) == 0 { |
1239 | return fmt.Errorf("no command specified") |
1240 | @@ -112,38 +110,16 @@ |
1241 | if c.subcmd, found = c.subcmds[subargs[0]]; !found { |
1242 | return fmt.Errorf("unrecognised command: %s", subargs[0]) |
1243 | } |
1244 | - return Parse(c, subargs[1:]) |
1245 | -} |
1246 | - |
1247 | -// initOutput sets up logging to a file or to stderr depending on what's been |
1248 | -// requested on the command line. |
1249 | -func (c *SuperCommand) initOutput() error { |
1250 | - if c.Debug { |
1251 | - log.Debug = true |
1252 | - } |
1253 | - var target io.Writer |
1254 | - if c.LogFile != "" { |
1255 | - var err error |
1256 | - target, err = os.OpenFile(c.LogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) |
1257 | - if err != nil { |
1258 | - return err |
1259 | - } |
1260 | - } else if c.Verbose || c.Debug { |
1261 | - target = os.Stderr |
1262 | - } |
1263 | - if target != nil { |
1264 | - log.Target = stdlog.New(target, "", stdlog.LstdFlags) |
1265 | - } |
1266 | - return nil |
1267 | + return Parse(e, c, subargs[1:]) |
1268 | } |
1269 | |
1270 | // Run executes the subcommand that was selected when Parse was called. |
1271 | -func (c *SuperCommand) Run() error { |
1272 | - if err := c.initOutput(); err != nil { |
1273 | +func (c *SuperCommand) Run(e Env) error { |
1274 | + if err := e.InitOutput(c.Verbose, c.Debug, c.LogFile); err != nil { |
1275 | return err |
1276 | } |
1277 | if c.subcmd == nil { |
1278 | panic("Run: missing subcommand; Parse failed or not called") |
1279 | } |
1280 | - return c.subcmd.Run() |
1281 | + return c.subcmd.Run(e) |
1282 | } |
1283 | |
1284 | === modified file 'cmd/supercommand_test.go' |
1285 | --- cmd/supercommand_test.go 2012-02-09 11:35:24 +0000 |
1286 | +++ cmd/supercommand_test.go 2012-02-28 17:13:19 +0000 |
1287 | @@ -31,20 +31,20 @@ |
1288 | f.StringVar(&c.Value, "value", "", "doc") |
1289 | } |
1290 | |
1291 | -func (c *TestCommand) ParsePositional(args []string) error { |
1292 | +func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error { |
1293 | if len(args) != 0 { |
1294 | return fmt.Errorf("BADARGS: %s", args) |
1295 | } |
1296 | return nil |
1297 | } |
1298 | |
1299 | -func (c *TestCommand) Run() error { |
1300 | +func (c *TestCommand) Run(e cmd.Env) error { |
1301 | return fmt.Errorf("BORKEN: value is %s.", c.Value) |
1302 | } |
1303 | |
1304 | func parseEmpty(args []string) (*cmd.SuperCommand, error) { |
1305 | jc := cmd.NewSuperCommand("jujutest", "") |
1306 | - err := cmd.Parse(jc, args) |
1307 | + err := cmd.Parse(&cmd.DefaultEnv{}, jc, args) |
1308 | return jc, err |
1309 | } |
1310 | |
1311 | @@ -52,7 +52,7 @@ |
1312 | jc := cmd.NewSuperCommand("jujutest", "") |
1313 | tc := &TestCommand{Name: "defenestrate"} |
1314 | jc.Register(tc) |
1315 | - err := cmd.Parse(jc, args) |
1316 | + err := cmd.Parse(&cmd.DefaultEnv{}, jc, args) |
1317 | return jc, tc, err |
1318 | } |
1319 | |
1320 | @@ -161,7 +161,7 @@ |
1321 | jc, _, err := parseDefenestrate(args) |
1322 | c.Assert(err, IsNil) |
1323 | |
1324 | - err = jc.Run() |
1325 | + err = jc.Run(&cmd.DefaultEnv{}) |
1326 | c.Assert(err, ErrorMatches, "BORKEN: value is cheese.") |
1327 | |
1328 | c.Assert(log.Debug, Equals, debug) |
Can this be broken down in smaller chunks?
The past changes have all been similarly large in scope. I thought they
might be hard to break down, but I'm getting more skeptical that every
single change needs to touch 20 files. Reviewing, applying comments, and
getting them through the pipeline in general is a lot more traumatic
that way.
https:/ /codereview. appspot. com/5695082/