2018-03-30 16:15:06 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
2018-04-04 19:41:19 +00:00
|
|
|
"bytes"
|
|
|
|
"encoding/hex"
|
2018-04-10 09:45:31 +00:00
|
|
|
"errors"
|
2018-03-30 16:15:06 +00:00
|
|
|
"fmt"
|
2018-04-04 19:41:19 +00:00
|
|
|
"io/ioutil"
|
2018-03-30 16:15:06 +00:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-03-03 14:21:42 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
2019-09-10 16:11:48 +00:00
|
|
|
"gopkg.in/abiosoft/ishell.v2"
|
2018-03-30 16:15:06 +00:00
|
|
|
)
|
|
|
|
|
2019-09-10 16:41:11 +00:00
|
|
|
const (
|
|
|
|
vmKey = "vm"
|
|
|
|
boolType = "bool"
|
|
|
|
boolFalse = "false"
|
|
|
|
boolTrue = "true"
|
|
|
|
intType = "int"
|
|
|
|
stringType = "string"
|
|
|
|
)
|
2019-09-10 16:11:48 +00:00
|
|
|
|
|
|
|
var commands = []*ishell.Cmd{
|
|
|
|
{
|
|
|
|
Name: "exit",
|
|
|
|
Help: "Exit the VM prompt",
|
|
|
|
LongHelp: "Exit the VM prompt",
|
|
|
|
Func: handleExit,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "ip",
|
|
|
|
Help: "Show current instruction",
|
|
|
|
LongHelp: "Show current instruction",
|
|
|
|
Func: handleIP,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "break",
|
|
|
|
Help: "Place a breakpoint",
|
|
|
|
LongHelp: `Usage: break <ip>
|
|
|
|
<ip> is mandatory parameter, example:
|
|
|
|
> break 12`,
|
|
|
|
Func: handleBreak,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "estack",
|
|
|
|
Help: "Show evaluation stack contents",
|
|
|
|
LongHelp: "Show evaluation stack contents",
|
|
|
|
Func: handleXStack,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "astack",
|
|
|
|
Help: "Show alt stack contents",
|
|
|
|
LongHelp: "Show alt stack contents",
|
|
|
|
Func: handleXStack,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "istack",
|
|
|
|
Help: "Show invocation stack contents",
|
|
|
|
LongHelp: "Show invocation stack contents",
|
|
|
|
Func: handleXStack,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "loadavm",
|
|
|
|
Help: "Load an avm script into the VM",
|
|
|
|
LongHelp: `Usage: loadavm <file>
|
|
|
|
<file> is mandatory parameter, example:
|
|
|
|
> load /path/to/script.avm`,
|
|
|
|
Func: handleLoadAVM,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "loadhex",
|
|
|
|
Help: "Load a hex-encoded script string into the VM",
|
|
|
|
LongHelp: `Usage: loadhex <string>
|
|
|
|
<string> is mandatory parameter, example:
|
|
|
|
> load 006166`,
|
|
|
|
Func: handleLoadHex,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "loadgo",
|
|
|
|
Help: "Compile and load a Go file into the VM",
|
|
|
|
LongHelp: `Usage: loadhex <file>
|
|
|
|
<file> is mandatory parameter, example:
|
|
|
|
> load /path/to/file.go`,
|
|
|
|
Func: handleLoadGo,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "run",
|
|
|
|
Help: "Execute the current loaded script",
|
2019-09-10 16:22:05 +00:00
|
|
|
LongHelp: `Usage: run [<operation> [<parameter>...]]
|
2019-09-10 16:11:48 +00:00
|
|
|
|
|
|
|
<operation> is an operation name, passed as a first parameter to Main() (and it
|
|
|
|
can't be 'help' at the moment)
|
2019-09-10 16:41:11 +00:00
|
|
|
<parameter> is a parameter (can be repeated multiple times) that can be specified
|
2019-09-10 16:30:32 +00:00
|
|
|
as <type>:<value>, where type can be:
|
2019-09-10 16:41:11 +00:00
|
|
|
'` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values
|
|
|
|
'` + intType + `': supports integers as values
|
|
|
|
'` + stringType + `': supports strings as values (that are pushed as a byte array
|
|
|
|
values to the stack)
|
|
|
|
or can be just <value>, for which the type will be detected automatically
|
|
|
|
following these rules: '` + boolTrue + `' and '` + boolFalse + `' are treated as respective
|
|
|
|
boolean values, everything that can be converted to integer is treated as
|
|
|
|
integer and everything else is treated like a string.
|
2019-09-10 16:30:32 +00:00
|
|
|
|
|
|
|
Passing parameters without operation is not supported. Parameters are packed
|
|
|
|
into array before they're passed to the script, so effectively 'run' only
|
|
|
|
supports contracts with signatures like this:
|
2019-09-10 16:11:48 +00:00
|
|
|
func Main(operation string, args []interface{}) interface{}
|
|
|
|
|
|
|
|
Example:
|
2019-09-10 16:41:11 +00:00
|
|
|
> run put ` + stringType + `:"Something to put"`,
|
2019-09-10 16:11:48 +00:00
|
|
|
Func: handleRun,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "cont",
|
|
|
|
Help: "Continue execution of the current loaded script",
|
|
|
|
LongHelp: "Continue execution of the current loaded script",
|
|
|
|
Func: handleCont,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "step",
|
|
|
|
Help: "Step (n) instruction in the program",
|
|
|
|
LongHelp: `Usage: step [<n>]
|
|
|
|
<n> is optional parameter to specify number of instructions to run, example:
|
|
|
|
> step 10`,
|
|
|
|
Func: handleStep,
|
|
|
|
},
|
2019-10-14 15:37:11 +00:00
|
|
|
{
|
|
|
|
Name: "stepinto",
|
|
|
|
Help: "Stepinto instruction to take in the debugger",
|
|
|
|
LongHelp: `Usage: stepInto
|
|
|
|
example:
|
|
|
|
> stepinto`,
|
|
|
|
Func: handleStepInto,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "stepout",
|
|
|
|
Help: "Stepout instruction to take in the debugger",
|
|
|
|
LongHelp: `Usage: stepOut
|
|
|
|
example:
|
|
|
|
> stepout`,
|
|
|
|
Func: handleStepOut,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "stepover",
|
|
|
|
Help: "Stepover instruction to take in the debugger",
|
|
|
|
LongHelp: `Usage: stepOver
|
|
|
|
example:
|
|
|
|
> stepover`,
|
|
|
|
Func: handleStepOver,
|
|
|
|
},
|
2019-09-10 16:11:48 +00:00
|
|
|
{
|
|
|
|
Name: "ops",
|
|
|
|
Help: "Dump opcodes of the current loaded program",
|
|
|
|
LongHelp: "Dump opcodes of the current loaded program",
|
|
|
|
Func: handleOps,
|
|
|
|
},
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
|
|
|
|
2019-10-14 15:38:05 +00:00
|
|
|
// VMCLI object for interacting with the VM.
|
2018-03-30 16:15:06 +00:00
|
|
|
type VMCLI struct {
|
2019-09-10 16:11:48 +00:00
|
|
|
vm *vm.VM
|
|
|
|
shell *ishell.Shell
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a new VMCLI object.
|
|
|
|
func New() *VMCLI {
|
2019-09-10 16:11:48 +00:00
|
|
|
vmcli := VMCLI{
|
2019-10-22 10:44:14 +00:00
|
|
|
vm: vm.New(),
|
2019-09-10 16:11:48 +00:00
|
|
|
shell: ishell.New(),
|
|
|
|
}
|
|
|
|
vmcli.shell.Set(vmKey, vmcli.vm)
|
|
|
|
for _, c := range commands {
|
|
|
|
vmcli.shell.AddCmd(c)
|
|
|
|
}
|
|
|
|
changePrompt(vmcli.shell, vmcli.vm)
|
|
|
|
return &vmcli
|
|
|
|
}
|
|
|
|
|
|
|
|
func getVMFromContext(c *ishell.Context) *vm.VM {
|
|
|
|
return c.Get(vmKey).(*vm.VM)
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkVMIsReady(c *ishell.Context) bool {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
if v == nil || !v.Ready() {
|
|
|
|
c.Err(errors.New("VM is not ready: no program loaded"))
|
|
|
|
return false
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
2019-09-10 16:11:48 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleExit(c *ishell.Context) {
|
|
|
|
c.Println("Bye!")
|
|
|
|
os.Exit(0)
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleIP(c *ishell.Context) {
|
|
|
|
if !checkVMIsReady(c) {
|
2018-03-30 16:15:06 +00:00
|
|
|
return
|
|
|
|
}
|
2019-09-10 16:11:48 +00:00
|
|
|
v := getVMFromContext(c)
|
|
|
|
ip, opcode := v.Context().CurrInstr()
|
|
|
|
c.Printf("instruction pointer at %d (%s)\n", ip, opcode)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleBreak(c *ishell.Context) {
|
|
|
|
if !checkVMIsReady(c) {
|
2018-03-30 16:15:06 +00:00
|
|
|
return
|
|
|
|
}
|
2019-09-10 16:11:48 +00:00
|
|
|
v := getVMFromContext(c)
|
|
|
|
if len(c.Args) != 1 {
|
2019-10-16 12:11:17 +00:00
|
|
|
c.Err(errors.New("missing parameter <ip>"))
|
2019-09-10 16:11:48 +00:00
|
|
|
}
|
|
|
|
n, err := strconv.Atoi(c.Args[0])
|
|
|
|
if err != nil {
|
|
|
|
c.Err(fmt.Errorf("argument conversion error: %s", err))
|
2018-03-30 16:15:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
v.AddBreakPoint(n)
|
|
|
|
c.Printf("breakpoint added at instruction %d\n", n)
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleXStack(c *ishell.Context) {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
c.Println(v.Stack(c.Cmd.Name))
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleLoadAVM(c *ishell.Context) {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
if err := v.LoadFile(c.Args[0]); err != nil {
|
|
|
|
c.Err(err)
|
|
|
|
} else {
|
|
|
|
c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
|
|
}
|
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleLoadHex(c *ishell.Context) {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
b, err := hex.DecodeString(c.Args[0])
|
|
|
|
if err != nil {
|
|
|
|
c.Err(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v.Load(b)
|
|
|
|
c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleLoadGo(c *ishell.Context) {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
fb, err := ioutil.ReadFile(c.Args[0])
|
|
|
|
if err != nil {
|
|
|
|
c.Err(err)
|
|
|
|
return
|
|
|
|
}
|
2019-10-29 10:02:54 +00:00
|
|
|
b, err := compiler.Compile(bytes.NewReader(fb))
|
2019-09-10 16:11:48 +00:00
|
|
|
if err != nil {
|
|
|
|
c.Err(err)
|
|
|
|
return
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
v.Load(b)
|
|
|
|
c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr())
|
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleRun(c *ishell.Context) {
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
if len(c.Args) != 0 {
|
|
|
|
var (
|
|
|
|
method []byte
|
|
|
|
params []vm.StackItem
|
|
|
|
err error
|
|
|
|
)
|
2019-09-10 16:22:05 +00:00
|
|
|
method = []byte(c.Args[0])
|
|
|
|
params, err = parseArgs(c.Args[1:])
|
2018-04-04 19:41:19 +00:00
|
|
|
if err != nil {
|
2019-09-10 16:11:48 +00:00
|
|
|
c.Err(err)
|
2018-04-04 19:41:19 +00:00
|
|
|
return
|
|
|
|
}
|
2019-09-10 16:11:48 +00:00
|
|
|
v.LoadArgs(method, params)
|
|
|
|
}
|
2019-10-22 10:44:14 +00:00
|
|
|
runVMWithHandling(c, v)
|
2019-09-10 16:11:48 +00:00
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
2018-04-04 19:41:19 +00:00
|
|
|
|
2019-10-22 10:44:14 +00:00
|
|
|
// runVMWithHandling runs VM with handling errors and additional state messages.
|
|
|
|
func runVMWithHandling(c *ishell.Context, v *vm.VM) {
|
|
|
|
err := v.Run()
|
|
|
|
if err != nil {
|
|
|
|
c.Err(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var message string
|
|
|
|
switch {
|
|
|
|
case v.HasFailed():
|
|
|
|
message = "FAILED"
|
|
|
|
case v.HasHalted():
|
|
|
|
message = v.Stack("estack")
|
|
|
|
case v.AtBreakpoint():
|
|
|
|
ctx := v.Context()
|
|
|
|
i, op := ctx.CurrInstr()
|
|
|
|
message = fmt.Sprintf("at breakpoint %d (%s)\n", i, op.String())
|
|
|
|
}
|
|
|
|
if message != "" {
|
|
|
|
c.Printf(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleCont(c *ishell.Context) {
|
|
|
|
if !checkVMIsReady(c) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v := getVMFromContext(c)
|
2019-10-22 10:44:14 +00:00
|
|
|
runVMWithHandling(c, v)
|
2019-09-10 16:11:48 +00:00
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleStep(c *ishell.Context) {
|
|
|
|
var (
|
|
|
|
n = 1
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
if !checkVMIsReady(c) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
if len(c.Args) > 0 {
|
|
|
|
n, err = strconv.Atoi(c.Args[0])
|
2018-04-04 19:41:19 +00:00
|
|
|
if err != nil {
|
2019-09-10 16:11:48 +00:00
|
|
|
c.Err(fmt.Errorf("argument conversion error: %s", err))
|
2018-04-04 19:41:19 +00:00
|
|
|
return
|
|
|
|
}
|
2019-09-10 16:11:48 +00:00
|
|
|
}
|
|
|
|
v.AddBreakPointRel(n)
|
2019-10-22 10:44:14 +00:00
|
|
|
runVMWithHandling(c, v)
|
2019-09-10 16:11:48 +00:00
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-10-14 15:37:11 +00:00
|
|
|
func handleStepInto(c *ishell.Context) {
|
|
|
|
handleStepType(c, "into")
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleStepOut(c *ishell.Context) {
|
|
|
|
handleStepType(c, "out")
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleStepOver(c *ishell.Context) {
|
|
|
|
handleStepType(c, "over")
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleStepType(c *ishell.Context, stepType string) {
|
|
|
|
if !checkVMIsReady(c) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v := getVMFromContext(c)
|
2019-10-22 10:44:14 +00:00
|
|
|
var err error
|
2019-10-14 15:37:11 +00:00
|
|
|
switch stepType {
|
|
|
|
case "into":
|
2019-10-22 10:44:14 +00:00
|
|
|
err = v.StepInto()
|
2019-10-14 15:37:11 +00:00
|
|
|
case "out":
|
2019-10-22 10:44:14 +00:00
|
|
|
err = v.StepOut()
|
2019-10-14 15:37:11 +00:00
|
|
|
case "over":
|
2019-10-22 10:44:14 +00:00
|
|
|
err = v.StepOver()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
c.Err(err)
|
2019-10-14 15:37:11 +00:00
|
|
|
}
|
2019-10-22 10:44:14 +00:00
|
|
|
handleIP(c)
|
2019-10-14 15:37:11 +00:00
|
|
|
changePrompt(c, v)
|
|
|
|
}
|
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func handleOps(c *ishell.Context) {
|
|
|
|
if !checkVMIsReady(c) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v := getVMFromContext(c)
|
|
|
|
v.PrintOps()
|
|
|
|
}
|
2018-03-30 16:15:06 +00:00
|
|
|
|
2019-09-10 16:11:48 +00:00
|
|
|
func changePrompt(c ishell.Actions, v *vm.VM) {
|
|
|
|
if v.Ready() && v.Context().IP()-1 >= 0 {
|
|
|
|
c.SetPrompt(fmt.Sprintf("NEO-GO-VM %d > ", v.Context().IP()-1))
|
|
|
|
} else {
|
|
|
|
c.SetPrompt("NEO-GO-VM > ")
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run waits for user input from Stdin and executes the passed command.
|
|
|
|
func (c *VMCLI) Run() error {
|
2019-10-16 10:47:42 +00:00
|
|
|
printLogo(c.shell)
|
2019-09-10 16:11:48 +00:00
|
|
|
c.shell.Run()
|
|
|
|
return nil
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|
|
|
|
|
2018-04-10 09:45:31 +00:00
|
|
|
func isMethodArg(s string) bool {
|
|
|
|
return len(strings.Split(s, ":")) == 1
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseArgs(args []string) ([]vm.StackItem, error) {
|
|
|
|
items := make([]vm.StackItem, len(args))
|
|
|
|
for i, arg := range args {
|
2019-09-10 16:41:11 +00:00
|
|
|
var typ, value string
|
2018-04-10 09:45:31 +00:00
|
|
|
typeAndVal := strings.Split(arg, ":")
|
|
|
|
if len(typeAndVal) < 2 {
|
2019-09-10 16:41:11 +00:00
|
|
|
if typeAndVal[0] == boolFalse || typeAndVal[0] == boolTrue {
|
|
|
|
typ = boolType
|
|
|
|
} else if _, err := strconv.Atoi(typeAndVal[0]); err == nil {
|
|
|
|
typ = intType
|
|
|
|
} else {
|
|
|
|
typ = stringType
|
|
|
|
}
|
|
|
|
value = typeAndVal[0]
|
|
|
|
} else {
|
|
|
|
typ = typeAndVal[0]
|
|
|
|
value = typeAndVal[1]
|
2018-04-10 09:45:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch typ {
|
2019-09-10 16:41:11 +00:00
|
|
|
case boolType:
|
|
|
|
if value == boolFalse {
|
2019-09-10 16:30:32 +00:00
|
|
|
items[i] = vm.NewBoolItem(false)
|
2019-09-10 16:41:11 +00:00
|
|
|
} else if value == boolTrue {
|
2019-09-10 16:30:32 +00:00
|
|
|
items[i] = vm.NewBoolItem(true)
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("failed to parse bool parameter")
|
|
|
|
}
|
2019-09-10 16:41:11 +00:00
|
|
|
case intType:
|
2020-03-24 08:06:26 +00:00
|
|
|
val, err := strconv.ParseInt(value, 10, 64)
|
2018-04-10 09:45:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
items[i] = vm.NewBigIntegerItem(val)
|
2019-09-10 16:41:11 +00:00
|
|
|
case stringType:
|
2018-04-10 09:45:31 +00:00
|
|
|
items[i] = vm.NewByteArrayItem([]byte(value))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return items, nil
|
|
|
|
}
|
|
|
|
|
2019-10-16 10:47:42 +00:00
|
|
|
func printLogo(c *ishell.Shell) {
|
2018-03-30 16:15:06 +00:00
|
|
|
logo := `
|
|
|
|
_ ____________ __________ _ ____ ___
|
|
|
|
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
|
|
|
|
/ |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ /
|
|
|
|
/ /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / /
|
|
|
|
/_/ |_/_____/\____/ \____/\____/ |___/_/ /_/
|
|
|
|
`
|
2019-10-16 10:47:42 +00:00
|
|
|
c.Print(logo)
|
|
|
|
c.Println()
|
|
|
|
c.Println()
|
2018-03-30 16:15:06 +00:00
|
|
|
}
|