Explorar el Código

完成web-ui命令终端页面的所有开发

niujiuru hace 1 semana
padre
commit
7e1dff2e6d

+ 2 - 0
go.mod

@@ -3,6 +3,8 @@ module rtu_linux_services
 go 1.24.2
 
 require (
+	github.com/creack/pty v1.1.24
+	github.com/gorilla/websocket v1.5.3
 	github.com/kardianos/service v1.2.4
 	hnyfkj.com.cn/rtu/linux v0.0.0
 )

+ 4 - 0
go.sum

@@ -2,9 +2,13 @@ github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUy
 github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A=
 github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
 github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
 github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

+ 2 - 0
web-ui.service/main.go

@@ -26,9 +26,11 @@ func (p *program) Start(s service.Service) error {
 
 	p.webUI = ui
 
+	fmt.Println("webui starting on http :8080")
 	go func() {
 		if err := ui.Start(); err != nil {
 			fmt.Printf("webui stopped: %v\n", err)
+			os.Exit(1)
 		}
 	}()
 

+ 90 - 0
web-ui.service/page8_handler.go

@@ -0,0 +1,90 @@
+package main
+
+import (
+	"net/http"
+	"os"
+	"os/exec"
+	"syscall"
+
+	"github.com/creack/pty"
+	"github.com/gorilla/websocket"
+)
+
+var sshUpgrader = websocket.Upgrader{
+	CheckOrigin: func(r *http.Request) bool { return true },
+}
+
+func sshWSHandler(w http.ResponseWriter, r *http.Request) {
+	ws, err := sshUpgrader.Upgrade(w, r, nil)
+	if err != nil {
+		return
+	}
+	defer ws.Close()
+
+	//cmd := exec.Command("bash", "-l", "-i")
+	cmd := exec.Command("login")
+	cmd.Env = append(os.Environ(),
+		"TERM=xterm-256color",
+		"LANG=C.UTF-8",
+		"LC_ALL=C.UTF-8",
+		"COLORTERM=truecolor",
+		"TERM_PROGRAM=xterm",
+	)
+	cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
+
+	ptmx, err := pty.Start(cmd)
+	if err != nil {
+		return
+	}
+
+	defer func() {
+		ptmx.Close()
+		if cmd.Process != nil {
+			syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+		}
+	}()
+
+	pty.Setsize(ptmx, &pty.Winsize{Cols: 120, Rows: 40})
+
+	go func() {
+		buf := make([]byte, 4096)
+		for {
+			n, err := ptmx.Read(buf)
+			if n > 0 {
+				if ws.WriteMessage(websocket.TextMessage, buf[:n]) != nil {
+					return
+				}
+			}
+			if err != nil {
+				return
+			}
+		}
+	}()
+
+	for {
+		var msg struct {
+			Type string `json:"type"`
+			Data string `json:"data"`
+			Cols uint16 `json:"cols"`
+			Rows uint16 `json:"rows"`
+		}
+
+		if err := ws.ReadJSON(&msg); err != nil {
+			return
+		}
+
+		switch msg.Type {
+		case "input":
+			if _, err := ptmx.Write([]byte(msg.Data)); err != nil {
+				return
+			}
+		case "resize":
+			if msg.Cols > 0 && msg.Rows > 0 {
+				pty.Setsize(ptmx, &pty.Winsize{
+					Cols: msg.Cols,
+					Rows: msg.Rows,
+				})
+			}
+		}
+	}
+}

+ 3 - 0
web-ui.service/web/app.html

@@ -6,6 +6,7 @@
 <title>河南云飞科技RTU设备管理</title>
 <link rel="stylesheet" href="static/css/app.css">
 <link rel="stylesheet" href="static/css/button.css">
+<link rel="stylesheet" href="static/css/xterm.css">
 </head>
 
 <body>
@@ -47,6 +48,8 @@
     </main>
   </div>
 </div>
+<script src="static/js/xterm.js"></script>
+<script src="static/js/xterm-addon-fit.js"></script>
 <script src="static/js/log-stream.js"></script>
 <script src="static/js/router.js"></script>
 <script src="static/js/app.js"></script>

+ 3 - 5
web-ui.service/web/pages/page8.html

@@ -11,11 +11,9 @@
 
 <body>
 <div class="app">
-  <div class="card">
-    <div class="icon">🚧</div>
-      <h1>页面8</h1>
-      <h1>敬请期待</h1>
-    <p class="subtitle">该功能正在开发中,敬请期待...</p>
+  <div class="card page8-card">
+    <div id="ssh-terminal">
+    </div>
   </div>
 </div>
 </body>

+ 10 - 1
web-ui.service/web/static/css/page8.css

@@ -1 +1,10 @@
-/* reserved */
+.page8-card {
+    padding: 8px;
+    height: calc(100vh - 120px);
+}
+
+#ssh-terminal {
+    width: 100%;
+    height: 100%;
+    background: #111;
+}

