client.go 6.9 KB

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