Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
27800: cli: sanitize the host/port command-line flags r=knz a=knz

Fixes  cockroachdb#27702.

This patch achieves the following:

- deprecates --host/--port in favor of --listen-addr/--listen-port for
  `cockroach start`
- produces a warning if neither --host/--listen nor --advertise are
  specified.
- deprecates --advertise-host in favor of --advertise-addr
- deprecates --http-host in favor of --http-addr
- suggests that "addresses" are also acceptable next to host names in
  the flag descriptions.
- suggests that the argument to --advertise must be routable
  everywhere.
- un-hides --advertise-port. This flag is actually useful when
  there is a NAT router with X:Y port forwarding.

Release note (cli change): the flags `--host` and `--port` to
`cockroach start` are deprecated in favor of `--listen-addr` and
`--listen-port`. They remain valid for other *client* commands.  The
flags `--advertise-host` and `--http-host` are deprecated in favor of
`--advertise-addr` and `--http-addr` and do not change meaning.

cc @jseldess 

28062: sql: do not use `rowsort` for queries that use ORDER BY r=knz a=knz

If a query uses ORDER BY, the test's expected output should be
stable, and the test should exercise that it is indeed stable and the
one expected given the ORDER BY constraint.

By improperly adding `rowsort` to a test that otherwises uses ORDER
BY, the value of the test is reduced as the quality of ORDER BY is not
tested any more.

This patch corrects this by removing `rowsort` from tests that also
use ORDER BY.

(There are a few tests that control the behavior of `rowsort` itself,
and these are left unchanged here.)

Release note: None

Co-authored-by: Raphael 'kena' Poss <[email protected]>
  • Loading branch information
craig[bot] and knz committed Aug 7, 2018
3 parents bb66aae + 3ec97c0 + 0c45f62 commit ab1cb1f
Show file tree
Hide file tree
Showing 20 changed files with 271 additions and 108 deletions.
2 changes: 1 addition & 1 deletion pkg/acceptance/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function finish() {
trap finish EXIT
HOST=$(hostname -f)
$bin start --logtostderr=INFO --background --insecure --host="${HOST}" --port=12345 &> out
$bin start --logtostderr=INFO --background --insecure --listen-addr="${HOST}" --listen-port=12345 &> out
$bin sql --insecure --host="${HOST}" --port=12345 -e "show databases"
$bin quit --insecure --host="${HOST}" --port=12345
`
Expand Down
2 changes: 1 addition & 1 deletion pkg/acceptance/cluster/dockercluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ func (l *DockerCluster) startNode(ctx context.Context, node *testNode) {
cmd := []string{
"start",
"--certs-dir=/certs/",
"--host=" + node.nodeStr,
"--listen-addr=" + node.nodeStr,
"--verbosity=1",
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/acceptance/localcluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ func (c *Cluster) makeNode(ctx context.Context, nodeIdx int, cfg NodeConfig) (*N
cfg.Binary,
"start",
"--insecure",
// Although --host/--port are deprecated, we cannot yet replace
// this here by --listen-addr/--listen-port, because
// TestVersionUpgrade will also try old binaries.
fmt.Sprintf("--host=%s", n.IPAddr()),
fmt.Sprintf("--port=%d", cfg.RPCPort),
fmt.Sprintf("--http-port=%d", cfg.HTTPPort),
Expand Down
63 changes: 47 additions & 16 deletions pkg/cli/cliflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,40 +324,71 @@ or both forms can be used together, for example:
</PRE>`,
}

ListenAddr = FlagInfo{
Name: "listen-addr",
Description: `
The address or hostname to listen on. An IPv6 address can also be
specified with the notation [...], for example [::1] or
[fe80::f6f2:::].
If --advertise-addr is left unspecified, the node will also announce
this address for use by other nodes. It is strongly recommended to use
--advertise-addr in cloud and container deployments or any setup where
NAT is present between cluster nodes.`,
}

ServerHost = FlagInfo{
Name: "host",
Name: "host",
Description: `Alias for --listen-addr. Deprecated.`,
}

