selfcare init

This commit is contained in:
zhangsz
2025-03-03 11:40:37 +08:00
parent 19f09dd7ea
commit aca2bace68
692 changed files with 273972 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
package telsh
type internalDiscardLogger struct{}
func (internalDiscardLogger) Debug(...interface{}) {}
func (internalDiscardLogger) Debugf(string, ...interface{}) {}
func (internalDiscardLogger) Debugln(...interface{}) {}
func (internalDiscardLogger) Error(...interface{}) {}
func (internalDiscardLogger) Errorf(string, ...interface{}) {}
func (internalDiscardLogger) Errorln(...interface{}) {}
func (internalDiscardLogger) Trace(...interface{}) {}
func (internalDiscardLogger) Tracef(string, ...interface{}) {}
func (internalDiscardLogger) Traceln(...interface{}) {}
func (internalDiscardLogger) Warn(...interface{}) {}
func (internalDiscardLogger) Warnf(string, ...interface{}) {}
func (internalDiscardLogger) Warnln(...interface{}) {}

View File

@@ -0,0 +1,154 @@
/*
Package telsh provides "middleware" (for the telnet package) that can be used to implement a TELNET or TELNETS server
that provides a "shell" interface (also known as a "command-line interface" or "CLI").
Shell interfaces you may be familiar with include: "bash", "csh", "sh", "zsk", etc.
TELNET Server
Here is an example usage:
package main
import (
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
"github.com/reiver/go-telnet/telsh"
"io"
)
func main() {
telnetHandler := telsh.NewShellHandler()
if err := telnetHandler.RegisterElse(
telsh.ProducerFunc(
func(ctx telnet.Context, name string, args ...string) telsh.Handler {
return telsh.PromoteHandlerFunc(
func(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, args ...string) error {
oi.LongWrite(stdout, []byte{'w','a','t','?', '\r','\n'})
return nil
},
)
},
),
); nil != err {
panic(err)
}
if err := telnetHandler.Register("help",
telsh.ProducerFunc(
func(ctx telnet.Context, name string, args ...string) telsh.Handler {
return telsh.PromoteHandlerFunc(
func(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, args ...string) error {
oi.LongWrite(stdout, []byte{'r','t','f','m','!', '\r','\n'})
return nil
},
)
},
),
); nil != err {
panic(err)
}
err := telnet.ListenAndServe(":5555", telnetHandler)
if nil != err {
//@TODO: Handle this error better.
panic(err)
}
}
Here is a more "unpacked" example:
package main
import (
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
"github.com/reiver/go-telnet/telsh"
"io"
"time"
)
var (
shellHandler = telsh.NewShellHandler()
)
func init() {
shellHandler.Register("dance", telsh.ProducerFunc(producer))
shellHandler.WelcomeMessage = `
__ __ ______ _ _____ ____ __ __ ______
\ \ / /| ____|| | / ____| / __ \ | \/ || ____|
\ \ /\ / / | |__ | | | | | | | || \ / || |__
\ \/ \/ / | __| | | | | | | | || |\/| || __|
\ /\ / | |____ | |____ | |____ | |__| || | | || |____
\/ \/ |______||______| \_____| \____/ |_| |_||______|
`
}
func producer(ctx telnet.Context, name string, args ...string) telsh.Handler{
return telsh.PromoteHandlerFunc(handler)
}
func handler(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, args ...string) error {
for i:=0; i<20; i++ {
oi.LongWriteString(stdout, "\r⠋")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠙")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠹")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠸")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠼")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠴")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠦")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠧")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠇")
time.Sleep(50*time.Millisecond)
oi.LongWriteString(stdout, "\r⠏")
time.Sleep(50*time.Millisecond)
}
oi.LongWriteString(stdout, "\r \r\n")
return nil
}
func main() {
addr := ":5555"
if err := telnet.ListenAndServe(addr, shellHandler); nil != err {
panic(err)
}
}
*/
package telsh

View File