+ 209 - 0
web-ui.service/web/static/css/xterm.css

@@ -0,0 +1,209 @@
+/**
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ * @license MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Originally forked from (with the author's permission):
+ *   Fabrice Bellard's javascript vt100 for jslinux:
+ *   http://bellard.org/jslinux/
+ *   Copyright (c) 2011 Fabrice Bellard
+ *   The original design remains. The terminal itself
+ *   has been extended to include xterm CSI codes, among
+ *   other features.
+ */
+
+/**
+ *  Default styles for xterm.js
+ */
+
+.xterm {
+    cursor: text;
+    position: relative;
+    user-select: none;
+    -ms-user-select: none;
+    -webkit-user-select: none;
+}
+
+.xterm.focus,
+.xterm:focus {
+    outline: none;
+}
+
+.xterm .xterm-helpers {
+    position: absolute;
+    top: 0;
+    /**
+     * The z-index of the helpers must be higher than the canvases in order for
+     * IMEs to appear on top.
+     */
+    z-index: 5;
+}
+
+.xterm .xterm-helper-textarea {
+    padding: 0;
+    border: 0;
+    margin: 0;
+    /* Move textarea out of the screen to the far left, so that the cursor is not visible */
+    position: absolute;
+    opacity: 0;
+    left: -9999em;
+    top: 0;
+    width: 0;
+    height: 0;
+    z-index: -5;
+    /** Prevent wrapping so the IME appears against the textarea at the correct position */
+    white-space: nowrap;
+    overflow: hidden;
+    resize: none;
+}
+
+.xterm .composition-view {
+    /* TODO: Composition position got messed up somewhere */
+    background: #000;
+    color: #FFF;
+    display: none;
+    position: absolute;
+    white-space: nowrap;
+    z-index: 1;
+}
+
+.xterm .composition-view.active {
+    display: block;
+}
+
+.xterm .xterm-viewport {
+    /* On OS X this is required in order for the scroll bar to appear fully opaque */
+    background-color: #000;
+    overflow-y: scroll;
+    cursor: default;
+    position: absolute;
+    right: 0;
+    left: 0;
+    top: 0;
+    bottom: 0;
+}
+
+.xterm .xterm-screen {
+    position: relative;
+}
+
+.xterm .xterm-screen canvas {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+.xterm .xterm-scroll-area {
+    visibility: hidden;
+}
+
+.xterm-char-measure-element {
+    display: inline-block;
+    visibility: hidden;
+    position: absolute;
+    top: 0;
+    left: -9999em;
+    line-height: normal;
+}
+
+.xterm.enable-mouse-events {
+    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
+    cursor: default;
+}
+
+.xterm.xterm-cursor-pointer,
+.xterm .xterm-cursor-pointer {
+    cursor: pointer;
+}
+
+.xterm.column-select.focus {
+    /* Column selection mode */
+    cursor: crosshair;
+}
+
+.xterm .xterm-accessibility,
+.xterm .xterm-message {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 10;
+    color: transparent;
+    pointer-events: none;
+}
+
+.xterm .live-region {
+    position: absolute;
+    left: -9999px;
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+}
+
+.xterm-dim {
+    /* Dim should not apply to background, so the opacity of the foreground color is applied
+     * explicitly in the generated class and reset to 1 here */
+    opacity: 1 !important;
+}
+
+.xterm-underline-1 { text-decoration: underline; }
+.xterm-underline-2 { text-decoration: double underline; }
+.xterm-underline-3 { text-decoration: wavy underline; }
+.xterm-underline-4 { text-decoration: dotted underline; }
+.xterm-underline-5 { text-decoration: dashed underline; }
+
+.xterm-overline {
+    text-decoration: overline;
+}
+
+.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
+.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
+.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
+.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
+.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
+
+.xterm-strikethrough {
+    text-decoration: line-through;
+}
+
+.xterm-screen .xterm-decoration-container .xterm-decoration {
+	z-index: 6;
+	position: absolute;
+}
+
+.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
+	z-index: 7;
+}
+
+.xterm-decoration-overview-ruler {
+    z-index: 8;
+    position: absolute;
+    top: 0;
+    right: 0;
+    pointer-events: none;
+}
+
+.xterm-decoration-top {
+    z-index: 2;
+    position: relative;
+}