ListenPort = FlagInfo{
Name: "listen-port",
Shorthand: "p",
Description: `
The hostname to listen on. The node will also advertise itself using this
hostname if advertise-host is not specified.`,
The port to bind to.
If --advertise-port is left unspecified, the node will also announce
this port for use by other nodes. If a router implements NAT or N:M
port forwarding, be sure to also use --advertise-port.`,
}

ServerPort = FlagInfo{
Name: "port",
Shorthand: "p",
Name: "port",
Description: `Alias for --listen-port. Deprecated.`,
}

AdvertiseAddr = FlagInfo{
Name: "advertise-addr",
Description: `
The port to bind to.`,
The address or hostname to advertise to other CockroachDB nodes for intra-cluster
communication; it must resolve and be routable from other nodes in the cluster.
An IPv6 address can also be specified with the notation [...], for
example [::1] or [fe80::f6f2:::].`,
}

AdvertiseHost = FlagInfo{
Name: "advertise-host",
Description: `
The hostname to advertise to other CockroachDB nodes for intra-cluster
communication; it must resolve from other nodes in the cluster.`,
Name: "advertise-host",
Description: `Alias for --advertise-addr. Deprecated.`,
}

AdvertisePort = FlagInfo{
Name: "advertise-port",
Description: `
The port to advertise to other CockroachDB nodes for intra-cluster
communication.`,
communication. This should not be set differently from --listen-port
unless port forwarding is set up on an intermediate firewall/router.`,
}

ServerHTTPHost = FlagInfo{
Name: "http-host",
ListenHTTPAddr = FlagInfo{
Name: "http-addr",
Description: `The hostname or IP address to bind to for HTTP requests.`,
}

ServerHTTPPort = FlagInfo{
ListenHTTPAddrAlias = FlagInfo{
Name: "http-host",
Description: `Alias for --http-addr. Deprecated.`,
}

ListenHTTPPort = FlagInfo{
Name: "http-port",
Description: `The port to bind to for HTTP requests.`,
}
Expand Down Expand Up @@ -397,10 +428,10 @@ production usage.`,
Name: "insecure",
Description: `
Start an insecure node, using unencrypted (non-TLS) connections,
listening on all IP addresses (unless --host is provided) and
listening on all IP addresses (unless --listen-addr is provided) and
disabling password authentication for all database users. This is
strongly discouraged for production usage and should never be used on
a public network without combining it with --host.`,
a public network without combining it with --listen-addr.`,
}

// KeySize, CertificateLifetime, AllowKeyReuse, and OverwriteFiles are used for
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func initCLIDefaults() {
serverCfg.PIDFile = ""
startCtx.serverInsecure = baseCfg.Insecure
startCtx.serverSSLCertsDir = base.DefaultCertsDirectory
startCtx.serverConnHost = ""
startCtx.serverListenAddr = ""
startCtx.tempDir = ""
startCtx.externalIODir = ""

Expand Down Expand Up @@ -218,7 +218,7 @@ var startCtx struct {
// server-specific values of some flags.
serverInsecure bool
serverSSLCertsDir string
serverConnHost string
serverListenAddr string

// temporary directory to use to spill computation results to disk.
tempDir string
Expand Down
78 changes: 55 additions & 23 deletions pkg/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ import (
// - the underlying context parameters must receive defaults in
// initCLIDefaults() even when they are otherwise overridden by the
// flags logic, because some tests to not use the flag logic at all.
var serverConnPort, serverAdvertiseHost, serverAdvertisePort string
var serverHTTPHost, serverHTTPPort string
var serverListenPort, serverAdvertiseAddr, serverAdvertisePort string
var serverHTTPAddr, serverHTTPPort string
var clientConnHost, clientConnPort string

// initPreFlagsDefaults initializes the values of the global variables
// defined above.
func initPreFlagsDefaults() {
serverConnPort = base.DefaultPort
serverAdvertiseHost = ""
serverListenPort = base.DefaultPort
serverAdvertiseAddr = ""
serverAdvertisePort = ""

serverHTTPHost = ""
serverHTTPAddr = ""
serverHTTPPort = base.DefaultHTTPPort

clientConnHost = ""
Expand Down Expand Up @@ -131,6 +131,26 @@ func VarFlag(f *pflag.FlagSet, value pflag.Value, flagInfo cliflags.FlagInfo) {
setFlagFromEnv(f, flagInfo)
}

// aliasStrVar wraps a string configuration option and is meant
// to be used in addition to / next to another flag that targets the
// same option. It does not implement "default values" so that the
// main flag can perform the default logic.
type aliasStrVar struct{ p *string }

// String implements the pflag.Value interface.
func (a aliasStrVar) String() string { return "" }

// Set implements the pflag.Value interface.
func (a aliasStrVar) Set(v string) error {
if v != "" {
*a.p = v
}
return nil
}

// Type implements the pflag.Value interface.
func (a aliasStrVar) Type() string { return "string" }

func init() {
initCLIDefaults()

Expand Down Expand Up @@ -191,14 +211,26 @@ func init() {
f := StartCmd.Flags()

// Server flags.
StringFlag(f, &startCtx.serverConnHost, cliflags.ServerHost, startCtx.serverConnHost)
StringFlag(f, &serverConnPort, cliflags.ServerPort, serverConnPort)
StringFlag(f, &serverAdvertiseHost, cliflags.AdvertiseHost, serverAdvertiseHost)
StringFlag(f, &startCtx.serverListenAddr, cliflags.ListenAddr, startCtx.serverListenAddr)
VarFlag(f, aliasStrVar{&startCtx.serverListenAddr}, cliflags.ServerHost)
_ = f.MarkDeprecated(cliflags.ServerHost.Name, "use --listen-addr/--advertise-addr instead.")

StringFlag(f, &serverListenPort, cliflags.ListenPort, serverListenPort)
VarFlag(f, aliasStrVar{&serverListenPort}, cliflags.ServerPort)
_ = f.MarkDeprecated(cliflags.ServerPort.Name, "use --listen-port/--advertise-port instead.")

StringFlag(f, &serverAdvertiseAddr, cliflags.AdvertiseAddr, serverAdvertiseAddr)
VarFlag(f, aliasStrVar{&serverAdvertiseAddr}, cliflags.AdvertiseHost)
_ = f.MarkDeprecated(cliflags.AdvertiseHost.Name, "use --advertise-addr instead.")

StringFlag(f, &serverAdvertisePort, cliflags.AdvertisePort, serverAdvertisePort)
// The advertise port flag is used for testing purposes only and is kept hidden.
_ = f.MarkHidden(cliflags.AdvertisePort.Name)
StringFlag(f, &serverHTTPHost, cliflags.ServerHTTPHost, serverHTTPHost)
StringFlag(f, &serverHTTPPort, cliflags.ServerHTTPPort, serverHTTPPort)

StringFlag(f, &serverHTTPAddr, cliflags.ListenHTTPAddr, serverHTTPAddr)
VarFlag(f, aliasStrVar{&serverHTTPAddr}, cliflags.ListenHTTPAddrAlias)
_ = f.MarkDeprecated(cliflags.ListenHTTPAddrAlias.Name, "use --http-addr instead.")

StringFlag(f, &serverHTTPPort, cliflags.ListenHTTPPort, serverHTTPPort)

StringFlag(f, &serverCfg.Attrs, cliflags.Attrs, serverCfg.Attrs)
VarFlag(f, &serverCfg.Locality, cliflags.Locality)

Expand Down Expand Up @@ -402,27 +434,27 @@ func init() {
}

func extraServerFlagInit() {
serverCfg.Addr = net.JoinHostPort(startCtx.serverConnHost, serverConnPort)
if serverAdvertiseHost == "" {
serverAdvertiseHost = startCtx.serverConnHost
serverCfg.Addr = net.JoinHostPort(startCtx.serverListenAddr, serverListenPort)
if serverAdvertiseAddr == "" {
serverAdvertiseAddr = startCtx.serverListenAddr
}
if serverAdvertisePort == "" {
serverAdvertisePort = serverConnPort
serverAdvertisePort = serverListenPort
}
serverCfg.AdvertiseAddr = net.JoinHostPort(serverAdvertiseHost, serverAdvertisePort)
if serverHTTPHost == "" {
serverHTTPHost = startCtx.serverConnHost
serverCfg.AdvertiseAddr = net.JoinHostPort(serverAdvertiseAddr, serverAdvertisePort)
if serverHTTPAddr == "" {
serverHTTPAddr = startCtx.serverListenAddr
}
serverCfg.HTTPAddr = net.JoinHostPort(serverHTTPHost, serverHTTPPort)
serverCfg.HTTPAddr = net.JoinHostPort(serverHTTPAddr, serverHTTPPort)
}

func extraClientFlagInit() {
serverCfg.Addr = net.JoinHostPort(clientConnHost, clientConnPort)
serverCfg.AdvertiseAddr = serverCfg.Addr
if serverHTTPHost == "" {
serverHTTPHost = startCtx.serverConnHost
if serverHTTPAddr == "" {
serverHTTPAddr = startCtx.serverListenAddr
}
serverCfg.HTTPAddr = net.JoinHostPort(serverHTTPHost, serverHTTPPort)
serverCfg.HTTPAddr = net.JoinHostPort(serverHTTPAddr, serverHTTPPort)
}

func setDefaultStderrVerbosity(cmd *cobra.Command, defaultSeverity log.Severity) error {
Expand Down
48 changes: 26 additions & 22 deletions pkg/cli/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,29 @@ func TestServerConnSettings(t *testing.T) {
expectedAdvertiseAddr string
}{
{[]string{"start"}, ":" + base.DefaultPort, ":" + base.DefaultPort},
{[]string{"start", "--host", "127.0.0.1"}, "127.0.0.1:" + base.DefaultPort, "127.0.0.1:" + base.DefaultPort},
{[]string{"start", "--listen-addr", "127.0.0.1"}, "127.0.0.1:" + base.DefaultPort, "127.0.0.1:" + base.DefaultPort},
{[]string{"start", "--listen-addr", "192.168.0.111"}, "192.168.0.111:" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--listen-port", "12345"}, ":12345", ":12345"},
{[]string{"start", "--listen-addr", "127.0.0.1", "--listen-port", "12345"}, "127.0.0.1:12345", "127.0.0.1:12345"},
{[]string{"start", "--advertise-addr", "192.168.0.111"}, ":" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--advertise-addr", "192.168.0.111", "--advertise-port", "12345"}, ":" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--listen-addr", "127.0.0.1", "--advertise-addr", "192.168.0.111"}, "127.0.0.1:" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--listen-addr", "127.0.0.1", "--advertise-addr", "192.168.0.111", "--listen-port", "12345"}, "127.0.0.1:12345", "192.168.0.111:12345"},
{[]string{"start", "--listen-addr", "127.0.0.1", "--advertise-addr", "192.168.0.111", "--advertise-port", "12345"}, "127.0.0.1:" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--listen-addr", "127.0.0.1", "--advertise-addr", "192.168.0.111", "--listen-port", "54321", "--advertise-port", "12345"}, "127.0.0.1:54321", "192.168.0.111:12345"},
{[]string{"start", "--advertise-addr", "192.168.0.111", "--listen-port", "12345"}, ":12345", "192.168.0.111:12345"},
{[]string{"start", "--advertise-addr", "192.168.0.111", "--advertise-port", "12345"}, ":" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--advertise-addr", "192.168.0.111", "--listen-port", "54321", "--advertise-port", "12345"}, ":54321", "192.168.0.111:12345"},
// confirm hostnames will work
{[]string{"start", "--listen-addr", "my.host.name"}, "my.host.name:" + base.DefaultPort, "my.host.name:" + base.DefaultPort},
{[]string{"start", "--listen-addr", "myhostname"}, "myhostname:" + base.DefaultPort, "myhostname:" + base.DefaultPort},
// confirm IPv6 works too
{[]string{"start", "--listen-addr", "::1"}, "[::1]:" + base.DefaultPort, "[::1]:" + base.DefaultPort},
{[]string{"start", "--listen-addr", "2622:6221:e663:4922:fc2b:788b:fadd:7b48"}, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultPort, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultPort},
// Backward-compatibility flags
{[]string{"start", "--host", "192.168.0.111"}, "192.168.0.111:" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--port", "12345"}, ":12345", ":12345"},
{[]string{"start", "--host", "127.0.0.1", "--port", "12345"}, "127.0.0.1:12345", "127.0.0.1:12345"},
{[]string{"start", "--advertise-host", "192.168.0.111"}, ":" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--advertise-host", "192.168.0.111", "--advertise-port", "12345"}, ":" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--host", "127.0.0.1", "--advertise-host", "192.168.0.111"}, "127.0.0.1:" + base.DefaultPort, "192.168.0.111:" + base.DefaultPort},
{[]string{"start", "--host", "127.0.0.1", "--advertise-host", "192.168.0.111", "--port", "12345"}, "127.0.0.1:12345", "192.168.0.111:12345"},
{[]string{"start", "--host", "127.0.0.1", "--advertise-host", "192.168.0.111", "--advertise-port", "12345"}, "127.0.0.1:" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--host", "127.0.0.1", "--advertise-host", "192.168.0.111", "--port", "54321", "--advertise-port", "12345"}, "127.0.0.1:54321", "192.168.0.111:12345"},
{[]string{"start", "--advertise-host", "192.168.0.111", "--port", "12345"}, ":12345", "192.168.0.111:12345"},
{[]string{"start", "--advertise-host", "192.168.0.111", "--advertise-port", "12345"}, ":" + base.DefaultPort, "192.168.0.111:12345"},
{[]string{"start", "--advertise-host", "192.168.0.111", "--port", "54321", "--advertise-port", "12345"}, ":54321", "192.168.0.111:12345"},
// confirm hostnames will work
{[]string{"start", "--host", "my.host.name"}, "my.host.name:" + base.DefaultPort, "my.host.name:" + base.DefaultPort},
{[]string{"start", "--host", "myhostname"}, "myhostname:" + base.DefaultPort, "myhostname:" + base.DefaultPort},
// confirm IPv6 works too
{[]string{"start", "--host", "::1"}, "[::1]:" + base.DefaultPort, "[::1]:" + base.DefaultPort},
{[]string{"start", "--host", "2622:6221:e663:4922:fc2b:788b:fadd:7b48"}, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultPort, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultPort},
}

for i, td := range testData {
Expand Down Expand Up @@ -256,14 +260,14 @@ func TestHttpHostFlagValue(t *testing.T) {
args []string
expected string
}{
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "127.0.0.1"}, "127.0.0.1:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "192.168.0.111"}, "192.168.0.111:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "127.0.0.1"}, "127.0.0.1:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "192.168.0.111"}, "192.168.0.111:" + base.DefaultHTTPPort},
// confirm hostnames will work
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "my.host.name"}, "my.host.name:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "myhostname"}, "myhostname:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "my.host.name"}, "my.host.name:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "myhostname"}, "myhostname:" + base.DefaultHTTPPort},
// confirm IPv6 works too
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "::1"}, "[::1]:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ServerHTTPHost.Name, "2622:6221:e663:4922:fc2b:788b:fadd:7b48"}, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "::1"}, "[::1]:" + base.DefaultHTTPPort},
{[]string{"start", "--" + cliflags.ListenHTTPAddr.Name, "2622:6221:e663:4922:fc2b:788b:fadd:7b48"}, "[2622:6221:e663:4922:fc2b:788b:fadd:7b48]:" + base.DefaultHTTPPort},
}

for i, td := range testData {
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/haproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type haProxyNodeInfo struct {

func nodeStatusesToNodeInfos(statuses []status.NodeStatus) []haProxyNodeInfo {
fs := flag.NewFlagSet("haproxy", flag.ContinueOnError)
checkPort := fs.String(cliflags.ServerHTTPPort.Name, base.DefaultHTTPPort, "" /* usage */)
checkPort := fs.String(cliflags.ListenHTTPPort.Name, base.DefaultHTTPPort, "" /* usage */)

// Discard parsing output.
fs.SetOutput(ioutil.Discard)
Expand All @@ -74,7 +74,7 @@ func nodeStatusesToNodeInfos(statuses []status.NodeStatus) []haProxyNodeInfo {
// when it encounters an undefined flag and we do not want to define all
// possible flags.
for i, arg := range status.Args {
if strings.Contains(arg, cliflags.ServerHTTPPort.Name) {
if strings.Contains(arg, cliflags.ListenHTTPPort.Name) {
_ = fs.Parse(status.Args[i:])
}
}
Expand Down
26 changes: 25 additions & 1 deletion pkg/cli/interactive_tests/test_flags.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ interrupt
eexpect ":/# "
end_test

start_test "Check that --host causes a deprecation warning."
send "$argv start --insecure --host=localhost\r"
eexpect "host has been deprecated, use --listen-addr/--advertise-addr instead."
eexpect "node starting"
interrupt
eexpect ":/# "
end_test

start_test "Check that --port causes a deprecation warning."
send "$argv start --insecure --port=26257\r"
eexpect "port has been deprecated, use --listen-port/--advertise-port instead."
eexpect "node starting"
interrupt
eexpect ":/# "
end_test

start_test "Check that not using --host nor --advertise causes a user warning."
send "$argv start --insecure\r"
eexpect "WARNING: neither --listen-addr nor --advertise-addr was specified"
eexpect "node starting"
interrupt
eexpect ":/# "
end_test


start_test {Check that the "failed running SUBCOMMAND" message does not consider a flag the subcommand}
send "$argv --verbosity 2 start --garbage\r"
eexpect {Failed running "start"}
Expand All @@ -55,4 +80,3 @@ end_test

send "exit 0\r"
eexpect eof

Loading

0 comments on commit ab1cb1f

Please sign in to comment.