@@ -0,0 +1,140 @@
package telsh
import (
"io"
)
// Hander is an abstraction that represents a "running" shell "command".
//
// Contrast this with a Producer, which is is an abstraction that
// represents a shell "command".
//
// To use a metaphor, the differences between a Producer and a Handler,
// is like the difference between a program executable and actually running
// the program executable.
//
// Conceptually, anything that implements the Hander, and then has its Producer
// registered with ShellHandler.Register() will be available as a command.
//
// Note that Handler was intentionally made to be compatible with
// "os/exec", which is part of the Go standard library.
type Handler interface {
Run() error
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
}
// HandlerFunc is useful to write inline Producers, and provides an alternative to
// creating something that implements Handler directly.
//
// For example:
//
// shellHandler := telsh.NewShellHandler()
//
// shellHandler.Register("five", telsh.ProducerFunc(
//
// func(ctx telnet.Context, name string, args ...string) telsh.Handler{
//
// return telsh.PromoteHandlerFunc(
//
// func(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, args ...string) error {
// oi.LongWrite(stdout, []byte{'5', '\r', '\n'})
//
// return nil
// },
// )
// },
// ))
//
// Note that PromoteHandlerFunc is used to turn a HandlerFunc into a Handler.
type HandlerFunc func(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, args ...string)error
type internalPromotedHandlerFunc struct {
err error
fn HandlerFunc
stdin io.ReadCloser
stdout io.WriteCloser
stderr io.WriteCloser
stdinPipe io.WriteCloser
stdoutPipe io.ReadCloser
stderrPipe io.ReadCloser
args []string
}
// PromoteHandlerFunc turns a HandlerFunc into a Handler.
func PromoteHandlerFunc(fn HandlerFunc, args ...string) Handler {
stdin, stdinPipe := io.Pipe()
stdoutPipe, stdout := io.Pipe()
stderrPipe, stderr := io.Pipe()
argsCopy := make([]string, len(args))
for i, datum := range args {
argsCopy[i] = datum
}
handler := internalPromotedHandlerFunc{
err:nil,
fn:fn,
stdin:stdin,
stdout:stdout,
stderr:stderr,
stdinPipe:stdinPipe,
stdoutPipe:stdoutPipe,
stderrPipe:stderrPipe,
args:argsCopy,
}
return &handler
}
func (handler *internalPromotedHandlerFunc) Run() error {
if nil != handler.err {
return handler.err
}
handler.err = handler.fn(handler.stdin, handler.stdout, handler.stderr, handler.args...)
handler.stdin.Close()
handler.stdout.Close()
handler.stderr.Close()
return handler.err
}
func (handler *internalPromotedHandlerFunc) StdinPipe() (io.WriteCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stdinPipe, nil
}
func (handler *internalPromotedHandlerFunc) StdoutPipe() (io.ReadCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stdoutPipe, nil
}
func (handler *internalPromotedHandlerFunc) StderrPipe() (io.ReadCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stderrPipe, nil
}

View File

@@ -0,0 +1,120 @@
package telsh
import (
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
"io"
"sort"
)
type internalHelpProducer struct {
shellHandler *ShellHandler
}
func Help(shellHandler *ShellHandler) Producer {
producer := internalHelpProducer{
shellHandler:shellHandler,
}
return &producer
}
func (producer *internalHelpProducer) Produce(telnet.Context, string, ...string) Handler {
return newHelpHandler(producer)
}
type internalHelpHandler struct {
helpProducer *internalHelpProducer
err error
stdin io.ReadCloser
stdout io.WriteCloser
stderr io.WriteCloser
stdinPipe io.WriteCloser
stdoutPipe io.ReadCloser
stderrPipe io.ReadCloser
}
func newHelpHandler(helpProducer *internalHelpProducer) *internalHelpHandler {
stdin, stdinPipe := io.Pipe()
stdoutPipe, stdout := io.Pipe()
stderrPipe, stderr := io.Pipe()
handler := internalHelpHandler{
helpProducer:helpProducer,
err:nil,
stdin:stdin,
stdout:stdout,
stderr:stderr,
stdinPipe:stdinPipe,
stdoutPipe:stdoutPipe,
stderrPipe:stderrPipe,
}
return &handler
}
func (handler *internalHelpHandler) Run() error {
if nil != handler.err {
return handler.err
}
//@TODO: Should this be reaching inside of ShellHandler? Maybe there should be ShellHandler public methods instead.
keys := make([]string, 1+len(handler.helpProducer.shellHandler.producers))
i:=0
for key,_ := range handler.helpProducer.shellHandler.producers {
keys[i] = key
i++
}
keys[i] = handler.helpProducer.shellHandler.ExitCommandName
sort.Strings(keys)
for _, key := range keys {
oi.LongWriteString(handler.stdout, key)
oi.LongWriteString(handler.stdout, "\r\n")
}
handler.stdin.Close()
handler.stdout.Close()
handler.stderr.Close()
return handler.err
}
func (handler *internalHelpHandler) StdinPipe() (io.WriteCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stdinPipe, nil
}
func (handler *internalHelpHandler) StdoutPipe() (io.ReadCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stdoutPipe, nil
}
func (handler *internalHelpHandler) StderrPipe() (io.ReadCloser, error) {
if nil != handler.err {
return nil, handler.err
}
return handler.stderrPipe, nil
}

View File

