├── test_loop.sh ├── cmdsFile ├── readme_source └── demo.gif ├── test_go_exec.sh ├── config.json ├── log.go ├── pid.go ├── webpage.go ├── README.md ├── main.go └── cmdsocket.go /test_loop.sh: -------------------------------------------------------------------------------- 1 | while [ 1 ]; do 2 | echo 1 1>/dev/null 3 | done -------------------------------------------------------------------------------- /cmdsFile: -------------------------------------------------------------------------------- 1 | pp:ps 2 | hi:echo hihi 3 | testloop:sh test_loop.sh 4 | errorsh:asdfff 5 | errorparams:ls --error -------------------------------------------------------------------------------- /readme_source/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spxvszero/go_shell_socket/HEAD/readme_source/demo.gif -------------------------------------------------------------------------------- /test_go_exec.sh: -------------------------------------------------------------------------------- 1 | echo "shell: try to test go" 2 | while [[ 1 ]]; do 3 | #statements 4 | read a 5 | echo "shell: Your INPUT is $a" 6 | done 7 | echo "shell: finished" 8 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port":18080, 3 | "Cmd_config": { 4 | "web_url": "/cmdsocket", 5 | "socket_url": "/ws", 6 | "cmds_file": "cmdsFile", 7 | "cmds": [ 8 | { 9 | "exec_alias": "ll", 10 | "cmd": "ls", 11 | "param": "-l" 12 | }, 13 | { 14 | "exec_alias": "shtest", 15 | "cmd": "sh", 16 | "param": "test_go_exec.sh" 17 | }, 18 | { 19 | "exec_alias": "pinggithub", 20 | "cmd": "ping", 21 | "param": "github.com" 22 | }, 23 | { 24 | "exec_alias": "cshow", 25 | "cmd": "cat", 26 | "param": "config.json" 27 | } 28 | ], 29 | "exit_string": "exit" 30 | } 31 | } -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var ( 11 | Trace *log.Logger 12 | Info *log.Logger 13 | Warning *log.Logger 14 | Error *log.Logger 15 | Debug *log.Logger 16 | ) 17 | 18 | type LogMode int 19 | const ( 20 | DebugMode LogMode = iota 21 | ReleaseMode 22 | ) 23 | 24 | var Mode = DebugMode 25 | 26 | func init() { 27 | defaultLogger(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr,os.Stdout) 28 | } 29 | 30 | func defaultLogger( 31 | traceHandle io.Writer, 32 | infoHandle io.Writer, 33 | warningHandle io.Writer, 34 | errorHandle io.Writer, 35 | debugHandle io.Writer) { 36 | 37 | Trace = log.New(traceHandle, 38 | "TRACE: ", 39 | log.Ldate|log.Ltime|log.Lshortfile) 40 | 41 | Info = log.New(infoHandle, 42 | "INFO: ", 43 | log.Ldate|log.Ltime|log.Lshortfile) 44 | 45 | Warning = log.New(warningHandle, 46 | "WARNING: ", 47 | log.Ldate|log.Ltime|log.Lshortfile) 48 | 49 | Error = log.New(errorHandle, 50 | "ERROR: ", 51 | log.Ldate|log.Ltime|log.Lshortfile) 52 | 53 | Debug = log.New(debugHandle, 54 | "DEBUG: ", 55 | log.Ldate|log.Ltime|log.Lshortfile) 56 | } 57 | 58 | func SetLogMode(mode LogMode) { 59 | Mode = mode 60 | } 61 | 62 | func SetWriter(w io.Writer) { 63 | Trace.SetOutput(w) 64 | Info.SetOutput(w) 65 | Warning.SetOutput(w) 66 | Error.SetOutput(w) 67 | if Mode == DebugMode { 68 | Debug.SetOutput(w) 69 | }else { 70 | Debug.SetOutput(ioutil.Discard) 71 | } 72 | } -------------------------------------------------------------------------------- /pid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/signal" 9 | "strconv" 10 | "syscall" 11 | ) 12 | 13 | var pid_file_name = "cmdsocket.pid" 14 | 15 | var pid_log_prefix = "PID : " 16 | 17 | func setupPidModule() { 18 | writePidFile() 19 | signalReceive() 20 | } 21 | 22 | func writePidFile() { 23 | pid := os.Getpid() 24 | file, err := os.Create(pid_file_name) 25 | if err != nil { 26 | Error.Println("create pid file :", "error : ",err) 27 | return 28 | } 29 | defer file.Close() 30 | 31 | pidstr := fmt.Sprint(pid) 32 | _,err = io.WriteString(file,pidstr) 33 | if err != nil { 34 | Error.Println("write config file error : ",err) 35 | return 36 | } 37 | Info.Println("Finished! Pid ",pidstr," successfully generated.") 38 | } 39 | 40 | func readPidFromFile() int{ 41 | buf,err := ioutil.ReadFile(pid_file_name) 42 | if err != nil { 43 | logPid("read pid file error",err) 44 | return -1 45 | } 46 | pid,err := strconv.Atoi(string(buf)) 47 | if err !=nil { 48 | logPid("string convert err :",err) 49 | return -1 50 | } 51 | return pid 52 | } 53 | 54 | func signalReceive() { 55 | sigc := make(chan os.Signal, 1) 56 | signal.Notify(sigc, 57 | syscall.SIGHUP) 58 | go func() { 59 | for s := range sigc { 60 | // reload cmd from cmd file 61 | logPid("get signal ",s) 62 | serializeCmds(UserConfig.Cmd) 63 | } 64 | }() 65 | 66 | } 67 | 68 | func sendSignal() { 69 | pid := readPidFromFile() 70 | if pid <= 0{ 71 | logPid("pid num small ") 72 | return 73 | } 74 | proc,err := os.FindProcess(pid) 75 | if err != nil { 76 | logPid("process not found ",err) 77 | return 78 | } 79 | err = proc.Signal(syscall.SIGHUP) 80 | if err != nil { 81 | logPid("send signal err ",err) 82 | }else { 83 | logPid("send signal success") 84 | } 85 | } 86 | 87 | func logPid(v ...interface{}) { 88 | Debug.Println(pid_log_prefix,v) 89 | } -------------------------------------------------------------------------------- /webpage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const CmdSocketHTMLPage = ` 4 | 5 | 6 | 7 | 8 | 9 | Cmd Socket 10 | 11 | 25 | 26 | 27 | 28 |

Cmd Socket Go

29 |

30 | 
31 | 
32 | 33 | 34 | 35 |
36 | 37 | 78 | 79 | 80 | 81 | 82 | 83 | ` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go_shell_socket 2 | 3 | run shell scripts by websocket with go lauguage 4 | 5 | 一个在 web 端执行服务器指令的小工具。 6 | 7 | ## Usage 8 | #### Build yourself 9 | 10 | * pull project 11 | * get [gin](https://github.com/gin-gonic/gin) and [websocket](https://github.com/gorilla/websocket) with `go get` 12 | * config `config.json` file 13 | * build it with `go build` 14 | * open browser with config url and have fun 15 | 16 |
17 | 18 | * 下载代码或者直接用 git 拉取过来 19 | * 通过 go get 装好 [gin](https://github.com/gin-gonic/gin) 和 [websocket](https://github.com/gorilla/websocket) 20 | * 配置 config.json 配置文件 21 | * 然后就可以通过 go build 编译了,跨平台编译根据官网的说明来就好了,没有特殊配置 22 | * 然后 go run 执行,就可以打开网页测试了 23 | 24 | 25 | #### Use Release File 26 | 27 | Sample For Linux : 28 | ``` 29 | $ curl -O -L https://github.com/spxvszero/go_shell_socket/releases/download/v1.0/go_shell_socket_linux 30 | $ chmod +x go_shell_socket_linux 31 | 32 | #generate config.json file 33 | $ ./go_shell_socket_linux --generate config.json 34 | 35 | #edit config.json 36 | #run in background if config.json when in same path 37 | $ ./go_shell_socket_linux & 38 | 39 | ``` 40 | 41 | 42 | ## Example 43 | 44 | ![demo](readme_source/demo.gif) 45 | 46 | ## Command For Application 47 | 48 | * `--help` to see what commands are supported. 49 | * `--generate [file_path]` to generate a sample config.json file. 50 | * `--signal reload` to reload command file which config in parameter `cmds_file` while server is running. 51 | 52 | 其实总共就两个指令 53 | 54 | * help 指令是 golang 的 flag 包自带的。 55 | * generate 指令会生成一个 config.json 文件,方便配置。 56 | * signal 指令只接受 reload 参数,可以重新加载 cmds_file 配置好的文本文件。 57 | 58 | ## Config 59 | 60 | This is config.json which example used. Don't copy to your own config.json directly. 61 | 62 | 这个配置文件仅用做说明,不要直接拷贝使用。 63 | 64 | ``` 65 | { 66 | //web page open port 67 | //网页打开的端口 68 | "port":18080, 69 | 70 | "Cmd_config": { 71 | 72 | //web socket page url 73 | //自带的网页地址 74 | "web_url": "/cmdsocket", 75 | 76 | //socket url 77 | //websocket的地址 78 | "socket_url": "/ws", 79 | 80 | //addition command files on server which can be reloaded without restart go program 81 | //本地定义的一个文本文件,与 cmds 区别就是,这个文件可以通过此程序的 --signal reload 重载配置,而不需要重启程序 82 | "cmds_file": "cmdsFile", 83 | 84 | //inner cmds which loaded on first boot 85 | //这个字段的内容在启动的时候加载,不支持 reload 重载 86 | "cmds": [ 87 | { 88 | 89 | //alias for actual command 90 | //指令的别名,web中仅能通过这个访问指令 91 | "exec_alias": "ll", 92 | 93 | //command 94 | //实际运行的指令 95 | "cmd": "ls", 96 | 97 | //param not supported long params or pipe, use script file instead 98 | //指令的参数,不支持管道和过长的指令,如果有需要,建议放在脚本中执行,这里只执行脚本 99 | "param": "-l" 100 | }, 101 | { 102 | "exec_alias": "shtest", 103 | "cmd": "sh", 104 | "param": "test_go_exec.sh" 105 | }, 106 | { 107 | "exec_alias": "pinggithub", 108 | "cmd": "ping", 109 | "param": "github.com" 110 | }, 111 | { 112 | "exec_alias": "cshow", 113 | "cmd": "cat", 114 | "param": "config.json" 115 | } 116 | ], 117 | 118 | //exit current running cmd 119 | //用来中止当前运行的指令 120 | "exit_string": "exit" 121 | } 122 | } 123 | ``` 124 | 125 | ## Format of cmds_file 126 | 127 | `[exec_alias]:[cmd] [param]` 128 | 129 | It easy to add new command. 130 | 131 | Look how to add a `ps` command. 132 | 133 | ``` 134 | #server is running in background, and there is a cmdsFile which i add path to config.json. 135 | #add cmd 136 | $ echo "ppps:ps aux" >> cmdsFile 137 | 138 | #send signal to server 139 | $ ./go_shell_socket_linux --signal reload 140 | ``` 141 | 142 | ## Something more 143 | 144 | * This program does not support any text editor such as `vim`. 145 | * It works fin in my macOS Catalina and CentOS 8, and i am not sure about other system. 146 | * `param` in config file will go through to `exec.Command`, so it does not support complicated command. If you want ,put command in shell script and run with `sh`. 147 | * ssh command will stuck in goroutine which cannot be kill,but why use it. 148 | * nginx do not support websocket default, if using nginx, see how to config it in this [site](https://nginx.org/en/docs/http/websocket.html). 149 | * This program will generate a log file and a pid file while running. 150 | 151 |
152 | 153 | * 这个小工具不支持文本编辑了,不要把它当作一个终端来用,写这个的目的只是为了方便某些不适合登录终端的场景了,要做复杂的任务还是老老实实登入终端吧。 154 | * 这个程序我在 mac 上和 centos8 上测试是正常的,windows 我也试了,交互正常,不过命令我用的少就没怎么测试了。 155 | * 不支持多参数主要自己不太需要,就没有写了,如果有需要可以自己修改 `exec` 执行的形式。 156 | * 还有 ssh 命令是会有问题的,开启的进程没办法正常 kill 掉,不晓得什么原因,不过通过一些方式修改之后,应该有办法实现的吧。 157 | * 通过 nginx 反向代理需要一些特殊配置才能支持 websocket 了,可以看[官网](https://nginx.org/en/docs/http/websocket.html)的描述。 158 | * 这个工具在运行的时候会生成一个 log 文件和一个 pid 文件。 159 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "github.com/gin-gonic/gin" 7 | "html/template" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | ) 15 | 16 | var generateConfig = flag.String("generate","", 17 | "generate sample config.json file, Example '--generate config.json' will generate config file in current directory ") 18 | var signalFlag = flag.String("signal","","reload") 19 | 20 | type ConfigFileStruct struct { 21 | Port int `json:"port"` 22 | Cmd CmdRoute `json:"cmd_config"` 23 | } 24 | 25 | var UserConfig = ConfigFileStruct{} 26 | 27 | type OUTPUT_MODE int 28 | const ( 29 | ModeDebug OUTPUT_MODE = iota 30 | ModeRelease 31 | ) 32 | 33 | var GO_OUTPUT_MODE OUTPUT_MODE 34 | var logFile = "go_log.txt" 35 | 36 | func main() { 37 | GO_OUTPUT_MODE = ModeRelease 38 | 39 | checkFlag() 40 | 41 | //pid listen module 42 | setupPidModule() 43 | logConfig() 44 | 45 | readConfig() 46 | ginSetup() 47 | } 48 | 49 | func checkFlag() { 50 | flag.Parse() 51 | 52 | if len(*generateConfig) > 0 { 53 | defer os.Exit(0) 54 | configPath := *generateConfig 55 | 56 | file, err := os.Create(configPath) 57 | if err != nil { 58 | Error.Println("create config file : ", configPath,"error : ",err) 59 | return 60 | } 61 | defer file.Close() 62 | 63 | _,err = io.WriteString(file,getSampleConfigJsonString()) 64 | if err != nil { 65 | Error.Println("write config file error : ",err) 66 | return 67 | } 68 | Info.Println("Finished! ",configPath," file successfully generated.") 69 | } 70 | if len(*signalFlag) > 0 { 71 | if *signalFlag == "reload" { 72 | sendSignal() 73 | os.Exit(0) 74 | } 75 | } 76 | } 77 | 78 | func ginSetup() { 79 | 80 | if GO_OUTPUT_MODE == ModeDebug { 81 | gin.SetMode(gin.DebugMode) 82 | }else { 83 | gin.SetMode(gin.ReleaseMode) 84 | } 85 | 86 | route := gin.Default() 87 | //pprof.Register(route,"debug/pprof") 88 | 89 | port := 8888 90 | configRoute(route) 91 | 92 | if &UserConfig != nil && UserConfig.Port > 0 { 93 | port = UserConfig.Port 94 | } 95 | portStr := ":"+strconv.Itoa(port) 96 | Info.Println("Server Open on Port ",port) 97 | err := route.Run(portStr) 98 | if err != nil { 99 | Error.Println("Server Open Failed :",err) 100 | } 101 | } 102 | 103 | func configRoute(route *gin.Engine) { 104 | 105 | //play routes config 106 | if &UserConfig == nil { 107 | Error.Println("Read Route Config Failed") 108 | return; 109 | } 110 | Info.Println("Read Route Config Success") 111 | 112 | //config cmd websocket route 113 | configRoute_cmd_websocket(route) 114 | } 115 | 116 | func configRoute_cmd_websocket(route gin.IRouter){ 117 | if len(UserConfig.Cmd.Web_url) > 0 { 118 | //download web page path 119 | route.GET(UserConfig.Cmd.Web_url, func(c *gin.Context) { 120 | 121 | cmdInfoPath := struct { 122 | Socket_Path string 123 | }{UserConfig.Cmd.Socket_url} 124 | 125 | tmpl, err := template.New("socket_page").Parse(CmdSocketHTMLPage) 126 | 127 | if err != nil { 128 | Error.Println("Cmd Socket Web Page Tmpl Err ", err); 129 | c.String(http.StatusNotFound,"something is wrong in this page") 130 | }else { 131 | c.Status(200) 132 | tmpl.Execute(c.Writer,cmdInfoPath) 133 | } 134 | }) 135 | 136 | Info.Println("Build Cmd Socket") 137 | route.GET(UserConfig.Cmd.Socket_url, func(c *gin.Context) { 138 | cmdWebSocketHandler(c.Writer,c.Request) 139 | }) 140 | } 141 | } 142 | 143 | func readConfig() { 144 | filePossiblePath := "config.json" 145 | UserConfig = *readConfigFile(filePossiblePath) 146 | if &UserConfig == nil { 147 | filePossiblePath = "config/config.json" 148 | UserConfig = *readConfigFile(filePossiblePath) 149 | } 150 | 151 | //serailize cmd config 152 | serializeCmds(UserConfig.Cmd) 153 | } 154 | 155 | func readConfigFile(filePath string) *ConfigFileStruct { 156 | 157 | res := ConfigFileStruct{} 158 | 159 | fileBlob,fileErr := ioutil.ReadFile(filePath) 160 | 161 | if fileErr != nil { 162 | Error.Println("Config Read Error : ",fileErr) 163 | return nil 164 | } 165 | 166 | json.Unmarshal(fileBlob,&res) 167 | return &res 168 | } 169 | 170 | func logConfig() { 171 | f,err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 172 | if err != nil { 173 | Error.Println("Open Log File Failed : ",err); 174 | return; 175 | } 176 | w := io.MultiWriter(os.Stdout,f) 177 | gin.DefaultWriter = w 178 | 179 | if GO_OUTPUT_MODE == ModeDebug { 180 | SetLogMode(DebugMode) 181 | }else { 182 | SetLogMode(ReleaseMode) 183 | } 184 | 185 | SetWriter(w) 186 | 187 | Info.Println("Log File Path : ",filepath.Dir(os.Args[0]) + "/" + logFile) 188 | } 189 | 190 | func getSampleConfigJsonString() string { 191 | return ` 192 | { 193 | "port":18080, 194 | "Cmd_config": { 195 | "web_url": "/cmdsocket", 196 | "socket_url": "/ws", 197 | "cmds_file": "cmdsFile", 198 | "cmds": [ 199 | { 200 | "exec_alias": "pinggithub", 201 | "cmd": "ping", 202 | "param": "github.com" 203 | } 204 | ], 205 | "exit_string": "exit" 206 | } 207 | } 208 | 209 | 210 | 211 | ` 212 | } -------------------------------------------------------------------------------- /cmdsocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "github.com/gorilla/websocket" 7 | "io" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type CmdRoute struct { 16 | Web_url string `json:"web_url"` 17 | Socket_url string `json:"socket_url"` 18 | Cmds []CmdConfig `json:"cmds"` 19 | CmdsFile string `json:"cmds_file"` 20 | Exit string `json:"exit_string"` 21 | } 22 | type CmdConfig struct { 23 | Alias string `json:"exec_alias"` 24 | Cmd string `json:"cmd"` 25 | Param string `json:"param"` 26 | } 27 | 28 | type SocketSafeWriter struct { 29 | Conn *websocket.Conn 30 | mu sync.Mutex 31 | } 32 | func (sw *SocketSafeWriter) SendMsg(msg []byte) { 33 | sw.mu.Lock() 34 | defer sw.mu.Unlock() 35 | if sw.Conn != nil { 36 | sw.Conn.WriteMessage(websocket.TextMessage,msg) 37 | } 38 | } 39 | 40 | var cmdsocket_log_prefix = "CMD Socket : " 41 | 42 | var cmds_map map[string]CmdConfig 43 | 44 | func serializeCmds(route CmdRoute) { 45 | cmds_map = make(map[string]CmdConfig) 46 | //scan file 47 | if len(route.CmdsFile) > 0 { 48 | file,err := os.Open(route.CmdsFile) 49 | if err != nil { 50 | Error.Println(cmdsocket_log_prefix,"Open cmdsfile error : ",err) 51 | }else { 52 | scanner := bufio.NewScanner(file) 53 | for scanner.Scan() { 54 | str := scanner.Text() 55 | splitIdx := strings.Index(str,":") 56 | if splitIdx > 0 { 57 | alias := str[0:splitIdx] 58 | blankSplitIdx := strings.Index(str," ") 59 | if blankSplitIdx > 0{ 60 | cmdStr := str[splitIdx+1:blankSplitIdx] 61 | paramStr := str[blankSplitIdx+1:] 62 | cmds_map[alias] = CmdConfig{Alias: alias,Cmd: cmdStr,Param: paramStr} 63 | logCmdsocket("Add cmd ",alias," ",cmdStr," ",paramStr) 64 | }else { 65 | cmdStr := str[splitIdx+1:] 66 | if len(cmdStr) > 0 { 67 | cmds_map[alias] = CmdConfig{Alias: alias,Cmd: cmdStr,Param: ""} 68 | logCmdsocket("Add cmd ",alias," ",cmdStr) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | //config file 76 | for _,cmd := range route.Cmds { 77 | cmds_map[cmd.Alias] = cmd 78 | logCmdsocket("Add cmd ",cmd.Alias," ",cmd) 79 | } 80 | } 81 | 82 | var wsupgrader = websocket.Upgrader{ 83 | ReadBufferSize: 1024, 84 | WriteBufferSize: 1024, 85 | CheckOrigin: func(r *http.Request) bool { 86 | return true 87 | }, 88 | } 89 | func cmdWebSocketHandler(w http.ResponseWriter, r *http.Request) { 90 | conn, err := wsupgrader.Upgrade(w, r, nil) 91 | if err != nil { 92 | Error.Println(cmdsocket_log_prefix,"Failed to set websocket upgrade: %+v", err) 93 | return 94 | } 95 | safeSocket := SocketSafeWriter{Conn: conn} 96 | 97 | remoteChan := make(chan string); 98 | okChan := make(chan bool) 99 | getCmd := false 100 | ctx,cancel := context.WithCancel(context.Background()) 101 | defer close(remoteChan) 102 | defer close(okChan) 103 | for { 104 | _,msg, err := conn.ReadMessage() 105 | if err != nil { 106 | break 107 | } 108 | select { 109 | case c:= <-okChan: 110 | getCmd = c 111 | default: 112 | } 113 | if len(string(msg)) > 0 { 114 | //if reading cmd, socket string will gothrought cmd interactive without make a new cmd. 115 | if getCmd { 116 | remoteChan <- string(msg) 117 | continue 118 | } 119 | logCmdsocket("recieve ",string(msg)) 120 | //get str 121 | if cmd := seekCmdFromConfig(string(msg));cmd!=nil { 122 | excmd(ctx,*cmd,&safeSocket,remoteChan,&okChan) 123 | safeSocket.SendMsg([]byte("CMD open success, put any key go on")) 124 | } else { 125 | safeSocket.SendMsg( msg) 126 | } 127 | } 128 | } 129 | 130 | //read last okchan which still waiting to receive before closed 131 | select { 132 | case c:= <-okChan: 133 | logCmdsocket("no use ",c) 134 | default: 135 | } 136 | okChan = nil 137 | remoteChan = nil 138 | safeSocket.Conn = nil 139 | cancel() 140 | logCmdsocket("Socket Handle Closed !") 141 | } 142 | 143 | func seekCmdFromConfig(alias string) *CmdConfig { 144 | cmd := cmds_map[alias] 145 | if len(cmd.Alias) > 0 { 146 | return &cmd 147 | } 148 | return nil 149 | } 150 | 151 | func writeInputToCmd(cmd *exec.Cmd,file *io.WriteCloser,stringChan chan string) { 152 | 153 | for puts := range stringChan{ 154 | logCmdsocket(">>> ",puts) 155 | if puts == UserConfig.Cmd.Exit { 156 | err := cmd.Process.Kill() 157 | if err!=nil { 158 | logCmdsocket("kill process error :",err) 159 | } 160 | break; 161 | } 162 | wNum,err := io.WriteString(*file, puts + "\n") 163 | logCmdsocket("write length ",wNum) 164 | if err!=nil { 165 | logCmdsocket("write failed : ",err) 166 | break; 167 | } 168 | } 169 | //check cmd if still running 170 | logCmdsocket("cmd process ",cmd.ProcessState) 171 | if cmd.ProcessState == nil && cmd.Process != nil { 172 | logCmdsocket("cmd still running, close it") 173 | cmd.Process.Kill() 174 | } 175 | logCmdsocket("goroutine write loop closed ") 176 | } 177 | func readOutputFromCmd(cmd *exec.Cmd,file *io.PipeReader, safeSocket *SocketSafeWriter) { 178 | logCmdsocket("output pipe file :",*file) 179 | 180 | //bufio.NewScanner is more easier than bufio.NewReader(),and won't cause loop leak 181 | scanner := bufio.NewScanner(file) 182 | for scanner.Scan() { 183 | str := scanner.Text() 184 | logCmdsocket("read cmd script msg : ",str) 185 | if len(str) > 0 { 186 | if safeSocket.Conn == nil { 187 | break 188 | } 189 | safeSocket.SendMsg([]byte(str)) 190 | logCmdsocket("socket : ",safeSocket.Conn) 191 | } 192 | } 193 | //check cmd if still running 194 | logCmdsocket("cmd process ",cmd.ProcessState) 195 | if cmd.ProcessState == nil && cmd.Process != nil { 196 | logCmdsocket("cmd still running, close it") 197 | cmd.Process.Kill() 198 | } 199 | logCmdsocket("goroutine read loop closed ") 200 | } 201 | func startCmdScripts(ctx context.Context, config CmdConfig,safeSocket *SocketSafeWriter, inputChan chan string, okChan *chan bool) { 202 | logCmdsocket("ready for send cmd ",config.Cmd) 203 | *okChan <- true 204 | 205 | var cmd *exec.Cmd 206 | if len(config.Param) > 0{ 207 | cmd = exec.Command(config.Cmd,config.Param) 208 | }else { 209 | cmd = exec.Command(config.Cmd) 210 | } 211 | 212 | pf,perr := cmd.StdinPipe() 213 | if perr != nil { 214 | logCmdsocket("pipe err ",perr) 215 | } 216 | defer pf.Close() 217 | 218 | go writeInputToCmd(cmd,&pf,inputChan) 219 | 220 | r,w := io.Pipe() 221 | cmd.Stdout = w 222 | cmd.Stderr = w 223 | defer r.Close() 224 | defer w.Close() 225 | go readOutputFromCmd(cmd,r,safeSocket) 226 | 227 | select { 228 | case <-ctx.Done(): 229 | logCmdsocket("cmd context forcus finished, skip cmd ",config.Cmd) 230 | default: 231 | err := cmd.Run() 232 | if err!=nil { 233 | logCmdsocket("cmd failed ",err) 234 | safeSocket.SendMsg([]byte("CMD Error: "+err.Error())) 235 | } 236 | 237 | safeSocket.SendMsg([]byte("CMD Finished!")) 238 | } 239 | 240 | logCmdsocket("goroutine cmd exit") 241 | //without goroutine, okchan will stuck this routine until next okchan read 242 | go func() { 243 | if *okChan != nil{ 244 | *okChan <- false 245 | } 246 | logCmdsocket("goroutine okchan finish!") 247 | }() 248 | } 249 | func excmd(ctx context.Context,config CmdConfig, safeSocket *SocketSafeWriter, cmdChan chan string, okChan *chan bool) { 250 | go startCmdScripts(ctx,config,safeSocket,cmdChan,okChan) 251 | } 252 | 253 | func logCmdsocket(v ...interface{}) { 254 | Debug.Println(cmdsocket_log_prefix,v) 255 | } --------------------------------------------------------------------------------