Resize a Console

I’ve been playing with the demo server code (https://github.com/lxc/lxd-demo-server) as a way to give clients access to a terminal via web socket (I’m writing an iOS native terminal front-end to view this). While that effort appears to be working, I’m hitting a wall trying to send a resize console command over the control web socket. My Go-fu is weak (er, non-existent might be better said), so it’s not entirely clear how I might tackle sending a command correctly.

If you don’t mind looking at the whole method, here’s what I’ve written to make this attempt:

func restConsoleResizeHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
	http.Error(w, "Not implemented", 501)
	return
}

name := r.FormValue("id")
width := r.FormValue("width")
height := r.FormValue("height")
widthInt, err := strconv.Atoi(width)
heightInt, err := strconv.Atoi(height)

w.Header().Set("Access-Control-Allow-Origin", "*")

req := api.ContainerConsolePost{
	Width:  widthInt,
	Height: heightInt,
}

// control socket handler
control := func(conn *websocket.Conn) {
	for {
		_, _, err = conn.ReadMessage()
		if err != nil {
			break
		}
	}
}

consoleDisconnect := make(chan bool)
sendDisconnect := make(chan bool)
defer close(sendDisconnect)
consoleArgs := lxd.ContainerConsoleArgs{
	Terminal: &ReadWriteCloser{inRead, outWrite},
	Control:           control,
	ConsoleDisconnect: consoleDisconnect,
}

go func() {
	<-sendDisconnect
	close(consoleDisconnect)
}()

// Run the command in the container
op, err := lxdDaemon.ConsoleContainer(name, req, &consoleArgs)
fmt.Fprintln(os.Stdout, "Resize op:", op)
fmt.Fprintln(os.Stdout, "Resize err:", err)

return
}

I believe the problem is the Terminal argument in ContainerConsoleArgs; ultimately it’s not going to the same terminal instance as the one created in the console handler. But a definitive dearth of documentation around these methods is stopping me.

Any suggestions for how to proceed from here would be most welcome!

Ah, interesting so you’re trying to get something like lxd-demo-server but using the interactive console of the container rather than an exec session.

@brauner may be able to help you there. I’m not sure exactly how much testing was done on the width/height (SIGWINCH) handling for consoles given how new that feature is.

Ahhh, as it happens, I’m not using the interactive console! This is a clue, isn’t it? I’m naive enough in my understanding to know that there’s a difference between the two, but not about what to implement!

So I’m actually using the exec session, as it exists in the demo server. What would be technique for implementing a resize behaviour using that, rather than the interactive console?

Or, alternatively, should I implement the console interactively vs the current exec session?

For the exec session, you’ll want to send a resize-window message over the control websocket.

As the code we’re using in the interactive lxc exec tool.

This is a great lead, thank you! I hope I can impose upon you one more time, because this gets to the central question that I feel has stymied me during this adventure: how do I get a reference to the control socket in the context of this code?

You can find the control socket there:

So you could inside that handler function, prepare a control message and send it before entering that for loop.

This reads like it is settled? :slight_smile:

I’m hoping so. I’ll have a chance to try this tonight.

Thanks again for all your help so far. As I look into this more it appears that sending the resize message over the control socket may work (assuming I can figure out what I should be sending), but only for the initial setup request. Is there a way to do this if the window size changes during a session?

For during the session, you’d most likely need to keep a reference to the control socket in some kind of global map, then when you get a REST API call to resize a particular session, you can look up what control socket to send the new size to.

I’ve made some progress here: I have the control socket sending a resize message when I first create the new console! But I’ve gotten stuck on updating the socket. If you’ll permit a dissection of the code I’ve written to explain?

In restConsoleHandler I’ve added a function call within the control socket handler to resize the console:

// control socket handler
	handler := func(conn *websocket.Conn) {
		sendControlResize(conn, widthInt, heightInt)
		for {
			_, _, err = conn.ReadMessage()
			if err != nil {
				break
			}
		}
	}

This relies on the in-scope values for widthInt and heightInt. Here’s that function:

func sendControlResize(conn *websocket.Conn, width int, height int) {
	w, _ := conn.NextWriter(websocket.TextMessage)

	msg := api.ContainerExecControl{}
	msg.Command = "window-resize"
	msg.Args = make(map[string]string)
	msg.Args["width"] = strconv.Itoa(width)
	msg.Args["height"] = strconv.Itoa(height)

	buf, _ := json.Marshal(msg)
	_, _ = w.Write(buf)

	w.Close()
}

This works! Values that I send on the console start are passed through and the resulting console is sized correctly.

It’s the update attempt that fails. Following your advice, I tried creating a map of… well ha ha… both handlers and web socket connections (I’m like that dog in those “I have no idea what I’m doing” pictures).

type ControlSocketHandler func(conn *websocket.Conn)
var handlerMap = map[string]ControlSocketHandler{}
var connMap = map[string]*websocket.Conn{}

Maybe you can tell me which one you have in mind! I populate both maps with calls like these:

handlerMap[containerName] = handler
connMap[containerName] = conn

And this is where I run into WTF Land:

func restConsoleResizeHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Not implemented", 501)
		return
	}
	
	w.Header().Set("Access-Control-Allow-Origin", "*")

	name := r.FormValue("id")
	width := r.FormValue("width")
	height := r.FormValue("height")
	widthInt, _ := strconv.Atoi(width)
	heightInt, _ := strconv.Atoi(height)

	conn := connMap[name]
	handler := handlerMap[name]
	handler(conn)
}

Naw, that’s all wrong. But how the heck do I get that handler to fire, and with the right int values? :smiley:

Thanks Stephane. Thank you so much.

A.