mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
removed rcli completly
This commit is contained in:
parent
f37399d22b
commit
5a2a5ccdaf
6 changed files with 96 additions and 404 deletions
23
commands.go
23
commands.go
|
@ -890,24 +890,6 @@ func CmdAttach(args ...string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// Ports type - Used to parse multiple -p flags
|
|
||||||
type ports []int
|
|
||||||
|
|
||||||
func (p *ports) String() string {
|
|
||||||
return fmt.Sprint(*p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ports) Set(value string) error {
|
|
||||||
port, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Invalid port: %v", value)
|
|
||||||
}
|
|
||||||
*p = append(*p, port)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ListOpts type
|
// ListOpts type
|
||||||
type ListOpts []string
|
type ListOpts []string
|
||||||
|
|
||||||
|
@ -1053,11 +1035,6 @@ func CmdRun(args ...string) error {
|
||||||
v.Set("stderr", "1")
|
v.Set("stderr", "1")
|
||||||
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
attach := Go(func() error {
|
|
||||||
err := hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty)
|
|
||||||
return err
|
|
||||||
})*/
|
|
||||||
|
|
||||||
//start the container
|
//start the container
|
||||||
_, _, err = call("POST", "/containers/"+out.Id+"/start", nil)
|
_, _, err = call("POST", "/containers/"+out.Id+"/start", nil)
|
||||||
|
|
169
rcli/tcp.go
169
rcli/tcp.go
|
@ -1,169 +0,0 @@
|
||||||
package rcli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Note: the globals are here to avoid import cycle
|
|
||||||
// FIXME: Handle debug levels mode?
|
|
||||||
var DEBUG_FLAG bool = false
|
|
||||||
var CLIENT_SOCKET io.Writer = nil
|
|
||||||
|
|
||||||
type DockerTCPConn struct {
|
|
||||||
conn *net.TCPConn
|
|
||||||
options *DockerConnOptions
|
|
||||||
optionsBuf *[]byte
|
|
||||||
handshaked bool
|
|
||||||
client bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDockerTCPConn(conn *net.TCPConn, client bool) *DockerTCPConn {
|
|
||||||
return &DockerTCPConn{
|
|
||||||
conn: conn,
|
|
||||||
options: &DockerConnOptions{},
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) SetOptionRawTerminal() {
|
|
||||||
c.options.RawTerminal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) GetOptions() *DockerConnOptions {
|
|
||||||
if c.client && !c.handshaked {
|
|
||||||
// Attempt to parse options encoded as a JSON dict and store
|
|
||||||
// the reminder of what we read from the socket in a buffer.
|
|
||||||
//
|
|
||||||
// bufio (and its ReadBytes method) would have been nice here,
|
|
||||||
// but if json.Unmarshal() fails (which will happen if we speak
|
|
||||||
// to a version of docker that doesn't send any option), then
|
|
||||||
// we can't put the data back in it for the next Read().
|
|
||||||
c.handshaked = true
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
if n, _ := c.conn.Read(buf); n > 0 {
|
|
||||||
buf = buf[:n]
|
|
||||||
if nl := bytes.IndexByte(buf, '\n'); nl != -1 {
|
|
||||||
if err := json.Unmarshal(buf[:nl], c.options); err == nil {
|
|
||||||
buf = buf[nl+1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.optionsBuf = &buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.options
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) Read(b []byte) (int, error) {
|
|
||||||
if c.optionsBuf != nil {
|
|
||||||
// Consume what we buffered in GetOptions() first:
|
|
||||||
optionsBuf := *c.optionsBuf
|
|
||||||
optionsBuflen := len(optionsBuf)
|
|
||||||
copied := copy(b, optionsBuf)
|
|
||||||
if copied < optionsBuflen {
|
|
||||||
optionsBuf = optionsBuf[copied:]
|
|
||||||
c.optionsBuf = &optionsBuf
|
|
||||||
return copied, nil
|
|
||||||
}
|
|
||||||
c.optionsBuf = nil
|
|
||||||
return copied, nil
|
|
||||||
}
|
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) Write(b []byte) (int, error) {
|
|
||||||
optionsLen := 0
|
|
||||||
if !c.client && !c.handshaked {
|
|
||||||
c.handshaked = true
|
|
||||||
options, _ := json.Marshal(c.options)
|
|
||||||
options = append(options, '\n')
|
|
||||||
if optionsLen, err := c.conn.Write(options); err != nil {
|
|
||||||
return optionsLen, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n, err := c.conn.Write(b)
|
|
||||||
return n + optionsLen, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) Flush() error {
|
|
||||||
_, err := c.Write([]byte{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) Close() error { return c.conn.Close() }
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) CloseWrite() error { return c.conn.CloseWrite() }
|
|
||||||
|
|
||||||
func (c *DockerTCPConn) CloseRead() error { return c.conn.CloseRead() }
|
|
||||||
|
|
||||||
// Connect to a remote endpoint using protocol `proto` and address `addr`,
|
|
||||||
// issue a single call, and return the result.
|
|
||||||
// `proto` may be "tcp", "unix", etc. See the `net` package for available protocols.
|
|
||||||
func Call(proto, addr string, args ...string) (DockerConn, error) {
|
|
||||||
cmd, err := json.Marshal(args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn, err := dialDocker(proto, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := fmt.Fprintln(conn, string(cmd)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen on `addr`, using protocol `proto`, for incoming rcli calls,
|
|
||||||
// and pass them to `service`.
|
|
||||||
func ListenAndServe(proto, addr string, service Service) error {
|
|
||||||
listener, err := net.Listen(proto, addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("Listening for RCLI/%s on %s\n", proto, addr)
|
|
||||||
defer listener.Close()
|
|
||||||
for {
|
|
||||||
if conn, err := listener.Accept(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
conn, err := newDockerServerConn(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func(conn DockerConn) {
|
|
||||||
defer conn.Close()
|
|
||||||
if DEBUG_FLAG {
|
|
||||||
CLIENT_SOCKET = conn
|
|
||||||
}
|
|
||||||
if err := Serve(conn, service); err != nil {
|
|
||||||
log.Println("Error:", err.Error())
|
|
||||||
fmt.Fprintln(conn, "Error:", err.Error())
|
|
||||||
}
|
|
||||||
}(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse an rcli call on a new connection, and pass it to `service` if it
|
|
||||||
// is valid.
|
|
||||||
func Serve(conn DockerConn, service Service) error {
|
|
||||||
r := bufio.NewReader(conn)
|
|
||||||
var args []string
|
|
||||||
if line, err := r.ReadString('\n'); err != nil {
|
|
||||||
return err
|
|
||||||
} else if err := json.Unmarshal([]byte(line), &args); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
return call(service, ioutil.NopCloser(r), conn, args...)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
181
rcli/types.go
181
rcli/types.go
|
@ -1,181 +0,0 @@
|
||||||
package rcli
|
|
||||||
|
|
||||||
// rcli (Remote Command-Line Interface) is a simple protocol for...
|
|
||||||
// serving command-line interfaces remotely.
|
|
||||||
//
|
|
||||||
// rcli can be used over any transport capable of a) sending binary streams in
|
|
||||||
// both directions, and b) capable of half-closing a connection. TCP and Unix sockets
|
|
||||||
// are the usual suspects.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/dotcloud/docker/term"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DockerConnOptions struct {
|
|
||||||
RawTerminal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerConn interface {
|
|
||||||
io.ReadWriteCloser
|
|
||||||
CloseWrite() error
|
|
||||||
CloseRead() error
|
|
||||||
GetOptions() *DockerConnOptions
|
|
||||||
SetOptionRawTerminal()
|
|
||||||
Flush() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerLocalConn struct {
|
|
||||||
writer io.WriteCloser
|
|
||||||
savedState *term.State
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDockerLocalConn(w io.WriteCloser) *DockerLocalConn {
|
|
||||||
return &DockerLocalConn{
|
|
||||||
writer: w,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) Read(b []byte) (int, error) {
|
|
||||||
return 0, fmt.Errorf("DockerLocalConn does not implement Read()")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) Write(b []byte) (int, error) { return c.writer.Write(b) }
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) Close() error {
|
|
||||||
if c.savedState != nil {
|
|
||||||
RestoreTerminal(c.savedState)
|
|
||||||
c.savedState = nil
|
|
||||||
}
|
|
||||||
return c.writer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) Flush() error { return nil }
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) CloseWrite() error { return nil }
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) CloseRead() error { return nil }
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) GetOptions() *DockerConnOptions { return nil }
|
|
||||||
|
|
||||||
func (c *DockerLocalConn) SetOptionRawTerminal() {
|
|
||||||
if state, err := SetRawTerminal(); err != nil {
|
|
||||||
if os.Getenv("DEBUG") != "" {
|
|
||||||
log.Printf("Can't set the terminal in raw mode: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.savedState = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var UnknownDockerProto = fmt.Errorf("Only TCP is actually supported by Docker at the moment")
|
|
||||||
|
|
||||||
func dialDocker(proto string, addr string) (DockerConn, error) {
|
|
||||||
conn, err := net.Dial(proto, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch i := conn.(type) {
|
|
||||||
case *net.TCPConn:
|
|
||||||
return NewDockerTCPConn(i, true), nil
|
|
||||||
}
|
|
||||||
return nil, UnknownDockerProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDockerFromConn(conn net.Conn, client bool) (DockerConn, error) {
|
|
||||||
switch i := conn.(type) {
|
|
||||||
case *net.TCPConn:
|
|
||||||
return NewDockerTCPConn(i, client), nil
|
|
||||||
}
|
|
||||||
return nil, UnknownDockerProto
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDockerServerConn(conn net.Conn) (DockerConn, error) {
|
|
||||||
return newDockerFromConn(conn, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service interface {
|
|
||||||
Name() string
|
|
||||||
Help() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cmd func(io.ReadCloser, io.Writer, ...string) error
|
|
||||||
type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error
|
|
||||||
|
|
||||||
// FIXME: For reverse compatibility
|
|
||||||
func call(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error {
|
|
||||||
return LocalCall(service, stdin, stdout, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LocalCall(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
args = []string{"help"}
|
|
||||||
}
|
|
||||||
flags := flag.NewFlagSet("main", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stdout)
|
|
||||||
flags.Usage = func() { stdout.Write([]byte(service.Help())) }
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cmd := flags.Arg(0)
|
|
||||||
log.Printf("%s\n", strings.Join(append(append([]string{service.Name()}, cmd), flags.Args()[1:]...), " "))
|
|
||||||
if cmd == "" {
|
|
||||||
cmd = "help"
|
|
||||||
}
|
|
||||||
method := getMethod(service, cmd)
|
|
||||||
if method != nil {
|
|
||||||
return method(stdin, stdout, flags.Args()[1:]...)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("No such command: %s", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMethod(service Service, name string) Cmd {
|
|
||||||
if name == "help" {
|
|
||||||
return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
stdout.Write([]byte(service.Help()))
|
|
||||||
} else {
|
|
||||||
if method := getMethod(service, args[0]); method == nil {
|
|
||||||
return fmt.Errorf("No such command: %s", args[0])
|
|
||||||
} else {
|
|
||||||
method(stdin, stdout, "--help")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])
|
|
||||||
method, exists := reflect.TypeOf(service).MethodByName(methodName)
|
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
|
|
||||||
ret := method.Func.CallSlice([]reflect.Value{
|
|
||||||
reflect.ValueOf(service),
|
|
||||||
reflect.ValueOf(stdin),
|
|
||||||
reflect.ValueOf(stdout),
|
|
||||||
reflect.ValueOf(args),
|
|
||||||
})[0].Interface()
|
|
||||||
if ret == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ret.(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet {
|
|
||||||
flags := flag.NewFlagSet(name, flag.ContinueOnError)
|
|
||||||
flags.SetOutput(output)
|
|
||||||
flags.Usage = func() {
|
|
||||||
fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description)
|
|
||||||
flags.PrintDefaults()
|
|
||||||
}
|
|
||||||
return flags
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package rcli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/dotcloud/docker/term"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
//FIXME: move these function to utils.go (in rcli to avoid import loop)
|
|
||||||
func SetRawTerminal() (*term.State, error) {
|
|
||||||
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, os.Interrupt)
|
|
||||||
go func() {
|
|
||||||
_ = <-c
|
|
||||||
term.Restore(int(os.Stdin.Fd()), oldState)
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
|
||||||
return oldState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func RestoreTerminal(state *term.State) {
|
|
||||||
term.Restore(int(os.Stdin.Fd()), state)
|
|
||||||
}
|
|
96
server_test.go
Normal file
96
server_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRm(t *testing.T) {
|
||||||
|
runtime, err := newTestRuntime()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer nuke(runtime)
|
||||||
|
|
||||||
|
srv := &Server{runtime: runtime}
|
||||||
|
|
||||||
|
config, _, err := ParseRun([]string{GetTestImage(runtime).Id, "echo test"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _, _, err := srv.ContainerCreate(*config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runtime.List()) != 1 {
|
||||||
|
t.Errorf("Expected 1 container, %v found", len(runtime.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = srv.ContainerDestroy(id, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runtime.List()) != 0 {
|
||||||
|
t.Errorf("Expected 0 container, %v found", len(runtime.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStartRestartStopStartKillRm(t *testing.T) {
|
||||||
|
runtime, err := newTestRuntime()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer nuke(runtime)
|
||||||
|
|
||||||
|
srv := &Server{runtime: runtime}
|
||||||
|
|
||||||
|
config, _, err := ParseRun([]string{GetTestImage(runtime).Id, "/bin/cat"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _, _, err := srv.ContainerCreate(*config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runtime.List()) != 1 {
|
||||||
|
t.Errorf("Expected 1 container, %v found", len(runtime.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.ContainerStart(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.ContainerRestart(id, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.ContainerStop(id, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.ContainerStart(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.ContainerKill(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = srv.ContainerDestroy(id, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runtime.List()) != 0 {
|
||||||
|
t.Errorf("Expected 0 container, %v found", len(runtime.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
4
utils.go
4
utils.go
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dotcloud/docker/rcli"
|
|
||||||
"github.com/dotcloud/docker/term"
|
"github.com/dotcloud/docker/term"
|
||||||
"index/suffixarray"
|
"index/suffixarray"
|
||||||
"io"
|
"io"
|
||||||
|
@ -58,9 +57,6 @@ func Debugf(format string, a ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...)
|
fmt.Fprintf(os.Stderr, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...)
|
||||||
if rcli.CLIENT_SOCKET != nil {
|
|
||||||
fmt.Fprintf(rcli.CLIENT_SOCKET, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue