Writing a web terminal emulator on Go using Websocket

    What will we write


    In the last article, we wrote a simple terminal emulator in PHP. I think now is the time to write something more serious on web sockets. What language to use for working with web sockets ..? Python..? Ruby ..? JavaScript ..? Not! Since Go 1 has been released, let's write on it;). I will try not to repeat myself and not write the whole code here. I will give only interesting, in my opinion, fragments.

    Demo


    Thanks to Aleks_ja for the opportunity to see the terminal emulator in action (you need a browser with the latest version of web sockets - for example, Firefox 11 or the latest Chrome). The first time it may not connect, if the daemon does not have time to start in 100 ms, try refreshing the page first.



    The source code for the web terminal is available on the github . It is necessary to compile the web socket daemon on your own (by the team go build) - this is a small measure of protection against those who like to “crack” hosters;).

    Ingredients


    So, we need:

    • Installed Go 1 compiler
    • Websocket ( go get code.google.com/p/go.net/websocket) library
    • A browser that supports the latest Websocket specification (e.g. latest Firefox and Chrome)
    • Any web server with PHP (for automatically starting the daemon)


    Writing a demo web socket


    In our daemon, a web socket server and a pseudo-terminal emulator will be combined. Despite the fact that there is no native support for working with pseudo-terminals in Go, this language is easily integrated with C, so we will use the corresponding calls in C to work directly with the pseudo-terminal.

    Work with web sockets

    package main
    import (
    	"code.google.com/p/go.net/websocket"
    	"http"
    	"log"
    )
    // наша функция-обработчик соединений
    func PtyServer(ws *websocket.Conn) {
    // ws — это название переменной типа *websocket.Conn, поддерживает
    // простые вызовы Read() и Write() для чтения/записи в сокет
    }
    func main() {
    	http.Handle("/ws", websocket.Handler(PtyServer)) // обрабатываем запросы на "/ws" как вебсокет
    	log.Fatal(http.ListenAndServe(":12345", nil)) // слушаем на порту 12345
    }
    


    Work with pseudo-terminal

    Let's write a binding for forkpty () and ioctl () (in ioctl () we will change the size of the “window” of the terminal): Go, although it integrates well with C, does not understand that pid_t and int are the same thing, as well Does not know how to work with a variable number of parameters in C functions.

    package main
    /*
    #cgo LDFLAGS: -lutil
    #include 
    #include 
    #...зависимые от системы заголовки и флаги...
    int goForkpty(int *amaster, struct winsize *winp) {
    	return forkpty(amaster, NULL, NULL, winp);
    }
    int goChangeWinsz(int fd, struct winsize *winp) {
    	return ioctl(fd, TIOCSWINSZ, winp);
    }
    */
    import "C"
    


    In the handler, we use this:

    func PtyServer(ws *websocket.Conn) {
    	cols, rows := 80, 24 // множественное присваивание с авто-типом int
    	var winsz = new(C.struct_winsize) // ещё можно написать "var name = ..." — это то же самое
    	winsz.ws_row = C.ushort(rows); // работа с сишными структурами подразумевает соответствующее приведение типа
    	winsz.ws_col = C.ushort(cols);
    	winsz.ws_xpixel = C.ushort(cols * 9);
    	winsz.ws_ypixel = C.ushort(rows * 16);
    	cpttyno := C.int(-1)
    	pid := int(C.goForkpty(&cpttyno, winsz))
    	pttyno := int(cpttyno)
    	// ...
    }
    


    Communication between the web socket and the pseudo-terminal

    Next, we need to run, for example, bash and send the output from the corresponding descriptor (pttyno) to the web socket, and vice versa, sending input from the web socket to the input pttyno is simple. The problem arises when an incomplete UTF-8 sequence comes to us from the pseudo-terminal. We can read from the pseudo-terminal only in blocks (say, 2 Kb) and the end of the block can “cut” the UTF-8 character into 2 parts - this “trim” should not be sent to the browser, otherwise it will simply ignore this fragment. Here is a small piece of code that correctly handles this situation:

    for end = buflen - 1; end >= 0; end-- { // синтаксис циклов в Go не требует скобок
    	if utf8.RuneStart(buf[end]) { // также как и условий...
    		ch, width := utf8.DecodeRune(buf[end:buflen])
    		if ch != utf8.RuneError {
    			end += width
    		}
    		break
    	}
    }
    


    We must find the byte at the end of the buffer (buf), which can serve as the beginning of a UTF-8 character (in Go terminology - rune), and then see if this character is intact. If everything is fine with the last character, then we return the “end” of the buffer back, otherwise we reduce the size of the buffer so that only whole characters remain there.

    Display the output from the pseudo-terminal in the browser


    At first, I used JSLinux to display the output, but its author does not allow the modification and distribution of the code of my libraries, so let's take the selectel / pyte library written by comrades from Selectel ... Wait a minute, it's on python :(! Another dependency is useless to us, let's we will rewrite it on Javascript :)! The port from the python is not perfect, besides, I'm not a special connoisseur of python, but it does its work - Midnight Commander starts and works without problems.

    Accept user input


    In order to accept user input, I still borrowed a certain amount of code from the author of JSLinux, the basic principles are described here . I also added the ability to insert some text in the input field below (for example, passwords) and added mappings for the F1 - F12 keys, as well as for Alt + (left / right arrow). As it turned out, the values ​​of the input characters for F-keys depend on the environment variable $ TERM and are not defined for vt100 at all, since they were not on the keyboard in VT100 :). Since pyte is used for output, the $ TERM environment variable must be equal to linux, therefore we will use the mapping of these keys for this terminal.

    We launch the demon "on demand"


    I implemented the web socket daemon in such a way that it exits one minute after the last connection, so it would be convenient if the script itself launched the web socket daemon when we open the page with the terminal. The PHP code for this is very simple:
    >ws.log 2>&1 &');
    


    If you don’t know what it is exec, I’ll explain: this is a special builtin-command in any UNIX shell that forces the shell to replace itself with the called process. That is, we will not have a "superfluous" process hanging sh -c ./ws ....

    Moments I Didn't Tell About


    I did not talk about the following implementation details :
    • the client communication protocol was a little complicated to support window resize , but a bug appeared with the input of Russian letters
    • the web daemon is password protected, which is generated automatically at startup and is used when connecting
    • uses its bashrc to set the desired terminal settings
    • since there is no rendering on the server, only bytes are sent, the daemon loads the server comparable to sshd (i.e. CPU load is close to zero)
    • javascript's pyte implementation is exceptionally fast: there is no visible delay when Midnight Commander starts, throughput is several thousand lines of text per second
    • when closing the browser window, the session ends correctly
    • one daemon can serve many clients at the same time without problems
    • by using a tag


    Github project


    For those interested, I’ll repeat the links to the github:
    github.com/YuriyNasretdinov/WebTerm - the terminal emulator, which I talked about in the article
    github.com/YuriyNasretdinov/pyte - my implementation of the selectel / pyte library on Javascript (not accepted by the developers, unfortunately)

    Also popular now: