Merge lp:~fwereade/pyjuju/go-unit-commands into lp:pyjuju/go

Proposed by William Reade
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
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+94885@code.launchpad.net

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.clientEnv implements the abstracted version which communicates
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.

https://codereview.appspot.com/5695082/

To post a comment you must log in.
lp:~fwereade/pyjuju/go-unit-commands updated
80. By William Reade

add tests for command server

81. By William Reade

added tests for request/response

82. By William Reade

doc tweaks

Revision history for this message
Gustavo Niemeyer (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.

https://codereview.appspot.com/5695082/

Revision history for this message
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.

>
> https://codereview.appspot.com/5695082/
>

Revision history for this message
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://niemeyer.net
http://niemeyer.net/plus
http://niemeyer.net/twitter
http://niemeyer.net/blog

-- 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches