client.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package main
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "os"
  10. "os/exec"
  11. "os/signal"
  12. "runtime"
  13. "strings"
  14. "sync/atomic"
  15. "syscall"
  16. "time"
  17. "github.com/google/uuid"
  18. "github.com/peterh/liner"
  19. )
  20. const MODULE_NAME = "YFKJ_SSH_CLIENT"
  21. var (
  22. coupler *MQTTCoupler
  23. Version = "1.0.0.1"
  24. BuildTime = "unknown"
  25. ErrBrokerAddressEmpty = errors.New("mqtt server address is empty")
  26. ErrIMEINotAvailable = errors.New("device imei is not available")
  27. )
  28. func main() {
  29. if IsArgsParam("-h") {
  30. help()
  31. return
  32. }
  33. if IsArgsParam("-v") {
  34. fmt.Println("程序版本:", Version, "\n构建时间:", BuildTime)
  35. return
  36. }
  37. devIMEI := GetArgsParamStr("-c", "")
  38. if devIMEI == "" {
  39. help()
  40. return
  41. }
  42. if err := loadAppConfig(); err != nil {
  43. fmt.Printf("[%s] 错误: %v!!\n", MODULE_NAME, err)
  44. return
  45. }
  46. if CfgServers.MQTTSrv.Address == "" {
  47. fmt.Printf("[%s] 错误: %v!!\n", MODULE_NAME, ErrBrokerAddressEmpty)
  48. return
  49. }
  50. ctx, cancel := context.WithCancel(context.Background())
  51. coupler = &MQTTCoupler{
  52. ctx: ctx,
  53. cancel: cancel,
  54. broker: CfgServers.MQTTSrv.Address,
  55. username: CfgServers.MQTTSrv.Username,
  56. password: CfgServers.MQTTSrv.Password,
  57. clientID: uuid.New().String(),
  58. imei: devIMEI,
  59. cwd: "/",
  60. }
  61. if err := coupler.init2(); err != nil {
  62. fmt.Printf("[%s] 错误: %v\n!!", MODULE_NAME, err)
  63. return
  64. }
  65. var pingState atomic.Bool
  66. heartbeatLoop(&pingState) // -启动-设备在线-心跳检测-
  67. for {
  68. if pingState.Load() { //// 等待成功连接上目标设备卍
  69. break
  70. }
  71. fmt.Printf("[%s] 正在尝试连接设备...\n", MODULE_NAME)
  72. time.Sleep(1 * time.Second)
  73. }
  74. term(&pingState) /////////////////// 启动终端模拟器卍
  75. }
  76. func term(pingState *atomic.Bool) {
  77. var executing atomic.Bool // 是否有正在执行中的命令
  78. var interrupted atomic.Bool // 用户是否按键取消了命令
  79. interruptLoop(&executing, &interrupted) // Ctrl+C卍
  80. printWelcome()
  81. line := liner.NewLiner()
  82. defer line.Close()
  83. line.SetCtrlCAborts(false)
  84. line.SetTabCompletionStyle(liner.TabCircular)
  85. historyFile := "history.txt"
  86. var history []string
  87. if f, err := os.Open(historyFile); err == nil {
  88. scanner := bufio.NewScanner(f)
  89. for scanner.Scan() {
  90. cmd := strings.TrimSpace(scanner.Text())
  91. if cmd != "" {
  92. history = append(history, cmd)
  93. line.AppendHistory(cmd)
  94. }
  95. }
  96. f.Close()
  97. }
  98. defer func() {
  99. if f, err := os.Create(historyFile); err == nil {
  100. line.WriteHistory(f)
  101. f.Close()
  102. }
  103. }()
  104. line.SetCompleter(func(input string) []string { // 补全
  105. var matches []string
  106. for _, cmd := range history {
  107. if strings.HasPrefix(cmd, input) {
  108. matches = append(matches, cmd)
  109. }
  110. }
  111. return matches
  112. })
  113. for {
  114. if !pingState.Load() {
  115. fmt.Printf("[%s] 目标设备连接丢失!!\n", MODULE_NAME)
  116. break
  117. }
  118. if interrupted.Swap(false) {
  119. fmt.Println("^C")
  120. }
  121. prompt := fmt.Sprintf("root@%s:%s# ", coupler.imei, coupler.cwd)
  122. input, err := line.Prompt(prompt) /// 等待用户输入指令
  123. if err == nil {
  124. goto inputOK
  125. }
  126. if err == io.EOF { ////////////////// Ctrl+D 按键处理
  127. _, _ = coupler.quit()
  128. fmt.Println("bye")
  129. break
  130. }
  131. fmt.Println("读取用户输入失败:", err)
  132. continue
  133. inputOK:
  134. input = strings.TrimSpace(input)
  135. if input == "" {
  136. continue
  137. }
  138. line.AppendHistory(input) ///// 保存用户输入的历史命令
  139. history = append(history, input) ///// 本次也立刻生效
  140. if input == "exit" || input == "quit" {
  141. _, _ = coupler.quit()
  142. break
  143. }
  144. if input == "clear" {
  145. clearScreen()
  146. continue
  147. }
  148. executing.Store(true)
  149. result, err := coupler.exec(input)
  150. executing.Store(false)
  151. if err != nil {
  152. fmt.Printf("执行错误: %v\n", err)
  153. continue
  154. }
  155. if result.Stdout != "" {
  156. fmt.Print(result.Stdout)
  157. }
  158. if result.Stderr != "" {
  159. fmt.Fprintln(os.Stderr, result.Stderr)
  160. }
  161. if result.ExitCode == 124 {
  162. fmt.Println("command timeout")
  163. }
  164. if result.Cwd != "" {
  165. coupler.cwd = result.Cwd
  166. }
  167. }
  168. }
  169. func help() {
  170. h := `
  171. -h 显示帮助提示
  172. -v 当前程序版本
  173. -c 连接目标设备(IMEI), 例如: -c 869523059113051
  174. `
  175. fmt.Println(h)
  176. }
  177. func interruptLoop(executing *atomic.Bool, interrupted *atomic.Bool) {
  178. sigCh := make(chan os.Signal, 1)
  179. signal.Notify(sigCh, syscall.SIGINT)
  180. go func() {
  181. for range sigCh {
  182. interrupted.Store(true)
  183. if executing.Load() {
  184. _, _ = coupler.stop()
  185. }
  186. }
  187. }()
  188. }
  189. func heartbeatLoop(pingState *atomic.Bool) {
  190. go func() {
  191. ticker := time.NewTicker(1 * time.Second)
  192. defer ticker.Stop()
  193. pingFailCount := 0
  194. pong := ""
  195. for range ticker.C {
  196. resp, err := coupler.ping()
  197. if err == nil && resp.Error == nil && resp.Result != nil &&
  198. json.Unmarshal(resp.Result, &pong) == nil && pong == "pong" {
  199. pingState.Store(true)
  200. pingFailCount = 0
  201. } else {
  202. pingFailCount++
  203. if pingFailCount >= 3 { ///// 连续3次ping失败, 可以认为设备离线
  204. pingState.Store(false)
  205. }
  206. } // end if
  207. } // end for
  208. }()
  209. }
  210. func printWelcome() {
  211. welcome := `
  212. _ _ _ _ _ _ _ _
  213. | \ | (_) | | | (_) | |
  214. | \| |_| |_ ___| |__ _| | | ___
  215. | . | | __/ __| '_ \| | | |/ _ \
  216. | |\ | | || (__| | | | | | | __/
  217. |_| \_|_|\__\___|_| |_|_|_|_|\___|
  218. ══════════════════════════════════
  219. 云飞科技RTU远程运维终端
  220. ══════════════════════════════════
  221. 提示: 输入'quit'命令, 退出终端
  222. `
  223. fmt.Println(welcome)
  224. }
  225. func clearScreen() {
  226. if runtime.GOOS == "windows" {
  227. cmd := exec.Command("cmd", "/c", "cls")
  228. cmd.Stdout = os.Stdout
  229. _ = cmd.Run()
  230. } else {
  231. cmd := exec.Command("clear")
  232. cmd.Stdout = os.Stdout
  233. _ = cmd.Run()
  234. }
  235. }