8 コミット 3844813ff4 ... 5d53a9c985

作者 SHA1 メッセージ 日付
  niujiuru 5d53a9c985 优化修改项目代码结构, 使得sshd的客户端为纯go编译, 以便生成linux和windows客户端 2 週間 前
  niujiuru a798f3d4a3 优化修改主Makefile 2 週間 前
  niujiuru 20caf558ac 优化修改sshd模块客户端代码,使其交互更加友好 2 週間 前
  niujiuru bdd5b44c1f 优化修改sshd模块客户端代码,使其交互更加友好 2 週間 前
  niujiuru 4ece0c9b1e 优化修改sshd模块代码,解决bug 2 週間 前
  niujiuru ae88896b06 优化修改sshd模块代码,解决bug 2 週間 前
  niujiuru 028b293b91 优化修改sshd模块代码,解决bug 2 週間 前
  niujiuru f0c259a69c 优化修改sshd模块代码, 修复昨天自测试中出现的问题 2 週間 前
11 ファイル変更516 行追加68 行削除
  1. 11 4
      Makefile
  2. 2 0
      go.mod
  3. 5 0
      go.sum
  4. 99 0
      sshd/client/args.go
  5. 92 29
      sshd/client/client.go
  6. 214 0
      sshd/client/client.go.old
  7. 1 3
      sshd/client/config.go
  8. 2 2
      sshd/sshd.go
  9. 3 20
      utils/shell/execute.go
  10. 31 0
      utils/shell/execute_types.go
  11. 56 10
      utils/shell/executor.go

+ 11 - 4
Makefile

@@ -7,7 +7,7 @@ DEFINS = -D_GNU_SOURCE
 
 GO := go
 GO_BUILD := $(GO) build
-GO_FLAGS := -ldflags "-s -w -X hnyfkj.com.cn/rtu/linux/baseapp.Version=1.0.0.1 -X hnyfkj.com.cn/rtu/linux/baseapp.BuildTime=$(shell date +%Y-%m-%dT%H:%M:%S)"
+GO_FLAGS := -ldflags "-s -w -X hnyfkj.com.cn/rtu/linux/baseapp.Version=1.0.0.1 -X hnyfkj.com.cn/rtu/linux/baseapp.BuildTime=$(shell date +%Y-%m-%dT%H:%M:%S) -X main.BuildTime=$(shell date +%Y-%m-%dT%H:%M:%S)"
 
 target ?= armv7hf
 ifeq ($(target),armv7hf)
@@ -29,7 +29,7 @@ SETGO_ENV = \
 DATE := $(shell date +%Y%m%d_%H%M%S)
 
 # 编译的目标
-all : camera_test.out hk_takephoto.out dh_takephoto.out air720u_4g.out rtu_linux_modules.out yfkj_sshd.out yfkj_ssh_client.out
+all : camera_test.out hk_takephoto.out dh_takephoto.out air720u_4g.out rtu_linux_modules.out yfkj_sshd.out yfkj_ssh_client.out yfkj_ssh_client.exe
 
 # 通用基础库
 libswapi.a :
@@ -130,10 +130,17 @@ yfkj_sshd.out       : libswapi.a libair530z.a ./sshd/server/server.go
 	$(SETGO_ENV) CGO_LDFLAGS="$(LIB6)" $(GO_BUILD) $(GO_FLAGS) -o $@ ./sshd/server/*.go
 	@cp $@ ./build/$(basename $@)_$(DATE)$(suffix $@)
 
-yfkj_ssh_client.out : libswapi.a libair530z.a ./sshd/client/client.go
+yfkj_ssh_client.out : ./sshd/client/client.go
 	mkdir -p ./build
 	$(GO) mod tidy
-	$(SETGO_ENV) CGO_LDFLAGS="$(LIB6)" $(GO_BUILD) $(GO_FLAGS) -o $@ ./sshd/client/*.go
+	GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO_BUILD) $(GO_FLAGS) -o $@ ./sshd/client/*.go
+	@cp $@ ./build/$(basename $@)_$(DATE)$(suffix $@)
+	rm -rf $@
+
+yfkj_ssh_client.exe : ./sshd/client/client.go
+	mkdir -p ./build
+	$(GO) mod tidy
+	GOOS=windows GOARCH=amd64 CGO_ENABLED=0 $(GO_BUILD) $(GO_FLAGS) -o $@ ./sshd/client/*.go
 	@cp $@ ./build/$(basename $@)_$(DATE)$(suffix $@)
 	rm -rf $@
 

+ 2 - 0
go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/jlaffaye/ftp v0.2.0
 	github.com/mattn/go-shellwords v1.0.12
+	github.com/peterh/liner v1.2.2
 	github.com/sirupsen/logrus v1.9.3
 	github.com/vishvananda/netlink v1.3.1
 	gopkg.in/ini.v1 v1.67.0
@@ -19,6 +20,7 @@ require (
 	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/mattn/go-runewidth v0.0.3 // indirect
 	github.com/vishvananda/netns v0.0.5 // indirect
 	golang.org/x/net v0.44.0 // indirect
 	golang.org/x/sync v0.17.0 // indirect

+ 5 - 0
go.sum

@@ -17,8 +17,12 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
 github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
+github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
 github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
+github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
+github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -36,6 +40,7 @@ golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
 golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
 golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
 golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 99 - 0
sshd/client/args.go

@@ -0,0 +1,99 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+
+package main
+
+import (
+	"os"
+	"strconv"
+	"strings"
+)
+
+// 解析命令行参数os.Args参数到map, 以'-'开头的视为key, 后面跟着的为value
+var os_args_param map[string]string = make(map[string]string)
+
+func init() {
+	procArgsParam(os.Args)
+}
+
+func GetArgsParamStr(p string, def string) string {
+	s, ok := os_args_param[p]
+	if ok {
+		return s
+	} else {
+		return def
+	}
+}
+
+func GetArgsParamBool(p string, def bool) bool {
+	s, ok := os_args_param[p]
+	if ok {
+		return strings.EqualFold(s, "y") || strings.EqualFold(s, "yes") || strings.EqualFold(s, "true")
+	} else {
+		return def
+	}
+}
+
+func GetArgsParamInt64(p string, def int64) int64 {
+	s, ok := os_args_param[p]
+	if ok {
+		i, e := strconv.ParseInt(s, 10, 64)
+		if e != nil {
+			return def
+		}
+		return i
+	} else {
+		return def
+	}
+}
+
+func GetArgsParamInt(p string, def int) int {
+	s, ok := os_args_param[p]
+	if ok {
+		i, e := strconv.Atoi(s)
+		if e != nil {
+			return def
+		}
+		return i
+	} else {
+		return def
+	}
+}
+
+func procArgsParam(p []string) {
+	if len(p) < 1 {
+		return
+	}
+
+	var k, v string
+	for i := 1; i < len(p); i++ {
+		if len(p[i]) < 1 {
+			continue
+		}
+
+		if p[i][0] == '-' {
+			if k != "" {
+				os_args_param[k] = v
+			}
+			k, v = p[i], ""
+		} else {
+			if k == "" {
+				continue
+			}
+			if v != "" {
+				v += " " + p[i]
+			} else {
+				v = p[i]
+			}
+		}
+	}
+
+	if k != "" {
+		os_args_param[k] = v
+	}
+}
+
+func IsArgsParam(p string) bool {
+	_, ok := os_args_param[p]
+	return ok
+}

+ 92 - 29
sshd/client/client.go

@@ -8,14 +8,16 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"os/exec"
 	"os/signal"
+	"runtime"
 	"strings"
 	"sync/atomic"
 	"syscall"
 	"time"
 
 	"github.com/google/uuid"
-	"hnyfkj.com.cn/rtu/linux/baseapp"
+	"github.com/peterh/liner"
 )
 
 const MODULE_NAME = "YFKJ_SSH_CLIENT"
@@ -23,22 +25,23 @@ const MODULE_NAME = "YFKJ_SSH_CLIENT"
 var (
 	coupler               *MQTTCoupler
 	Version               = "1.0.0.1"
+	BuildTime             = "unknown"
 	ErrBrokerAddressEmpty = errors.New("mqtt server address is empty")
 	ErrIMEINotAvailable   = errors.New("device imei is not available")
 )
 
 func main() {
-	if baseapp.IsArgsParam("-h") {
+	if IsArgsParam("-h") {
 		help()
 		return
 	}
 
-	if baseapp.IsArgsParam("-v") {
-		fmt.Println("程序版本:", Version, "\n构建时间:", baseapp.BuildTime)
+	if IsArgsParam("-v") {
+		fmt.Println("程序版本:", Version, "\n构建时间:", BuildTime)
 		return
 	}
 
-	devIMEI := baseapp.GetArgsParamStr("-c", "")
+	devIMEI := GetArgsParamStr("-c", "")
 	if devIMEI == "" {
 		help()
 		return
@@ -70,15 +73,13 @@ func main() {
 		return
 	}
 
-	fmt.Printf("[%s] 正在连接目标设备...\n", MODULE_NAME)
-
 	var pingState atomic.Bool
 	heartbeatLoop(&pingState) // -启动-设备在线-心跳检测-
 	for {
 		if pingState.Load() { //// 等待成功连接上目标设备卍
 			break
 		}
-		fmt.Printf("[%s] 无法连接目标设备!!\n", MODULE_NAME)
+		fmt.Printf("[%s] 正在尝试连接设备...\n", MODULE_NAME)
 		time.Sleep(1 * time.Second)
 	}
 
@@ -93,40 +94,88 @@ func term(pingState *atomic.Bool) {
 
 	printWelcome()
 
-	reader := bufio.NewReader(os.Stdin)
+	line := liner.NewLiner()
+	defer line.Close()
+	line.SetCtrlCAborts(false)
+	line.SetTabCompletionStyle(liner.TabCircular)
+
+	historyFile := "history.txt"
+	var history []string
+
+	if f, err := os.Open(historyFile); err == nil {
+		scanner := bufio.NewScanner(f)
+		for scanner.Scan() {
+			cmd := strings.TrimSpace(scanner.Text())
+			if cmd != "" {
+				history = append(history, cmd)
+				line.AppendHistory(cmd)
+			}
+		}
+		f.Close()
+	}
+
+	defer func() {
+		if f, err := os.Create(historyFile); err == nil {
+			line.WriteHistory(f)
+			f.Close()
+		}
+	}()
+
+	line.SetCompleter(func(input string) []string { // 补全
+		var matches []string
+		for _, cmd := range history {
+			if strings.HasPrefix(cmd, input) {
+				matches = append(matches, cmd)
+			}
+		}
+		return matches
+	})
+
 	for {
 		if !pingState.Load() {
 			fmt.Printf("[%s] 目标设备连接丢失!!\n", MODULE_NAME)
 			break
 		}
 
-		fmt.Print("\033[?25h") // 显示光标
 		if interrupted.Swap(false) {
 			fmt.Println("^C")
 		}
-		fmt.Printf("root@%s:%s# ", coupler.imei, coupler.cwd)
 
-		input, err := reader.ReadString('\n')
-		if err != nil {
-			if err == io.EOF {
-				os.Exit(0)
-			}
-			fmt.Println("读取用户输入失败:", err)
-			continue
+		prompt := fmt.Sprintf("root@%s:%s# ", coupler.imei, coupler.cwd)
+		input, err := line.Prompt(prompt) /// 等待用户输入指令
+		if err == nil {
+			goto inputOK
 		}
 
+		if err == io.EOF { ////////////////// Ctrl+D 按键处理
+			_, _ = coupler.quit()
+			fmt.Println("bye")
+			break
+		}
+
+		fmt.Println("读取用户输入失败:", err)
+		continue
+
+	inputOK:
 		input = strings.TrimSpace(input)
 		if input == "" {
 			continue
 		}
 
-		switch input {
-		case "quit":
+		line.AppendHistory(input) ///// 保存用户输入的历史命令
+
+		history = append(history, input) ///// 本次也立刻生效
+
+		if input == "exit" || input == "quit" {
 			_, _ = coupler.quit()
-			os.Exit(0)
+			break
+		}
+
+		if input == "clear" {
+			clearScreen()
+			continue
 		}
 
-		fmt.Print("\033[?25l") // 隐藏光标
 		executing.Store(true)
 		result, err := coupler.exec(input)
 		executing.Store(false)
@@ -137,11 +186,14 @@ func term(pingState *atomic.Bool) {
 		}
 
 		if result.Stdout != "" {
-			fmt.Println(result.Stdout)
+			fmt.Print(result.Stdout)
 		}
 		if result.Stderr != "" {
 			fmt.Fprintln(os.Stderr, result.Stderr)
 		}
+		if result.ExitCode == 124 {
+			fmt.Println("command timeout")
+		}
 
 		if result.Cwd != "" {
 			coupler.cwd = result.Cwd
@@ -176,7 +228,6 @@ func heartbeatLoop(pingState *atomic.Bool) {
 	go func() {
 		ticker := time.NewTicker(1 * time.Second)
 		defer ticker.Stop()
-
 		pingFailCount := 0
 		pong := ""
 		for range ticker.C {
@@ -190,7 +241,7 @@ func heartbeatLoop(pingState *atomic.Bool) {
 				if pingFailCount >= 3 { ///// 连续3次ping失败, 可以认为设备离线
 					pingState.Store(false)
 				}
-			}
+			} // end if
 		} // end for
 	}()
 }
@@ -204,10 +255,22 @@ func printWelcome() {
 | |\  | | || (__| | | | | | |  __/
 |_| \_|_|\__\___|_| |_|_|_|_|\___|
 
-══════════════════════════════════
-      云飞科技 RTU远程运维终端
-══════════════════════════════════
-提示: 输入'quit'命令, 退出终端模拟器
+══════════════════════════════════
+       云飞科技RTU远程运维终端
+══════════════════════════════════
+提示: 输入'quit'命令, 退出终端
 `
 	fmt.Println(welcome)
 }
+
+func clearScreen() {
+	if runtime.GOOS == "windows" {
+		cmd := exec.Command("cmd", "/c", "cls")
+		cmd.Stdout = os.Stdout
+		_ = cmd.Run()
+	} else {
+		cmd := exec.Command("clear")
+		cmd.Stdout = os.Stdout
+		_ = cmd.Run()
+	}
+}

+ 214 - 0
sshd/client/client.go.old

@@ -0,0 +1,214 @@
+package main
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"os/signal"
+	"strings"
+	"sync/atomic"
+	"syscall"
+	"time"
+
+	"github.com/google/uuid"
+	"hnyfkj.com.cn/rtu/linux/baseapp"
+)
+
+const MODULE_NAME = "YFKJ_SSH_CLIENT"
+
+var (
+	coupler               *MQTTCoupler
+	Version               = "1.0.0.1"
+	ErrBrokerAddressEmpty = errors.New("mqtt server address is empty")
+	ErrIMEINotAvailable   = errors.New("device imei is not available")
+)
+
+func main() {
+	if baseapp.IsArgsParam("-h") {
+		help()
+		return
+	}
+
+	if baseapp.IsArgsParam("-v") {
+		fmt.Println("程序版本:", Version, "\n构建时间:", baseapp.BuildTime)
+		return
+	}
+
+	devIMEI := baseapp.GetArgsParamStr("-c", "")
+	if devIMEI == "" {
+		help()
+		return
+	}
+
+	if err := loadAppConfig(); err != nil {
+		fmt.Printf("[%s] 错误: %v!!\n", MODULE_NAME, err)
+		return
+	}
+	if CfgServers.MQTTSrv.Address == "" {
+		fmt.Printf("[%s] 错误: %v!!\n", MODULE_NAME, ErrBrokerAddressEmpty)
+		return
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	coupler = &MQTTCoupler{
+		ctx:      ctx,
+		cancel:   cancel,
+		broker:   CfgServers.MQTTSrv.Address,
+		username: CfgServers.MQTTSrv.Username,
+		password: CfgServers.MQTTSrv.Password,
+		clientID: uuid.New().String(),
+		imei:     devIMEI,
+		cwd:      "/",
+	}
+
+	if err := coupler.init2(); err != nil {
+		fmt.Printf("[%s] 错误: %v\n!!", MODULE_NAME, err)
+		return
+	}
+
+	var pingState atomic.Bool
+	heartbeatLoop(&pingState) // -启动-设备在线-心跳检测-
+	for {
+		if pingState.Load() { //// 等待成功连接上目标设备卍
+			break
+		}
+		fmt.Printf("[%s] 正在尝试连接设备....\n", MODULE_NAME)
+		time.Sleep(1 * time.Second)
+	}
+
+	term(&pingState) /////////////////// 启动终端模拟器卍
+}
+
+func term(pingState *atomic.Bool) {
+	var executing atomic.Bool   // 是否有正在执行中的命令
+	var interrupted atomic.Bool // 用户是否按键取消了命令
+
+	interruptLoop(&executing, &interrupted) // Ctrl+C卍
+
+	printWelcome()
+
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		if !pingState.Load() {
+			fmt.Printf("[%s] 目标设备连接丢失!!\n", MODULE_NAME)
+			break
+		}
+
+		fmt.Print("\033[?25h") // 显示光标
+		if interrupted.Swap(false) {
+			fmt.Println("^C")
+		}
+		fmt.Printf("root@%s:%s# ", coupler.imei, coupler.cwd)
+
+		input, err := reader.ReadString('\n')
+		if err != nil {
+			if err == io.EOF {
+				os.Exit(0)
+			}
+			fmt.Println("读取用户输入失败: ", err)
+			continue
+		}
+
+		input = strings.TrimSpace(input)
+		if input == "" {
+			continue
+		}
+
+		switch input {
+		case "quit":
+			_, _ = coupler.quit()
+			os.Exit(0)
+		}
+
+		fmt.Print("\033[?25l") // 隐藏光标
+		executing.Store(true)
+		result, err := coupler.exec(input)
+		executing.Store(false)
+
+		if err != nil {
+			fmt.Printf("执行错误: %v\n", err)
+			continue
+		}
+
+		if result.Stdout != "" {
+			fmt.Print(result.Stdout)
+		}
+		if result.Stderr != "" {
+			fmt.Fprintln(os.Stderr, result.Stderr)
+		}
+		if result.ExitCode == 124 {
+			fmt.Println("command timed out")
+		}
+
+		if result.Cwd != "" {
+			coupler.cwd = result.Cwd
+		}
+	}
+}
+
+func help() {
+	h := `
+-h 显示帮助提示
+-v 当前程序版本
+-c 连接目标设备(IMEI), 例如: -c 869523059113051
+`
+
+	fmt.Println(h)
+}
+
+func interruptLoop(executing *atomic.Bool, interrupted *atomic.Bool) {
+	sigCh := make(chan os.Signal, 1)
+	signal.Notify(sigCh, syscall.SIGINT)
+	go func() {
+		for range sigCh {
+			interrupted.Store(true)
+			if executing.Load() {
+				_, _ = coupler.stop()
+			}
+		}
+	}()
+}
+
+func heartbeatLoop(pingState *atomic.Bool) {
+	go func() {
+		ticker := time.NewTicker(1 * time.Second)
+		defer ticker.Stop()
+
+		pingFailCount := 0
+		pong := ""
+		for range ticker.C {
+			resp, err := coupler.ping()
+			if err == nil && resp.Error == nil && resp.Result != nil &&
+				json.Unmarshal(resp.Result, &pong) == nil && pong == "pong" {
+				pingState.Store(true)
+				pingFailCount = 0
+			} else {
+				pingFailCount++
+				if pingFailCount >= 3 { ///// 连续3次ping失败, 可以认为设备离线
+					pingState.Store(false)
+				}
+			}
+		} // end for
+	}()
+}
+
+func printWelcome() {
+	welcome := `
+ _   _ _ _       _     _ _ _     
+| \ | (_) |     | |   (_) | |    
+|  \| |_| |_ ___| |__  _| | | ___
+| .   | | __/ __| '_ \| | | |/ _ \
+| |\  | | || (__| | | | | | |  __/
+|_| \_|_|\__\___|_| |_|_|_|_|\___|
+
+══════════════════════════════════
+       云飞科技RTU远程运维终端
+══════════════════════════════════
+提示: 输入'quit'命令, 退出终端
+`
+	fmt.Println(welcome)
+}

+ 1 - 3
sshd/client/config.go

@@ -2,10 +2,8 @@ package main
 
 import (
 	"fmt"
-	"path/filepath"
 
 	"gopkg.in/ini.v1"
-	"hnyfkj.com.cn/rtu/linux/baseapp"
 )
 
 type MQTTBroker struct {
@@ -29,7 +27,7 @@ var (
 )
 
 func loadAppConfig() error {
-	cfgFile := filepath.Join(baseapp.EXEC_DIR, "config.ini")
+	cfgFile := "./config.ini"
 	cfgIni, err := ini.Load(cfgFile)
 	if err != nil {
 		return err

+ 2 - 2
sshd/sshd.go

@@ -222,7 +222,7 @@ func (c *MQTTCoupler) handleRequests(client mqtt.Client, msg mqtt.Message) {
 
 func (c *MQTTCoupler) execOneCmd(msg mqtt.Message) {
 	str := string(msg.Payload())
-	baseapp.Logger.Infof("[%s] 收到一个RPC请求: %s", MODULE_NAME, str)
+	baseapp.Logger.Debugf("[%s] 收到一个RPC请求: %s", MODULE_NAME, str)
 
 	var resp *jsonrpc2.Response // 预先定义一个空的应答
 	var clientID string         // 该客户端的|唯一标识|
@@ -354,7 +354,7 @@ retp:
 		baseapp.Logger.Errorf("[%s] 发送RPC应答失败: %v!!", MODULE_NAME, err)
 	}
 
-	baseapp.Logger.Infof("[%s] 发送一个RPC应答, 报文内容: %s", MODULE_NAME, text)
+	baseapp.Logger.Debugf("[%s] 发送一个RPC应答, 报文内容: %s", MODULE_NAME, text)
 }
 
 func (c *MQTTCoupler) startExecutorReaper(interval, timeout time.Duration) {

+ 3 - 20
utils/shell/execute.go

@@ -1,3 +1,6 @@
+//go:build !windows
+// +build !windows
+
 // Author: NiuJiuRu
 // Email: niujiuru@qq.com
 
@@ -6,7 +9,6 @@ package shell
 import (
 	"bytes"
 	"context"
-	"errors"
 	"fmt"
 	"io"
 	"os/exec"
@@ -17,7 +19,6 @@ import (
 )
 
 const (
-	DefaultTimeout    = 3 * time.Second
 	gracePeriod       = 2 * time.Second
 	forceKillWait     = 2 * time.Second
 	exitTimeoutCode   = 124
@@ -25,24 +26,6 @@ const (
 	checkProcessDelay = 50 * time.Millisecond
 )
 
-var (
-	ErrInvalidCommand      = errors.New("invalid command")
-	ErrExecutorLostControl = errors.New("executor lost control of process")
-)
-
-type ExecuteParams struct {
-	Cmd     string `json:"cmd"`               // 命令
-	Timeout int    `json:"timeout,omitempty"` // 超时(秒)
-	Dir     string `json:"-"`                 // 工作目录
-}
-
-type ExecuteResult struct {
-	Stdout   string `json:"stdout"`    ///////// 标准输出
-	Stderr   string `json:"stderr"`    ///////// 错误输出
-	ExitCode int    `json:"exit_code"` ///////// 退出状态码: 0表示成功, 非0表示失败
-	Cwd      string `json:"cwd"`       ///////// 当前目录
-}
-
 type limitedBuffer struct {
 	buf   *bytes.Buffer
 	limit int

+ 31 - 0
utils/shell/execute_types.go

@@ -0,0 +1,31 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+
+package shell
+
+import (
+	"errors"
+	"time"
+)
+
+const (
+	DefaultTimeout = 5 * time.Second
+)
+
+var (
+	ErrInvalidCommand      = errors.New("invalid command")
+	ErrExecutorLostControl = errors.New("executor lost control of process")
+)
+
+type ExecuteParams struct {
+	Cmd     string `json:"cmd"`               // 命令
+	Timeout int    `json:"timeout,omitempty"` // 超时(秒)
+	Dir     string `json:"-"`                 // 工作目录
+}
+
+type ExecuteResult struct {
+	Stdout   string `json:"stdout"`    ///////// 标准输出
+	Stderr   string `json:"stderr"`    ///////// 错误输出
+	ExitCode int    `json:"exit_code"` ///////// 退出状态码: 0表示成功, 非0表示失败
+	Cwd      string `json:"cwd"`       ///////// 当前目录
+}

+ 56 - 10
utils/shell/executor.go

@@ -1,14 +1,31 @@
+//go:build !windows
+// +build !windows
+
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+
 package shell
 
 import (
+	"os"
 	"path/filepath"
 	"strings"
 	"syscall"
 )
 
 type Executor struct {
-	cwd string        // 当前目录
-	pg  *processGroup // --进程组
+	cwd     string        // 当前目录
+	prevCwd string        // 上次目录
+	pg      *processGroup // --进程组
+}
+
+type PathError struct {
+	Path string
+	Info string
+}
+
+func (e *PathError) Error() string {
+	return e.Info + ": " + e.Path
 }
 
 func NewExecutor() *Executor {
@@ -16,16 +33,17 @@ func NewExecutor() *Executor {
 }
 
 func (e *Executor) Exec(p ExecuteParams) (*ExecuteResult, error) {
-	if isCD(p.Cmd) {
-		dir, err := resolveCD(e.cwd, p.Cmd)
+	if e.isCD(p.Cmd) {
+		dir, err := e.resolveCD(e.cwd, p.Cmd)
 		if err != nil {
 			return &ExecuteResult{
-				Stderr:   err.Error() + "\n",
+				Stderr:   err.Error(),
 				ExitCode: 1,
 			}, nil
 		}
+		e.prevCwd = e.cwd
 		e.cwd = dir
-		return &ExecuteResult{ExitCode: 0}, nil
+		return &ExecuteResult{ExitCode: 0, Cwd: e.cwd}, nil
 	}
 
 	if strings.TrimSpace(p.Cmd) == "pwd" {
@@ -49,23 +67,51 @@ func (e *Executor) Exec(p ExecuteParams) (*ExecuteResult, error) {
 	return result, err
 }
 
-func isCD(cmd string) bool {
+func (e *Executor) isCD(cmd string) bool {
 	s := strings.TrimSpace(cmd)
 	return s == "cd" || strings.HasPrefix(s, "cd ")
 }
 
-func resolveCD(cwd, cmd string) (string, error) {
+func (e *Executor) resolveCD(cwd, cmd string) (string, error) {
 	fields := strings.Fields(cmd)
 	if len(fields) == 1 { // 只有"cd", 没有参数
 		return "/", nil
 	}
 
 	path := fields[1]
+	switch path {
+	case "~":
+		home := os.Getenv("HOME")
+		if home == "" {
+			home = "/"
+		}
+		return home, nil
+	case "-":
+		if e.prevCwd == "" {
+			return cwd, nil
+		}
+		return e.prevCwd, nil
+	}
+
+	var target string
 	if filepath.IsAbs(path) { // 绝对路径
-		return filepath.Clean(path), nil
+		target = filepath.Clean(path)
+	} else { // 相对路径
+		target = filepath.Clean(filepath.Join(cwd, path))
+	}
+
+	info, err := os.Stat(target)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return "", &PathError{Path: target, Info: "directory not exist"}
+		}
+		return "", err
+	}
+	if !info.IsDir() {
+		return "", &PathError{Path: target, Info: "not a directory"}
 	}
 
-	return filepath.Clean(filepath.Join(cwd, path)), nil // 相对路径
+	return target, nil
 }
 
 func (e *Executor) Interrupt() error {