+ 91 - 0
web-ui.service/web/static/js/page8.js

@@ -1,5 +1,96 @@
+let sshTerm = null;
+let sshFit = null;
+let sshWS = null;
+let sshResizeTimer = null;
+
 function initPage8() {
+  const el = document.getElementById("ssh-terminal");
+
+  if (!el || sshTerm) {
+    return;
+  }
+
+  sshTerm = new Terminal({
+    cursorBlink: true,
+    scrollback: 5000,
+    fontSize: 14,
+    convertEol: true,
+    theme: {
+      background: "#111111"
+    }
+  });
+
+  sshFit = new FitAddon.FitAddon();
+  sshTerm.loadAddon(sshFit);
+  sshTerm.open(el);
+
+  connectSSH();
+
+  sshTerm.onData(data => {
+    if (sshWS?.readyState === WebSocket.OPEN) {
+      sshWS.send(JSON.stringify({
+        type: "input",
+        data: data
+      }));
+    }
+  });
+
+  window.addEventListener("resize", resizeSSH);
+
+  resizeSSH();
+}
+
+function connectSSH() {
+  sshWS = new WebSocket(
+    "ws://" + location.host + "/api/ssh/ws"
+  );
+
+  sshWS.onopen = resizeSSH;
+
+  sshWS.onmessage = e => {
+    sshTerm?.write(e.data);
+  };
+
+  sshWS.onclose = () => {
+    sshWS = null;
+  };
+}
+
+function resizeSSH() {
+  if (!sshFit || !sshTerm) {
+    return;
+  }
+
+  clearTimeout(sshResizeTimer);
+
+  sshResizeTimer = setTimeout(() => {
+
+    sshFit.fit();
+
+    if (sshWS?.readyState === WebSocket.OPEN) {
+      sshWS.send(JSON.stringify({
+        type: "resize",
+        cols: sshTerm.cols,
+        rows: sshTerm.rows
+      }));
+    }
+
+  }, 100);
 }
 
 function exitPage8() {
+  window.removeEventListener(
+    "resize",
+    resizeSSH
+  );
+
+  clearTimeout(sshResizeTimer);
+
+  sshWS?.close();
+  sshWS = null;
+
+  sshTerm?.dispose();
+  sshTerm = null;
+
+  sshFit = null;
 }

+ 4 - 1
web-ui.service/web/static/js/router.js

@@ -22,7 +22,10 @@ const pageHandlers = {
   page5: {},
   page6: {},
   page7: {},
-  page8: {},
+  page8: {
+    init: () => window.initPage8?.(),
+    exit: () => window.exitPage8?.()
+  },
 };
 
 function clearOldScripts() {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2 - 0
web-ui.service/web/static/js/xterm-addon-fit.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2 - 0
web-ui.service/web/static/js/xterm.js


+ 5 - 0
web-ui.service/web_route.go

@@ -67,6 +67,11 @@ var routes = []Route{
 		"/api/network/status",
 		networkStatusHandler,
 	},
+
+	{
+		"/api/ssh/ws",
+		sshWSHandler,
+	},
 }
 
 func registerRoutes(mux *http.ServeMux) {