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
=== modified file 'cmd/command.go'
--- cmd/command.go 2012-02-15 10:29:05 +0000
+++ cmd/command.go 2012-02-28 17:13:19 +0000
@@ -2,8 +2,10 @@
22
3import (3import (
4 "fmt"4 "fmt"
5 "io"
5 "launchpad.net/gnuflag"6 "launchpad.net/gnuflag"
6 "launchpad.net/juju/go/log"7 "launchpad.net/juju/go/log"
8 stdlog "log"
7 "os"9 "os"
8 "strings"10 "strings"
9)11)
@@ -32,8 +34,7 @@
32 return fmt.Sprintf("%s %s", i.Name, i.Args)34 return fmt.Sprintf("%s %s", i.Name, i.Args)
33}35}
3436
35// Command is implemented by types that interpret any command-line arguments37// Command is implemented by types that interpret command-line arguments.
36// passed to the "juju" command.
37type Command interface {38type Command interface {
38 // Info returns information about the command.39 // Info returns information about the command.
39 Info() *Info40 Info() *Info
@@ -44,40 +45,85 @@
4445
45 // ParsePositional is called by Parse to allow the Command to handle46 // ParsePositional is called by Parse to allow the Command to handle
46 // positional command-line arguments.47 // positional command-line arguments.
47 ParsePositional(args []string) error48 ParsePositional(e Env, args []string) error
4849
49 // Run will execute the command according to the options and positional50 // Run will execute the command according to the options and positional
50 // arguments interpreted by a call to Parse.51 // arguments interpreted by a call to Parse.
51 Run() error52 Run(e Env) error
53}
54
55// Env represents an environment in which a Command can be run.
56type Env interface {
57 Stderr() io.Writer
58 Stdout() io.Writer
59 Exit(int)
60 InitOutput(bool, bool, string) error
61}
62
63// DefaultEnv is the environment in which normal command-line commands are run.
64type DefaultEnv struct{}
65
66func (e *DefaultEnv) Stdout() io.Writer {
67 return os.Stdout
68}
69
70func (e *DefaultEnv) Stderr() io.Writer {
71 return os.Stderr
72}
73
74func (e *DefaultEnv) Exit(code int) {
75 os.Exit(code)
76}
77
78func (e *DefaultEnv) InitOutput(verbose bool, debug bool, logfile string) error {
79 if debug {
80 log.Debug = true
81 }
82 var target io.Writer
83 if logfile != "" {
84 var err error
85 target, err = os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
86 if err != nil {
87 return err
88 }
89 } else if verbose || debug {
90 target = e.Stderr()
91 }
92 if target != nil {
93 log.Target = stdlog.New(target, "", stdlog.LstdFlags)
94 }
95 return nil
52}96}
5397
54// NewFlagSet returns a FlagSet initialized for use with c.98// NewFlagSet returns a FlagSet initialized for use with c.
55func NewFlagSet(c Command) *gnuflag.FlagSet {99func NewFlagSet(e Env, c Command) *gnuflag.FlagSet {
56 f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ExitOnError)100 f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError)
57 f.Usage = func() { PrintUsage(c) }101 f.SetOutput(e.Stderr())
102 f.Usage = func() { PrintUsage(e, c) }
58 c.InitFlagSet(f)103 c.InitFlagSet(f)
59 return f104 return f
60}105}
61106
62// PrintUsage prints usage information for c to stderr.107// PrintUsage prints usage information for c to stderr.
63func PrintUsage(c Command) {108func PrintUsage(e Env, c Command) {
64 i := c.Info()109 i := c.Info()
65 fmt.Fprintf(os.Stderr, "usage: %s\n", i.Usage())110 stderr := e.Stderr()
66 fmt.Fprintf(os.Stderr, "purpose: %s\n", i.Purpose)111 fmt.Fprintf(stderr, "usage: %s\n", i.Usage())
67 fmt.Fprintf(os.Stderr, "\noptions:\n")112 fmt.Fprintf(stderr, "purpose: %s\n", i.Purpose)
68 NewFlagSet(c).PrintDefaults()113 fmt.Fprintf(stderr, "\noptions:\n")
114 NewFlagSet(e, c).PrintDefaults()
69 if i.Doc != "" {115 if i.Doc != "" {
70 fmt.Fprintf(os.Stderr, "\n%s\n", strings.TrimSpace(i.Doc))116 fmt.Fprintf(stderr, "\n%s\n", strings.TrimSpace(i.Doc))
71 }117 }
72}118}
73119
74// Parse parses args on c. This must be called before c is Run.120// Parse parses args on c. This must be called before c is Run.
75func Parse(c Command, args []string) error {121func Parse(e Env, c Command, args []string) error {
76 f := NewFlagSet(c)122 f := NewFlagSet(e, c)
77 if err := f.Parse(c.Info().Intersperse, args); err != nil {123 if err := f.Parse(c.Info().Intersperse, args); err != nil {
78 return err124 return err
79 }125 }
80 return c.ParsePositional(f.Args())126 return c.ParsePositional(e, f.Args())
81}127}
82128
83// CheckEmpty is a utility function that returns an error if args is not empty.129// CheckEmpty is a utility function that returns an error if args is not empty.
@@ -89,16 +135,19 @@
89}135}
90136
91// Main will Parse and Run a Command, and exit appropriately.137// Main will Parse and Run a Command, and exit appropriately.
92func Main(c Command, args []string) {138func Main(e Env, c Command, args []string) {
93 if err := Parse(c, args[1:]); err != nil {139 stderr := e.Stderr()
94 fmt.Fprintf(os.Stderr, "%v\n", err)140 if err := Parse(e, c, args); err != nil {
95 PrintUsage(c)141 fmt.Fprintf(stderr, "%v\n", err)
96 os.Exit(2)142 PrintUsage(e, c)
143 e.Exit(2)
144 return
97 }145 }
98 if err := c.Run(); err != nil {146 if err := c.Run(e); err != nil {
99 log.Debugf("%s command failed: %s\n", c.Info().Name, err)147 log.Debugf("%s command failed: %s\n", c.Info().Name, err)
100 fmt.Fprintf(os.Stderr, "%v\n", err)148 fmt.Fprintf(stderr, "%v\n", err)
101 os.Exit(1)149 e.Exit(1)
150 return
102 }151 }
103 os.Exit(0)152 e.Exit(0)
104}153}
105154
=== modified file 'cmd/juju/bootstrap.go'
--- cmd/juju/bootstrap.go 2012-02-09 11:35:24 +0000
+++ cmd/juju/bootstrap.go 2012-02-28 17:13:19 +0000
@@ -30,13 +30,13 @@
3030
31// ParsePositional checks that no unexpected extra command-line arguments have31// ParsePositional checks that no unexpected extra command-line arguments have
32// been specified.32// been specified.
33func (c *BootstrapCommand) ParsePositional(args []string) error {33func (c *BootstrapCommand) ParsePositional(e cmd.Env, args []string) error {
34 return cmd.CheckEmpty(args)34 return cmd.CheckEmpty(args)
35}35}
3636
37// Run connects to the environment specified on the command line and bootstraps37// Run connects to the environment specified on the command line and bootstraps
38// a juju in that environment if none already exists.38// a juju in that environment if none already exists.
39func (c *BootstrapCommand) Run() error {39func (c *BootstrapCommand) Run(e cmd.Env) error {
40 conn, err := juju.NewConn(c.Environment)40 conn, err := juju.NewConn(c.Environment)
41 if err != nil {41 if err != nil {
42 return err42 return err
4343
=== modified file 'cmd/juju/bootstrap_test.go'
--- cmd/juju/bootstrap_test.go 2012-02-08 11:29:32 +0000
+++ cmd/juju/bootstrap_test.go 2012-02-28 17:13:19 +0000
@@ -12,7 +12,7 @@
1212
13func checkEnv(c *C, args []string, expect string) {13func checkEnv(c *C, args []string, expect string) {
14 bc := &main.BootstrapCommand{}14 bc := &main.BootstrapCommand{}
15 err := cmd.Parse(bc, args)15 err := cmd.Parse(&cmd.DefaultEnv{}, bc, args)
16 c.Assert(err, IsNil)16 c.Assert(err, IsNil)
17 c.Assert(bc.Environment, Equals, expect)17 c.Assert(bc.Environment, Equals, expect)
18}18}
@@ -20,7 +20,7 @@
20func (s *BootstrapSuite) TestEnvironment(c *C) {20func (s *BootstrapSuite) TestEnvironment(c *C) {
21 bc := &main.BootstrapCommand{}21 bc := &main.BootstrapCommand{}
22 c.Assert(bc.Environment, Equals, "")22 c.Assert(bc.Environment, Equals, "")
23 err := cmd.Parse(bc, []string{"hotdog"})23 err := cmd.Parse(&cmd.DefaultEnv{}, bc, []string{"hotdog"})
24 c.Assert(err, ErrorMatches, `unrecognised args: \[hotdog\]`)24 c.Assert(err, ErrorMatches, `unrecognised args: \[hotdog\]`)
25 c.Assert(bc.Environment, Equals, "")25 c.Assert(bc.Environment, Equals, "")
2626
2727
=== modified file 'cmd/juju/main.go'
--- cmd/juju/main.go 2012-02-14 16:42:42 +0000
+++ cmd/juju/main.go 2012-02-28 17:13:19 +0000
@@ -18,7 +18,7 @@
18func Main(args []string) {18func Main(args []string) {
19 jc := cmd.NewSuperCommand("juju", jujuDoc)19 jc := cmd.NewSuperCommand("juju", jujuDoc)
20 jc.Register(&BootstrapCommand{})20 jc.Register(&BootstrapCommand{})
21 cmd.Main(jc, args)21 cmd.Main(&cmd.DefaultEnv{}, jc, args[1:])
22}22}
2323
24func main() {24func main() {
2525
=== modified file 'cmd/jujud/agent.go'
--- cmd/jujud/agent.go 2012-02-21 15:13:47 +0000
+++ cmd/jujud/agent.go 2012-02-28 17:13:19 +0000
@@ -36,7 +36,7 @@
3636
37// ParsePositional checks that there are no unwanted arguments, and that all37// ParsePositional checks that there are no unwanted arguments, and that all
38// required flags have been set.38// required flags have been set.
39func (c *agentConf) ParsePositional(args []string) error {39func (c *agentConf) ParsePositional(e cmd.Env, args []string) error {
40 if c.jujuDir == "" {40 if c.jujuDir == "" {
41 return requiredError("juju-directory")41 return requiredError("juju-directory")
42 }42 }
4343
=== modified file 'cmd/jujud/initzk.go'
--- cmd/jujud/initzk.go 2012-02-21 15:13:47 +0000
+++ cmd/jujud/initzk.go 2012-02-28 17:13:19 +0000
@@ -32,7 +32,7 @@
3232
33// ParsePositional checks that there are no unwanted arguments, and that all33// ParsePositional checks that there are no unwanted arguments, and that all
34// required flags have been set.34// required flags have been set.
35func (c *InitzkCommand) ParsePositional(args []string) error {35func (c *InitzkCommand) ParsePositional(e cmd.Env, args []string) error {
36 if c.StateInfo.Addrs == nil {36 if c.StateInfo.Addrs == nil {
37 return requiredError("zookeeper-servers")37 return requiredError("zookeeper-servers")
38 }38 }
@@ -46,7 +46,7 @@
46}46}
4747
48// Run initializes zookeeper state for an environment.48// Run initializes zookeeper state for an environment.
49func (c *InitzkCommand) Run() error {49func (c *InitzkCommand) Run(e cmd.Env) error {
50 // TODO connect to zookeeper; call State.Initialize50 // TODO connect to zookeeper; call State.Initialize
51 return fmt.Errorf("InitzkCommand.Run not implemented")51 return fmt.Errorf("InitzkCommand.Run not implemented")
52}52}
5353
=== modified file 'cmd/jujud/initzk_test.go'
--- cmd/jujud/initzk_test.go 2012-02-27 08:48:13 +0000
+++ cmd/jujud/initzk_test.go 2012-02-28 17:13:19 +0000
@@ -12,7 +12,7 @@
1212
13func parseInitzkCommand(args []string) (*main.InitzkCommand, error) {13func parseInitzkCommand(args []string) (*main.InitzkCommand, error) {
14 c := &main.InitzkCommand{}14 c := &main.InitzkCommand{}
15 err := cmd.Parse(c, args)15 err := cmd.Parse(&cmd.DefaultEnv{}, c, args)
16 return c, err16 return c, err
17}17}
1818
1919
=== modified file 'cmd/jujud/machine.go'
--- cmd/jujud/machine.go 2012-02-15 12:28:26 +0000
+++ cmd/jujud/machine.go 2012-02-28 17:13:19 +0000
@@ -3,6 +3,7 @@
3import (3import (
4 "fmt"4 "fmt"
5 "launchpad.net/gnuflag"5 "launchpad.net/gnuflag"
6 "launchpad.net/juju/go/cmd"
6)7)
78
8// MachineAgent is a cmd.Command responsible for running a machine agent.9// MachineAgent is a cmd.Command responsible for running a machine agent.
@@ -23,14 +24,14 @@
2324
24// ParsePositional checks that there are no unwanted arguments, and that all25// ParsePositional checks that there are no unwanted arguments, and that all
25// required flags have been set.26// required flags have been set.
26func (a *MachineAgent) ParsePositional(args []string) error {27func (a *MachineAgent) ParsePositional(e cmd.Env, args []string) error {
27 if a.MachineId < 0 {28 if a.MachineId < 0 {
28 return fmt.Errorf("--machine-id option must be set, and expects a non-negative integer")29 return fmt.Errorf("--machine-id option must be set, and expects a non-negative integer")
29 }30 }
30 return a.agentConf.ParsePositional(args)31 return a.agentConf.ParsePositional(e, args)
31}32}
3233
33// Run runs a machine agent.34// Run runs a machine agent.
34func (a *MachineAgent) Run() error {35func (a *MachineAgent) Run(e cmd.Env) error {
35 return fmt.Errorf("MachineAgent.Run not implemented")36 return fmt.Errorf("MachineAgent.Run not implemented")
36}37}
3738
=== modified file 'cmd/jujud/main.go'
--- cmd/jujud/main.go 2012-02-15 10:10:38 +0000
+++ cmd/jujud/main.go 2012-02-28 17:13:19 +0000
@@ -21,7 +21,7 @@
21 jc.Register(NewUnitAgent())21 jc.Register(NewUnitAgent())
22 jc.Register(NewMachineAgent())22 jc.Register(NewMachineAgent())
23 jc.Register(NewProvisioningAgent())23 jc.Register(NewProvisioningAgent())
24 cmd.Main(jc, args)24 cmd.Main(&cmd.DefaultEnv{}, jc, args[1:])
25}25}
2626
27func main() {27func main() {
2828
=== modified file 'cmd/jujud/provisioning.go'
--- cmd/jujud/provisioning.go 2012-02-15 10:29:05 +0000
+++ cmd/jujud/provisioning.go 2012-02-28 17:13:19 +0000
@@ -1,6 +1,9 @@
1package main1package main
22
3import "fmt"3import (
4 "fmt"
5 "launchpad.net/juju/go/cmd"
6)
47
5// ProvisioningAgent is a cmd.Command responsible for running a provisioning agent.8// ProvisioningAgent is a cmd.Command responsible for running a provisioning agent.
6type ProvisioningAgent struct {9type ProvisioningAgent struct {
@@ -12,6 +15,6 @@
12}15}
1316
14// Run runs a provisioning agent.17// Run runs a provisioning agent.
15func (a *ProvisioningAgent) Run() error {18func (a *ProvisioningAgent) Run(e cmd.Env) error {
16 return fmt.Errorf("MachineAgent.Run not implemented")19 return fmt.Errorf("ProvisioningAgent.Run not implemented")
17}20}
1821
=== modified file 'cmd/jujud/unit.go'
--- cmd/jujud/unit.go 2012-02-17 08:03:35 +0000
+++ cmd/jujud/unit.go 2012-02-28 17:13:19 +0000
@@ -3,6 +3,7 @@
3import (3import (
4 "fmt"4 "fmt"
5 "launchpad.net/gnuflag"5 "launchpad.net/gnuflag"
6 "launchpad.net/juju/go/cmd"
6 "launchpad.net/juju/go/juju"7 "launchpad.net/juju/go/juju"
7)8)
89
@@ -24,17 +25,17 @@
2425
25// ParsePositional checks that there are no unwanted arguments, and that all26// ParsePositional checks that there are no unwanted arguments, and that all
26// required flags have been set.27// required flags have been set.
27func (a *UnitAgent) ParsePositional(args []string) error {28func (a *UnitAgent) ParsePositional(e cmd.Env, args []string) error {
28 if a.UnitName == "" {29 if a.UnitName == "" {
29 return requiredError("unit-name")30 return requiredError("unit-name")
30 }31 }
31 if !juju.ValidUnit.MatchString(a.UnitName) {32 if !juju.ValidUnit.MatchString(a.UnitName) {
32 return fmt.Errorf(`--unit-name option expects "<service>/<n>" argument`)33 return fmt.Errorf(`--unit-name option expects "<service>/<n>" argument`)
33 }34 }
34 return a.agentConf.ParsePositional(args)35 return a.agentConf.ParsePositional(e, args)
35}36}
3637
37// Run runs a unit agent.38// Run runs a unit agent.
38func (a *UnitAgent) Run() error {39func (a *UnitAgent) Run(e cmd.Env) error {
39 return fmt.Errorf("UnitAgent.Run not implemented")40 return fmt.Errorf("UnitAgent.Run not implemented")
40}41}
4142
=== modified file 'cmd/jujud/util_test.go'
--- cmd/jujud/util_test.go 2012-02-27 08:48:13 +0000
+++ cmd/jujud/util_test.go 2012-02-28 17:13:19 +0000
@@ -13,23 +13,23 @@
13// command pre-parsed with the always-required options and whatever others13// command pre-parsed with the always-required options and whatever others
14// are necessary to allow parsing to succeed (specified in args).14// are necessary to allow parsing to succeed (specified in args).
15func CheckAgentCommand(c *C, create acCreator, args []string) main.AgentCommand {15func CheckAgentCommand(c *C, create acCreator, args []string) main.AgentCommand {
16 err := cmd.Parse(create(), args)16 err := cmd.Parse(&cmd.DefaultEnv{}, create(), args)
17 c.Assert(err, ErrorMatches, "--zookeeper-servers option must be set")17 c.Assert(err, ErrorMatches, "--zookeeper-servers option must be set")
18 args = append(args, "--zookeeper-servers", "zk1:2181,zk2:2181")18 args = append(args, "--zookeeper-servers", "zk1:2181,zk2:2181")
1919
20 err = cmd.Parse(create(), args)20 err = cmd.Parse(&cmd.DefaultEnv{}, create(), args)
21 c.Assert(err, ErrorMatches, "--session-file option must be set")21 c.Assert(err, ErrorMatches, "--session-file option must be set")
22 args = append(args, "--session-file", "sf")22 args = append(args, "--session-file", "sf")
2323
24 ac := create()24 ac := create()
25 c.Assert(cmd.Parse(ac, args), IsNil)25 c.Assert(cmd.Parse(&cmd.DefaultEnv{}, ac, args), IsNil)
26 c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})26 c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})
27 c.Assert(ac.SessionFile(), Equals, "sf")27 c.Assert(ac.SessionFile(), Equals, "sf")
28 c.Assert(ac.JujuDir(), Equals, "/var/lib/juju")28 c.Assert(ac.JujuDir(), Equals, "/var/lib/juju")
29 args = append(args, "--juju-directory", "jd")29 args = append(args, "--juju-directory", "jd")
3030
31 ac = create()31 ac = create()
32 c.Assert(cmd.Parse(ac, args), IsNil)32 c.Assert(cmd.Parse(&cmd.DefaultEnv{}, ac, args), IsNil)
33 c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})33 c.Assert(ac.StateInfo().Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})
34 c.Assert(ac.SessionFile(), Equals, "sf")34 c.Assert(ac.SessionFile(), Equals, "sf")
35 c.Assert(ac.JujuDir(), Equals, "jd")35 c.Assert(ac.JujuDir(), Equals, "jd")
@@ -44,5 +44,5 @@
44 "--session-file", "sf",44 "--session-file", "sf",
45 "--juju-directory", "jd",45 "--juju-directory", "jd",
46 }46 }
47 return cmd.Parse(ac, append(common, args...))47 return cmd.Parse(&cmd.DefaultEnv{}, ac, append(common, args...))
48}48}
4949
=== added directory 'cmd/proxy'
=== added file 'cmd/proxy/main.go'
--- cmd/proxy/main.go 1970-01-01 00:00:00 +0000
+++ cmd/proxy/main.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,42 @@
1package main
2
3import (
4 "fmt"
5 "os"
6)
7
8// requireEnv returns an environment variable, and calls os.Exit(2) if it's not
9// set.
10func requireEnv(name string) string {
11 value := os.Getenv(name)
12 if value == "" {
13 fmt.Fprintf(os.Stderr, "%s not set\n", name)
14 os.Exit(2)
15 }
16 return value
17}
18
19// Main implements every command that calls back into a juju unit agent by using
20// a Proxy to ask the unit agent to execute the actual command for us and pipe
21// back stdout/stderr. This function is not redundant with main, because it
22// provides an entry point for testing with arbitrary command line arguments.
23// Individual commands should be exposed by symlinking the command name to this
24// executable.
25func Main(args []string) {
26 p := &Proxy{
27 AgentSock: requireEnv("JUJU_AGENT_SOCKET"),
28 ClientId: requireEnv("JUJU_CLIENT_ID"),
29 Stdout: os.Stdout,
30 Stderr: os.Stderr,
31 }
32 result, err := p.Run(args)
33 if err != nil {
34 fmt.Fprintf(os.Stderr, "%s command failed: %v\n", args[0], err)
35 os.Exit(1)
36 }
37 os.Exit(result)
38}
39
40func main() {
41 Main(os.Args)
42}
043
=== added file 'cmd/proxy/proxy.go'
--- cmd/proxy/proxy.go 1970-01-01 00:00:00 +0000
+++ cmd/proxy/proxy.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,103 @@
1package main
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "launchpad.net/juju/go/cmd/server"
8 "net"
9 "os"
10 "path/filepath"
11)
12
13// Proxy is responsible for asking a running Server to execute a Command, and
14// for propagating the Command's output and return code.
15type Proxy struct {
16 AgentSock string
17 ClientId string
18 Stdout io.Writer
19 Stderr io.Writer
20}
21
22// connect listens for a connection on sock, and echoes anything received to dst.
23func connect(dst io.Writer, sock string) (<-chan bool, error) {
24 l, err := net.Listen("unix", sock)
25 if err != nil {
26 return nil, err
27 }
28 done := make(chan bool)
29 go func() {
30 defer close(done)
31 defer os.Remove(sock)
32 defer l.Close()
33 conn, err := l.Accept()
34 if err != nil {
35 return
36 }
37 defer conn.Close()
38 _, err = io.Copy(dst, conn)
39 if err != nil {
40 return
41 }
42 done <- true
43 }()
44 return done, nil
45}
46
47// Run connects to a Server listening on p.AgentSock, asks it to run the Command
48// specified by args, forwards the Command's stdout/stderr to p.Stdout/p.Stderr,
49// and returns the Command's result code.
50func (p *Proxy) Run(args []string) (int, error) {
51 // Create tempdir for stdout/stderr sockets.
52 sockDir, err := ioutil.TempDir("", "juju-proxy")
53 if err != nil {
54 return 0, err
55 }
56 defer os.RemoveAll(sockDir)
57
58 // Pipe data from sockets into stdout/stderr.
59 outSock := filepath.Join(sockDir, "stdout.sock")
60 outDone, err := connect(p.Stdout, outSock)
61 if err != nil {
62 return 0, err
63 }
64 errSock := filepath.Join(sockDir, "stderr.sock")
65 errDone, err := connect(p.Stderr, errSock)
66 if err != nil {
67 return 0, err
68 }
69
70 // Connect to the agent socket.
71 conn, err := net.Dial("unix", p.AgentSock)
72 if err != nil {
73 return 0, err
74 }
75 defer conn.Close()
76
77 // Tell the agent to run the command specified by args (with the command
78 // name suitably trimmed) and pipe its output back to our sockets.
79 args[0] = filepath.Base(args[0])
80 req := &server.Request{p.ClientId, args, outSock, errSock}
81 _, err = req.WriteTo(conn)
82 if err != nil {
83 return 0, err
84 }
85
86 // Extract response code from agent connection.
87 var response server.Response
88 _, err = response.ReadFrom(conn)
89 if err != nil {
90 return 0, err
91 }
92
93 // Wait for streams to close before returning.
94 _, ok := <-outDone
95 if !ok {
96 return 0, fmt.Errorf("stdout connection failed on %s", outSock)
97 }
98 _, ok = <-errDone
99 if !ok {
100 return 0, fmt.Errorf("stderr connection failed on %s", errSock)
101 }
102 return int(response), nil
103}
0104
=== added file 'cmd/proxy/proxy_test.go'
--- cmd/proxy/proxy_test.go 1970-01-01 00:00:00 +0000
+++ cmd/proxy/proxy_test.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,136 @@
1package main_test
2
3import (
4 "bytes"
5 "fmt"
6 "launchpad.net/gnuflag"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju/go/cmd"
9 main "launchpad.net/juju/go/cmd/proxy"
10 "launchpad.net/juju/go/cmd/server"
11 "path/filepath"
12 "strings"
13 "testing"
14)
15
16func Test(t *testing.T) { TestingT(t) }
17
18type TestCommand struct {
19 Value string
20}
21
22func (c *TestCommand) Info() *cmd.Info {
23 return &cmd.Info{"magic", "[options]", "do magic", "blah doc", true}
24}
25
26func (c *TestCommand) InitFlagSet(f *gnuflag.FlagSet) {
27 f.StringVar(&c.Value, "value", "", "doc")
28}
29
30func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error {
31 if len(args) != 0 {
32 return fmt.Errorf("BADARGS: %s", args)
33 }
34 return nil
35}
36
37func (c *TestCommand) Run(e cmd.Env) error {
38 if c.Value != "zyxxy" {
39 return fmt.Errorf("insufficiently magic")
40 }
41 e.Stdout().Write([]byte("eye of newt"))
42 e.Stderr().Write([]byte("toe of frog"))
43 return nil
44}
45
46type ProxySuite struct {
47 sock string
48 server *server.Server
49}
50
51var _ = Suite(&ProxySuite{})
52
53func (s *ProxySuite) SetUpSuite(c *C) {
54 factory := func(clientId string) (cmd.Command, error) {
55 if clientId != "jeremiah" {
56 return nil, fmt.Errorf("unknown client")
57 }
58 sc := cmd.NewSuperCommand("", "")
59 sc.Register(&TestCommand{})
60 return sc, nil
61 }
62 s.sock = filepath.Join(c.MkDir(), "test.sock")
63 var err error
64 s.server, err = server.NewServer(factory, s.sock)
65 c.Assert(err, IsNil)
66}
67
68func (s *ProxySuite) TearDownSuite(c *C) {
69 c.Assert(s.server.Stop(), IsNil)
70}
71
72func (s *ProxySuite) AssertCall(c *C, clientId string, args []string) (int, string, string) {
73 stdout := bytes.NewBuffer([]byte{})
74 stderr := bytes.NewBuffer([]byte{})
75 p := &main.Proxy{
76 AgentSock: s.sock,
77 ClientId: clientId,
78 Stdout: stdout,
79 Stderr: stderr,
80 }
81 result, err := p.Run(args)
82 c.Assert(err, IsNil)
83 return result, stdout.String(), stderr.String()
84}
85
86func (s *ProxySuite) TestSuccess(c *C) {
87 result, stdout, stderr := s.AssertCall(
88 c, "jeremiah", []string{"magic", "--value", "zyxxy"})
89 c.Assert(result, Equals, 0)
90 c.Assert(stdout, Equals, "eye of newt")
91 c.Assert(stderr, Equals, "toe of frog")
92}
93
94func (s *ProxySuite) TestCommandPath(c *C) {
95 result, stdout, stderr := s.AssertCall(
96 c, "jeremiah", []string{"some/path/to/magic", "--value", "zyxxy"})
97 c.Assert(result, Equals, 0)
98 c.Assert(stdout, Equals, "eye of newt")
99 c.Assert(stderr, Equals, "toe of frog")
100}
101
102func (s *ProxySuite) TestBadRun(c *C) {
103 result, stdout, stderr := s.AssertCall(
104 c, "jeremiah", []string{"magic", "--value", "wrong"})
105 c.Assert(result, Equals, 1)
106 c.Assert(stdout, Equals, "")
107 errLines := strings.Split(stderr, "\n")
108 c.Assert(errLines[0], Equals, "insufficiently magic")
109}
110
111func (s *ProxySuite) TestBadClient(c *C) {
112 result, stdout, stderr := s.AssertCall(
113 c, "methuselah", []string{"magic"})
114 c.Assert(result, Equals, 1)
115 c.Assert(stdout, Equals, "")
116 errLines := strings.Split(stderr, "\n")
117 c.Assert(errLines[0], Equals, "command unavailable: unknown client")
118}
119
120func (s *ProxySuite) TestNonsenseArgs(c *C) {
121 result, stdout, stderr := s.AssertCall(
122 c, "jeremiah", []string{"magic", "--cheese"})
123 c.Assert(result, Equals, 2)
124 c.Assert(stdout, Equals, "")
125 errLines := strings.Split(stderr, "\n")
126 c.Assert(errLines[0], Equals, "flag provided but not defined: --cheese")
127}
128
129func (s *ProxySuite) TestUnknownCommand(c *C) {
130 result, stdout, stderr := s.AssertCall(
131 c, "jeremiah", []string{"witchcraft"})
132 c.Assert(result, Equals, 2)
133 c.Assert(stdout, Equals, "")
134 errLines := strings.Split(stderr, "\n")
135 c.Assert(errLines[0], Equals, "unrecognised command: witchcraft")
136}
0137
=== added directory 'cmd/server'
=== added file 'cmd/server/protocol.go'
--- cmd/server/protocol.go 1970-01-01 00:00:00 +0000
+++ cmd/server/protocol.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,86 @@
1package server
2
3import (
4 "encoding/binary"
5 "fmt"
6 "io"
7 "launchpad.net/goyaml"
8)
9
10// Request contains all the information necessary for a Server to run a Command
11// on behalf of a Proxy.
12type Request struct {
13 ClientId string
14 Args []string
15 OutSock string
16 ErrSock string
17}
18
19var (
20 bo = binary.BigEndian
21 codeS = int64(binary.Size(int32(0)))
22 sizeS = int64(binary.Size(uint32(0)))
23 maxSize = uint32(1024 * 1024)
24)
25
26// ReadFrom fills in a Request's fields using data read from src (which is
27// expected in the format written by WriteTo).
28func (r *Request) ReadFrom(src io.Reader) (count int64, err error) {
29 var size uint32
30 err = binary.Read(src, bo, &size)
31 if err != nil {
32 return
33 }
34 count += sizeS
35 if size > maxSize {
36 err = fmt.Errorf("unreasonably large request: %d bytes", size)
37 return
38 }
39 data := make([]byte, size)
40 n, err := io.ReadFull(src, data[:])
41 count += int64(n)
42 if err != nil {
43 return
44 }
45 err = goyaml.Unmarshal(data, r)
46 return
47}
48
49// WriteTo writes a Request's data to dst, in a format suitable for
50// reconstruction by ReadFrom.
51func (r *Request) WriteTo(dst io.Writer) (int64, error) {
52 yaml, err := goyaml.Marshal(r)
53 if err != nil {
54 return 0, err
55 }
56 bytes := []byte(yaml)
57 err = binary.Write(dst, bo, uint32(len(bytes)))
58 if err != nil {
59 return 0, err
60 }
61 n, err := dst.Write(bytes)
62 return int64(n) + sizeS, err
63}
64
65// Response is a command's return code, with extra methods to make it convenient
66// to transmit and interpret.
67type Response int32
68
69// ReadFrom decodes a Response from src (expected in the format written by WriteTo).
70func (r *Response) ReadFrom(src io.Reader) (int64, error) {
71 err := binary.Read(src, bo, r)
72 if err != nil {
73 return 0, err
74 }
75 return codeS, nil
76}
77
78// WriteTo encodes a Response to dst, in a format suitable for reconstruction by
79// ReadFrom.
80func (r *Response) WriteTo(dst io.Writer) (int64, error) {
81 err := binary.Write(dst, bo, r)
82 if err != nil {
83 return 0, err
84 }
85 return codeS, nil
86}
087
=== added file 'cmd/server/protocol_test.go'
--- cmd/server/protocol_test.go 1970-01-01 00:00:00 +0000
+++ cmd/server/protocol_test.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,38 @@
1package server_test
2
3import (
4 "bytes"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/cmd/server"
7)
8
9type ProtocolSuite struct{}
10
11var _ = Suite(&ProtocolSuite{})
12
13func (s *ProtocolSuite) TestWriteReadRequest(c *C) {
14 buf := bytes.NewBuffer([]byte{})
15 req0 := &server.Request{"client", []string{"command", "arg"}, "out", "err"}
16 count0, err := req0.WriteTo(buf)
17 c.Assert(err, IsNil)
18 var req1 server.Request
19 count1, err := req1.ReadFrom(buf)
20 c.Assert(err, IsNil)
21 c.Assert(count1, Equals, count0)
22 c.Assert(req1.ClientId, Equals, "client")
23 c.Assert(req1.Args, DeepEquals, []string{"command", "arg"})
24 c.Assert(req1.OutSock, Equals, "out")
25 c.Assert(req1.ErrSock, Equals, "err")
26}
27
28func (s *ProtocolSuite) TestWriteReadResponse(c *C) {
29 buf := bytes.NewBuffer([]byte{})
30 resp0 := server.Response(-123)
31 count0, err := resp0.WriteTo(buf)
32 c.Assert(err, IsNil)
33 var resp1 server.Response
34 count1, err := resp1.ReadFrom(buf)
35 c.Assert(err, IsNil)
36 c.Assert(count1, Equals, count0)
37 c.Assert(resp1, Equals, resp0)
38}
039
=== added file 'cmd/server/server.go'
--- cmd/server/server.go 1970-01-01 00:00:00 +0000
+++ cmd/server/server.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,149 @@
1package server
2
3import (
4 "fmt"
5 "io"
6 "launchpad.net/juju/go/cmd"
7 "launchpad.net/tomb"
8 "net"
9)
10
11// clientEnv implements cmd.Env so as to act for the Proxy which communicated a
12// Request to a Server.
13type clientEnv struct {
14 stdout io.WriteCloser
15 stderr io.WriteCloser
16 ret io.WriteCloser
17}
18
19// newClientEnv returns a clientEnv whose stdout/stderr are connected to the
20// sockets specified in req, and whose Exit() will write the return code to conn.
21func newClientEnv(conn net.Conn, req *Request) (*clientEnv, error) {
22 stdout, err := net.Dial("unix", req.OutSock)
23 if err != nil {
24 return nil, err
25 }
26 stderr, err := net.Dial("unix", req.ErrSock)
27 if err != nil {
28 return nil, err
29 }
30 return &clientEnv{stdout, stderr, conn}, nil
31}
32
33func (e *clientEnv) Stdout() io.Writer {
34 return e.stdout
35}
36
37func (e *clientEnv) Stderr() io.Writer {
38 return e.stderr
39}
40
41func (e *clientEnv) Exit(code int) {
42 response := Response(code)
43 response.WriteTo(e.ret)
44 e.ret.Close()
45 e.stdout.Close()
46 e.stderr.Close()
47}
48
49func (e *clientEnv) InitOutput(verbose bool, debug bool, logfile string) error {
50 // TODO move away from global logging to enable this to do something
51 // helpful? (We don't want to override this process's logging setup with
52 // the proxy command's, but I think it's still worth exposing the flags).
53 return nil
54}
55
56// CmdFactory returns an appropriate Command for the context specified by
57// clientId.
58type CmdFactory func(clientId string) (cmd.Command, error)
59
60// Server is a bare-bones "RPC" ("RCC"?) server which accepts serialized Request
61// structs and runs the specified Command, piping its output to the sockets
62// specified in the Request and writing its return code back to the original
63// connection.
64type Server struct {
65 cmdFactory CmdFactory
66 listener net.Listener
67 reqs chan bool
68 *tomb.Tomb
69}
70
71var (
72 maxReqs = 4
73)
74
75// NewServer starts a Server listening on sock and exposing the Command
76// returned by f.
77func NewServer(f CmdFactory, sock string) (*Server, error) {
78 s := &Server{f, nil, make(chan bool, maxReqs), tomb.New()}
79 if err := s.serve(sock); err != nil {
80 return nil, err
81 }
82 return s, nil
83}
84
85// serve starts listening for Requests sent on sock.
86func (s *Server) serve(sock string) error {
87 var err error
88 s.listener, err = net.Listen("unix", sock)
89 if err != nil {
90 return err
91 }
92 go func() {
93 <-s.Dying
94 s.listener.Close()
95 for i := 0; i < maxReqs; i++ {
96 s.reqs <- true
97 }
98 s.Done()
99 }()
100 go func() {
101 for {
102 s.reqs <- true
103 conn, err := s.listener.Accept()
104 if err != nil {
105 // if err *is* tomb.Stop, it implies we Stop~ed the listener;
106 // and that the error is an expected connection-lost complaint,
107 // which we should quietly suppress. I feel that this bit could
108 // maybe be done better..?
109 if s.Err() != tomb.Stop {
110 s.Fatal(err)
111 }
112 <-s.reqs
113 return
114 }
115 go s.handle(conn)
116 }
117 }()
118 return nil
119}
120
121// handle reads and handles a single Request from conn.
122func (s *Server) handle(conn net.Conn) {
123 defer conn.Close()
124 defer func() { <-s.reqs }()
125
126 req := &Request{}
127 _, err := req.ReadFrom(conn)
128 if err != nil {
129 return
130 }
131 env, err := newClientEnv(conn, req)
132 if err != nil {
133 return
134 }
135 c, err := s.cmdFactory(req.ClientId)
136 if err != nil {
137 fmt.Fprintf(env.Stderr(), "command unavailable: %s", err)
138 env.Exit(1)
139 return
140 }
141 cmd.Main(env, c, req.Args)
142}
143
144// Stop stops the server from accepting new requests, and returns once all
145// current requests have completed.
146func (s *Server) Stop() error {
147 s.Fatal(tomb.Stop)
148 return s.Wait()
149}
0150
=== added file 'cmd/server/server_test.go'
--- cmd/server/server_test.go 1970-01-01 00:00:00 +0000
+++ cmd/server/server_test.go 2012-02-28 17:13:19 +0000
@@ -0,0 +1,205 @@
1package server_test
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "launchpad.net/gnuflag"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju/go/cmd"
10 proxy "launchpad.net/juju/go/cmd/proxy"
11 "launchpad.net/juju/go/cmd/server"
12 "path/filepath"
13 "strings"
14 "testing"
15)
16
17func Test(t *testing.T) { TestingT(t) }
18
19type TestCommand struct {
20 Value string
21}
22
23func (c *TestCommand) Info() *cmd.Info {
24 return &cmd.Info{"magic", "[options]", "do magic", "blah doc", true}
25}
26
27func (c *TestCommand) InitFlagSet(f *gnuflag.FlagSet) {
28 f.StringVar(&c.Value, "value", "", "doc")
29}
30
31func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error {
32 return cmd.CheckEmpty(args)
33}
34
35func (c *TestCommand) Run(e cmd.Env) error {
36 if c.Value != "zyxxy" {
37 return fmt.Errorf("insufficiently magic")
38 }
39 e.Stdout().Write([]byte("eye of newt"))
40 e.Stderr().Write([]byte("toe of frog"))
41 return nil
42}
43
44type WaitCommand struct {
45 unblock chan bool
46}
47
48func (c *WaitCommand) Info() *cmd.Info {
49 return &cmd.Info{"wait", "", "wait until unblocked", "blah doc", true}
50}
51
52func (c *WaitCommand) InitFlagSet(f *gnuflag.FlagSet) {}
53
54func (c *WaitCommand) ParsePositional(e cmd.Env, args []string) error {
55 return cmd.CheckEmpty(args)
56}
57
58func (c *WaitCommand) Run(e cmd.Env) error {
59 fmt.Fprintf(e.Stderr(), "waiting...\n")
60 _, ok := <-c.unblock
61 fmt.Fprintf(e.Stderr(), "unblocked; ok = %s\n", ok)
62 if !ok {
63 return fmt.Errorf("oh no, command failed")
64 }
65 return nil
66}
67
68type ServerSuite struct{}
69
70var _ = Suite(&ServerSuite{})
71
72func startServer(c *C, register func(*cmd.SuperCommand)) (string, *server.Server) {
73 factory := func(clientId string) (cmd.Command, error) {
74 if clientId != "jeremiah" {
75 return nil, fmt.Errorf("unknown client")
76 }
77 sc := cmd.NewSuperCommand("", "")
78 register(sc)
79 return sc, nil
80 }
81 sock := filepath.Join(c.MkDir(), "test.sock")
82 server, err := server.NewServer(factory, sock)
83 c.Assert(err, IsNil)
84 return sock, server
85}
86
87func newProxy(sock string, stdout io.Writer, stderr io.Writer) *proxy.Proxy {
88 if stdout == nil {
89 stdout = bytes.NewBuffer([]byte{})
90 }
91 if stderr == nil {
92 stderr = bytes.NewBuffer([]byte{})
93 }
94 return &proxy.Proxy{
95 AgentSock: sock,
96 ClientId: "jeremiah",
97 Stdout: stdout,
98 Stderr: stderr,
99 }
100}
101
102func (s *ServerSuite) TestStartStop(c *C) {
103 sock, server := startServer(c, func(sc *cmd.SuperCommand) {
104 sc.Register(&TestCommand{})
105 })
106
107 stdout := bytes.NewBuffer([]byte{})
108 stderr := bytes.NewBuffer([]byte{})
109 p := newProxy(sock, stdout, stderr)
110 args := []string{"magic", "--value", "zyxxy"}
111 result, err := p.Run(args)
112 c.Assert(err, IsNil)
113 c.Assert(result, Equals, 0)
114 c.Assert(stdout.String(), Equals, "eye of newt")
115 c.Assert(stderr.String(), Equals, "toe of frog")
116
117 server.Stop()
118 stdout.Reset()
119 stderr.Reset()
120 _, err = p.Run(args)
121 c.Assert(err, NotNil)
122}
123
124func (s *ServerSuite) TestConcurrentStop(c *C) {
125 concurrency := 4
126 unblockOne := make(chan bool)
127 gotOneRequest := make(chan bool)
128 sock, server := startServer(c, func(sc *cmd.SuperCommand) {
129 sc.Register(&WaitCommand{unblockOne})
130 gotOneRequest <- true
131 })
132
133 args := []string{"wait"}
134 oneRunComplete := make(chan bool)
135 for i := 0; i < concurrency; i++ {
136 go func() {
137 result, err := newProxy(sock, nil, nil).Run(args)
138 c.Assert(err, IsNil)
139 c.Assert(result, Equals, 0)
140 oneRunComplete <- true
141 }()
142 <-gotOneRequest
143 }
144
145 stopComplete := make(chan bool)
146 go func() {
147 err := server.Stop()
148 c.Assert(err, IsNil)
149 stopComplete <- true
150 }()
151
152 for i := 0; i < concurrency; i++ {
153 select {
154 case <-server.Dying:
155 // Jolly good; check it's not accepting new requests.
156 _, err := newProxy(sock, nil, nil).Run(args)
157 c.Assert(err, NotNil)
158 case <-server.Dead:
159 c.Fatal("server did not finish processing requests")
160 }
161
162 unblockOne <- true
163 <-oneRunComplete
164 }
165 <-server.Dead
166 <-stopComplete
167}
168
169func (s *ServerSuite) TestIgnoresHugeRequests(c *C) {
170 sock, server := startServer(c, func(sc *cmd.SuperCommand) {
171 c.Fatal("should never call this")
172 })
173 defer server.Stop()
174
175 p := newProxy(sock, nil, nil)
176 stupidArgs := [1e6]string{}
177 _, err := p.Run(stupidArgs[:])
178 c.Assert(err, NotNil)
179}
180
181func (s *ServerSuite) TestBadCommands(c *C) {
182 sock, server := startServer(c, func(sc *cmd.SuperCommand) {
183 sc.Register(&TestCommand{})
184 })
185 defer server.Stop()
186
187 assertBadCmd := func(clientId string, args []string, expect int, stderr0 string) {
188 stderr := bytes.NewBuffer([]byte{})
189 p := newProxy(sock, nil, stderr)
190 p.ClientId = clientId
191 result, err := p.Run(args)
192 c.Assert(err, IsNil)
193 c.Assert(result, Equals, expect)
194 lines := strings.Split(stderr.String(), "\n")
195 c.Assert(lines[0], Equals, stderr0)
196 }
197 assertBadCmd(
198 "jeremiah", []string{"witchcraft"}, 2, "unrecognised command: witchcraft")
199 assertBadCmd(
200 "jeremiah", []string{"magic", "--ham"}, 2, "flag provided but not defined: --ham")
201 assertBadCmd(
202 "jeremiah", []string{"magic", "--value", "wrong"}, 1, "insufficiently magic")
203 assertBadCmd(
204 "jeff", []string{"magic", "--value", "zyxxy"}, 1, "command unavailable: unknown client")
205}
0206
=== modified file 'cmd/supercommand.go'
--- cmd/supercommand.go 2012-02-17 08:03:35 +0000
+++ cmd/supercommand.go 2012-02-28 17:13:19 +0000
@@ -2,11 +2,7 @@
22
3import (3import (
4 "fmt"4 "fmt"
5 "io"
6 "launchpad.net/gnuflag"5 "launchpad.net/gnuflag"
7 "launchpad.net/juju/go/log"
8 stdlog "log"
9 "os"
10 "sort"6 "sort"
11 "strings"7 "strings"
12)8)
@@ -72,7 +68,9 @@
72 var info *Info68 var info *Info
73 if c.subcmd != nil {69 if c.subcmd != nil {
74 info = c.subcmd.Info()70 info = c.subcmd.Info()
75 info.Name = fmt.Sprintf("%s %s", c.Name, info.Name)71 if c.Name != "" {
72 info.Name = fmt.Sprintf("%s %s", c.Name, info.Name)
73 }
76 return info74 return info
77 }75 }
78 return &Info{76 return &Info{
@@ -89,7 +87,7 @@
89 if c.subcmd != nil {87 if c.subcmd != nil {
90 c.subcmd.InitFlagSet(f)88 c.subcmd.InitFlagSet(f)
91 }89 }
92 // SuperCommand's flags are always added to subcommands./ Note that the90 // SuperCommand's flags are always added to subcommands'. Note that the
93 // flag defaults come from the SuperCommand itself, so that ParsePositional91 // flag defaults come from the SuperCommand itself, so that ParsePositional
94 // can call Parse twice on the same SuperCommand without losing information.92 // can call Parse twice on the same SuperCommand without losing information.
95 f.StringVar(&c.LogFile, "log-file", c.LogFile, "path to write log to")93 f.StringVar(&c.LogFile, "log-file", c.LogFile, "path to write log to")
@@ -101,9 +99,9 @@
10199
102// ParsePositional selects the subcommand specified by subargs and uses it to100// ParsePositional selects the subcommand specified by subargs and uses it to
103// Parse any remaining unconsumed command-line arguments.101// Parse any remaining unconsumed command-line arguments.
104func (c *SuperCommand) ParsePositional(subargs []string) error {102func (c *SuperCommand) ParsePositional(e Env, subargs []string) error {
105 if c.subcmd != nil {103 if c.subcmd != nil {
106 return c.subcmd.ParsePositional(subargs)104 return c.subcmd.ParsePositional(e, subargs)
107 }105 }
108 if len(subargs) == 0 {106 if len(subargs) == 0 {
109 return fmt.Errorf("no command specified")107 return fmt.Errorf("no command specified")
@@ -112,38 +110,16 @@
112 if c.subcmd, found = c.subcmds[subargs[0]]; !found {110 if c.subcmd, found = c.subcmds[subargs[0]]; !found {
113 return fmt.Errorf("unrecognised command: %s", subargs[0])111 return fmt.Errorf("unrecognised command: %s", subargs[0])
114 }112 }
115 return Parse(c, subargs[1:])113 return Parse(e, c, subargs[1:])
116}
117
118// initOutput sets up logging to a file or to stderr depending on what's been
119// requested on the command line.
120func (c *SuperCommand) initOutput() error {
121 if c.Debug {
122 log.Debug = true
123 }
124 var target io.Writer
125 if c.LogFile != "" {
126 var err error
127 target, err = os.OpenFile(c.LogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
128 if err != nil {
129 return err
130 }
131 } else if c.Verbose || c.Debug {
132 target = os.Stderr
133 }
134 if target != nil {
135 log.Target = stdlog.New(target, "", stdlog.LstdFlags)
136 }
137 return nil
138}114}
139115
140// Run executes the subcommand that was selected when Parse was called.116// Run executes the subcommand that was selected when Parse was called.
141func (c *SuperCommand) Run() error {117func (c *SuperCommand) Run(e Env) error {
142 if err := c.initOutput(); err != nil {118 if err := e.InitOutput(c.Verbose, c.Debug, c.LogFile); err != nil {
143 return err119 return err
144 }120 }
145 if c.subcmd == nil {121 if c.subcmd == nil {
146 panic("Run: missing subcommand; Parse failed or not called")122 panic("Run: missing subcommand; Parse failed or not called")
147 }123 }
148 return c.subcmd.Run()124 return c.subcmd.Run(e)
149}125}
150126
=== modified file 'cmd/supercommand_test.go'
--- cmd/supercommand_test.go 2012-02-09 11:35:24 +0000
+++ cmd/supercommand_test.go 2012-02-28 17:13:19 +0000
@@ -31,20 +31,20 @@
31 f.StringVar(&c.Value, "value", "", "doc")31 f.StringVar(&c.Value, "value", "", "doc")
32}32}
3333
34func (c *TestCommand) ParsePositional(args []string) error {34func (c *TestCommand) ParsePositional(e cmd.Env, args []string) error {
35 if len(args) != 0 {35 if len(args) != 0 {
36 return fmt.Errorf("BADARGS: %s", args)36 return fmt.Errorf("BADARGS: %s", args)
37 }37 }
38 return nil38 return nil
39}39}
4040
41func (c *TestCommand) Run() error {41func (c *TestCommand) Run(e cmd.Env) error {
42 return fmt.Errorf("BORKEN: value is %s.", c.Value)42 return fmt.Errorf("BORKEN: value is %s.", c.Value)
43}43}
4444
45func parseEmpty(args []string) (*cmd.SuperCommand, error) {45func parseEmpty(args []string) (*cmd.SuperCommand, error) {
46 jc := cmd.NewSuperCommand("jujutest", "")46 jc := cmd.NewSuperCommand("jujutest", "")
47 err := cmd.Parse(jc, args)47 err := cmd.Parse(&cmd.DefaultEnv{}, jc, args)
48 return jc, err48 return jc, err
49}49}
5050
@@ -52,7 +52,7 @@
52 jc := cmd.NewSuperCommand("jujutest", "")52 jc := cmd.NewSuperCommand("jujutest", "")
53 tc := &TestCommand{Name: "defenestrate"}53 tc := &TestCommand{Name: "defenestrate"}
54 jc.Register(tc)54 jc.Register(tc)
55 err := cmd.Parse(jc, args)55 err := cmd.Parse(&cmd.DefaultEnv{}, jc, args)
56 return jc, tc, err56 return jc, tc, err
57}57}
5858
@@ -161,7 +161,7 @@
161 jc, _, err := parseDefenestrate(args)161 jc, _, err := parseDefenestrate(args)
162 c.Assert(err, IsNil)162 c.Assert(err, IsNil)
163163
164 err = jc.Run()164 err = jc.Run(&cmd.DefaultEnv{})
165 c.Assert(err, ErrorMatches, "BORKEN: value is cheese.")165 c.Assert(err, ErrorMatches, "BORKEN: value is cheese.")
166166
167 c.Assert(log.Debug, Equals, debug)167 c.Assert(log.Debug, Equals, debug)

Subscribers

People subscribed via source and target branches