@@ -0,0 +1,36 @@
package telsh
import (
"github.com/reiver/go-telnet"
)
// A Producer provides a Produce method which creates a Handler.
//
// Producer is an abstraction that represents a shell "command".
//
// Contrast this with a Handler, which is is an abstraction that
// represents a "running" shell "command".
//
// To use a metaphor, the differences between a Producer and a Handler,
// is like the difference between a program executable and actually running
// the program executable.
type Producer interface {
Produce(telnet.Context, string, ...string) Handler
}
// ProducerFunc is an adaptor, that can be used to turn a func with the
// signature:
//
// func(telnet.Context, string, ...string) Handler
//
// Into a Producer
type ProducerFunc func(telnet.Context, string, ...string) Handler
// Produce makes ProducerFunc fit the Producer interface.
func (fn ProducerFunc) Produce(ctx telnet.Context, name string, args ...string) Handler {
return fn(ctx, name, args...)
}

View File

@@ -0,0 +1,297 @@
package telsh
import (
"github.com/reiver/go-oi"
"github.com/reiver/go-telnet"
"bytes"
"io"
"strings"
"sync"
)
const (
defaultExitCommandName = "exit"
defaultPrompt = "§ "
defaultWelcomeMessage = "\r\nWelcome!\r\n"
defaultExitMessage = "\r\nGoodbye!\r\n"
)
type ShellHandler struct {
muxtex sync.RWMutex
producers map[string]Producer
elseProducer Producer
ExitCommandName string
Prompt string
WelcomeMessage string
ExitMessage string
}
func NewShellHandler() *ShellHandler {
producers := map[string]Producer{}
telnetHandler := ShellHandler{
producers:producers,
Prompt: defaultPrompt,
ExitCommandName: defaultExitCommandName,
WelcomeMessage: defaultWelcomeMessage,
ExitMessage: defaultExitMessage,
}
return &telnetHandler
}
func (telnetHandler *ShellHandler) Register(name string, producer Producer) error {
telnetHandler.muxtex.Lock()
telnetHandler.producers[name] = producer
telnetHandler.muxtex.Unlock()
return nil
}
func (telnetHandler *ShellHandler) MustRegister(name string, producer Producer) *ShellHandler {
if err := telnetHandler.Register(name, producer); nil != err {
panic(err)
}
return telnetHandler
}
func (telnetHandler *ShellHandler) RegisterHandlerFunc(name string, handlerFunc HandlerFunc) error {
produce := func(ctx telnet.Context, name string, args ...string) Handler {
return PromoteHandlerFunc(handlerFunc, args...)
}
producer := ProducerFunc(produce)
return telnetHandler.Register(name, producer)
}
func (telnetHandler *ShellHandler) MustRegisterHandlerFunc(name string, handlerFunc HandlerFunc) *ShellHandler {
if err := telnetHandler.RegisterHandlerFunc(name, handlerFunc); nil != err {
panic(err)
}
return telnetHandler
}
func (telnetHandler *ShellHandler) RegisterElse(producer Producer) error {
telnetHandler.muxtex.Lock()
telnetHandler.elseProducer = producer
telnetHandler.muxtex.Unlock()
return nil
}
func (telnetHandler *ShellHandler) MustRegisterElse(producer Producer) *ShellHandler {
if err := telnetHandler.RegisterElse(producer); nil != err {
panic(err)
}
return telnetHandler
}
func (telnetHandler *ShellHandler) ServeTELNET(ctx telnet.Context, writer telnet.Writer, reader telnet.Reader) {
logger := ctx.Logger()
if nil == logger {
logger = internalDiscardLogger{}
}
colonSpaceCommandNotFoundEL := []byte(": command not found\r\n")
var prompt bytes.Buffer
var exitCommandName string
var welcomeMessage string
var exitMessage string
prompt.WriteString(telnetHandler.Prompt)
promptBytes := prompt.Bytes()
exitCommandName = telnetHandler.ExitCommandName
welcomeMessage = telnetHandler.WelcomeMessage
exitMessage = telnetHandler.ExitMessage
if _, err := oi.LongWriteString(writer, welcomeMessage); nil != err {
logger.Errorf("Problem long writing welcome message: %v", err)
return
}
logger.Debugf("Wrote welcome message: %q.", welcomeMessage)
if _, err := oi.LongWrite(writer, promptBytes); nil != err {
logger.Errorf("Problem long writing prompt: %v", err)
return
}
logger.Debugf("Wrote prompt: %q.", promptBytes)
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
p := buffer[:]
var line bytes.Buffer
for {
// Read 1 byte.
n, err := reader.Read(p)
if n <= 0 && nil == err {
continue
} else if n <= 0 && nil != err {
break
}
line.WriteByte(p[0])
//logger.Tracef("Received: %q (%d).", p[0], p[0])
if '\n' == p[0] {
lineString := line.String()
if "\r\n" == lineString {
line.Reset()
if _, err := oi.LongWrite(writer, promptBytes); nil != err {
return
}
continue
}
//@TODO: support piping.
fields := strings.Fields(lineString)
logger.Debugf("Have %d tokens.", len(fields))
logger.Tracef("Tokens: %v", fields)
if len(fields) <= 0 {
line.Reset()
if _, err := oi.LongWrite(writer, promptBytes); nil != err {
return
}
continue
}
field0 := fields[0]
if exitCommandName == field0 {
oi.LongWriteString(writer, exitMessage)
return
}
var producer Producer
telnetHandler.muxtex.RLock()
var ok bool
producer, ok = telnetHandler.producers[field0]
telnetHandler.muxtex.RUnlock()
if !ok {
telnetHandler.muxtex.RLock()
producer = telnetHandler.elseProducer
telnetHandler.muxtex.RUnlock()
}
if nil == producer {
//@TODO: Don't convert that to []byte! think this creates "garbage" (for collector).
oi.LongWrite(writer, []byte(field0))
oi.LongWrite(writer, colonSpaceCommandNotFoundEL)
line.Reset()
if _, err := oi.LongWrite(writer, promptBytes); nil != err {
return
}
continue
}
handler := producer.Produce(ctx, field0, fields[1:]...)
if nil == handler {
oi.LongWrite(writer, []byte(field0))
//@TODO: Need to use a different error message.
oi.LongWrite(writer, colonSpaceCommandNotFoundEL)
line.Reset()
oi.LongWrite(writer, promptBytes)
continue
}
//@TODO: Wire up the stdin, stdout, stderr of the handler.
if stdoutPipe, err := handler.StdoutPipe(); nil != err {
//@TODO:
} else if nil == stdoutPipe {
//@TODO:
} else {
connect(ctx, writer, stdoutPipe)
}
if stderrPipe, err := handler.StderrPipe(); nil != err {
//@TODO:
} else if nil == stderrPipe {
//@TODO:
} else {
connect(ctx, writer, stderrPipe)
}
if err := handler.Run(); nil != err {
//@TODO:
}
line.Reset()
if _, err := oi.LongWrite(writer, promptBytes); nil != err {
return
}
}
//@TODO: Are there any special errors we should be dealing with separately?
if nil != err {
break
}
}
oi.LongWriteString(writer, exitMessage)
return
}
func connect(ctx telnet.Context, writer io.Writer, reader io.Reader) {
logger := ctx.Logger()
go func(logger telnet.Logger){
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
p := buffer[:]
for {
// Read 1 byte.
n, err := reader.Read(p)
if n <= 0 && nil == err {
continue
} else if n <= 0 && nil != err {
break
}
//logger.Tracef("Sending: %q.", p)
//@TODO: Should we be checking for errors?
oi.LongWrite(writer, p)
//logger.Tracef("Sent: %q.", p)
}
}(logger)
}

View File

@@ -0,0 +1,110 @@
package telsh
import (
"github.com/reiver/go-telnet"
"bytes"
"strings"
"testing"
)
func TestServeTELNETCommandNotFound(t *testing.T) {
tests := []struct{
ClientSends string
Expected string
}{
{
ClientSends: "\r\n",
Expected: "",
},
{
ClientSends: "apple\r\n",
Expected: "apple: command not found\r\n",
},
{
ClientSends: "banana\r\n",
Expected: "banana: command not found\r\n",
},
{
ClientSends: "cherry\r\n",
Expected: "cherry: command not found\r\n",
},
{
ClientSends: "\t\r\n",
Expected: "",
},
{
ClientSends: "\t\t\r\n",
Expected: "",
},
{
ClientSends: "\t\t\t\r\n",
Expected: "",
},
{
ClientSends: " \r\n",
Expected: "",
},
{
ClientSends: " \r\n",
Expected: "",
},
{
ClientSends: " \r\n",
Expected: "",
},
{
ClientSends: " \t\r\n",
Expected: "",
},
{
ClientSends: "\t \r\n",
Expected: "",
},
{
ClientSends: "ls -alF\r\n",
Expected: "ls: command not found\r\n",
},
}
for testNumber, test := range tests {
shellHandler := NewShellHandler()
if nil == shellHandler {
t.Errorf("For test #%d, did not expect to get nil, but actually got it: %v; for client sent: %q", testNumber, shellHandler, test.ClientSends)
continue
}
ctx := telnet.NewContext()
var buffer bytes.Buffer
shellHandler.ServeTELNET(ctx, &buffer, strings.NewReader(test.ClientSends))
if expected, actual := shellHandler.WelcomeMessage+shellHandler.Prompt+test.Expected+shellHandler.Prompt+shellHandler.ExitMessage, buffer.String(); expected != actual {
t.Errorf("For test #%d, expect %q, but actually got %q; for client sent: %q", testNumber, expected, actual, test.ClientSends)
continue
}
}
}