mirror of https://github.com/elisescu/tty-share
Compare commits
53 Commits
Author | SHA1 | Date |
---|---|---|
Vitaly Zdanevich | 95ee2b9814 | 4 months ago |
Elis Popescu | 3801cfbdd7 | 1 year ago |
Michael J Feher | 407bfbbbbe | 1 year ago |
dependabot[bot] | 3c3993ca33 | 2 years ago |
Vasile Popescu | e0292cbc4d | 2 years ago |
Vasile Popescu | ef39dfc5a1 | 2 years ago |
Vasile Popescu | f67f97c41d | 2 years ago |
Vasile Popescu | cbaed0c937 | 2 years ago |
Vasile Popescu | aca5802379 | 2 years ago |
Vasile Popescu | b4b9c20bdb | 2 years ago |
Vasile Popescu | 3994b813bf | 2 years ago |
Vasile Popescu | 02af910efe | 2 years ago |
dependabot[bot] | e6bbaefd47 | 3 years ago |
dependabot[bot] | 49d0e848a4 | 3 years ago |
dependabot[bot] | fb3f50e81b | 3 years ago |
Francisco Vilmar Cardoso Ruviaro | 757a377a78 | 3 years ago |
Philippe Grégoire | f89d4cd34f | 3 years ago |
Philippe Grégoire | f75206bce6 | 3 years ago |
Elis Popescu | 5ab9061d22 | 4 years ago |
Elis Popescu | de15db1ca0 | 4 years ago |
dependabot[bot] | 102b58f597 | 4 years ago |
dependabot[bot] | 9dfc6994c3 | 4 years ago |
dependabot[bot] | 79b7add81f | 4 years ago |
Matthew Strasiotto | a0140aabf3 | 4 years ago |
Matthew Strasiotto | 600f1c11ed | 4 years ago |
Matthew Strasiotto | 2cf0d63ef0 | 4 years ago |
Vasile Popescu | 6d477487d9 | 4 years ago |
Vasile Popescu | fcefcdfb02 | 4 years ago |
Vasile Popescu | 4e3dae80e2 | 4 years ago |
Vasile Popescu | 69c544e993 | 4 years ago |
Vasile Popescu | 150e5932d3 | 4 years ago |
Vasile Popescu | 3230cee786 | 4 years ago |
Vasile Popescu | cf40aea3a2 | 4 years ago |
Vasile Popescu | 9573987434 | 4 years ago |
Vasile Popescu | 8802e00ef3 | 4 years ago |
Vasile Popescu | eb5ce54856 | 4 years ago |
Vasile Popescu | 0dc1cdb6b4 | 4 years ago |
Vasile Popescu | 33c6017254 | 4 years ago |
Vasile Popescu | e1f4cdd06d | 4 years ago |
Vasile Popescu | 1646e00a12 | 4 years ago |
Vasile Popescu | e8c6caac80 | 4 years ago |
Vasile Popescu | 366edcd23e | 4 years ago |
Vasile Popescu | 3c77e059b3 | 4 years ago |
Vasile Popescu | e8bfc06dab | 4 years ago |
Vasile Popescu | 14266a7f24 | 4 years ago |
Vasile Popescu | 6c0171e9d9 | 4 years ago |
Vasile Popescu | 2100e4ab90 | 4 years ago |
Vasile Popescu | 2d481a5a98 | 4 years ago |
Vasile Popescu | afc909a358 | 4 years ago |
Vasile Popescu | 46e1bdaab2 | 4 years ago |
Vasile Popescu | 584cba56c9 | 4 years ago |
Vasile Popescu | ba44046699 | 4 years ago |
Vasile Popescu | 6fc776b924 | 5 years ago |
@ -0,0 +1,2 @@
|
|||||||
|
**/.git
|
||||||
|
**/node_modules
|
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: check the code builds
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# build targets
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: '386'
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v2.3.4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Golang
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.13.0
|
||||||
|
|
||||||
|
- name: Build stage
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
|
||||||
|
run: |
|
||||||
|
# TODO: figure out how to get go-bindata here
|
||||||
|
# make -C server
|
||||||
|
v=$(echo ${GITHUB_REF} | awk -F/ '{print substr($3,2,10);}')
|
||||||
|
go build -x -v -mod=vendor -ldflags "-X main.version=${v} -w -s" -o "tty-share_${GOOS}-${GOARCH}"
|
||||||
|
|
||||||
|
- name: Upload to artifact storage
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: tty-share_${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
if-no-files-found: error
|
||||||
|
# only meant for sharing with the publish job
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
path: tty-share_*
|
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# build targets
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: '386'
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v2.3.4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
|
||||||
|
- name: Build for Linux-amd64
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
|
||||||
|
run: |
|
||||||
|
v=$(echo ${GITHUB_REF} | awk -F/ '{print substr($3,2,10);}')
|
||||||
|
go build -x -v -mod=vendor -ldflags "-X main.version=${v} -w -s" -o "tty-share_${GOOS}-${GOARCH}"
|
||||||
|
|
||||||
|
- name: Upload to artifact storage
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: tty-share_${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
if-no-files-found: error
|
||||||
|
# only meant for sharing with the publish job
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
path: tty-share_*
|
||||||
|
|
||||||
|
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
|
with:
|
||||||
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
tty-share_*
|
||||||
|
id: "automatic_releases"
|
@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
|
server/frontend/node_modules
|
||||||
|
server/frontend/public
|
||||||
|
|
||||||
out
|
out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
playground/
|
playground/
|
||||||
.vscode/
|
.vscode/
|
||||||
tty-share
|
|
||||||
tmp-*
|
tmp-*
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
FROM alpine:3.12
|
||||||
|
|
||||||
|
ARG build_deps="go git"
|
||||||
|
|
||||||
|
COPY . /go/src/github.com/elisescu/tty-share
|
||||||
|
|
||||||
|
RUN apk update && apk add -u $build_deps
|
||||||
|
|
||||||
|
|
||||||
|
RUN cd /go/src/github.com/elisescu/tty-share && \
|
||||||
|
GOPATH=/go go get github.com/go-bindata/go-bindata/... && \
|
||||||
|
GOPATH=/go /go/bin/go-bindata --prefix server/frontend/static -o gobindata.go server/frontend/static/* && \
|
||||||
|
GOPATH=/go go build && \
|
||||||
|
cp tty-share /usr/bin/ && \
|
||||||
|
rm -r /go && \
|
||||||
|
apk del $build_deps
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tty-share", "--command", "/bin/sh"]
|
@ -0,0 +1,311 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/elisescu/tty-share/server"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
"github.com/moby/term"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ttyShareClient struct {
|
||||||
|
url string
|
||||||
|
ttyWsConn *websocket.Conn
|
||||||
|
tunnelWsConn *websocket.Conn
|
||||||
|
tunnelAddresses *string
|
||||||
|
detachKeys string
|
||||||
|
wcChan chan os.Signal
|
||||||
|
ioFlagAtomic uint32 // used with atomic
|
||||||
|
winSizes struct {
|
||||||
|
thisW uint16
|
||||||
|
thisH uint16
|
||||||
|
remoteW uint16
|
||||||
|
remoteH uint16
|
||||||
|
}
|
||||||
|
winSizesMutex sync.Mutex
|
||||||
|
tunnelMuxSession *yamux.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTtyShareClient(url string, detachKeys string, tunnelConfig *string) *ttyShareClient {
|
||||||
|
return &ttyShareClient{
|
||||||
|
url: url,
|
||||||
|
ttyWsConn: nil,
|
||||||
|
detachKeys: detachKeys,
|
||||||
|
wcChan: make(chan os.Signal, 1),
|
||||||
|
ioFlagAtomic: 1,
|
||||||
|
tunnelAddresses: tunnelConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearScreen() {
|
||||||
|
fmt.Fprintf(os.Stdout, "\033[H\033[2J")
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyListener struct {
|
||||||
|
wrappedReader io.Reader
|
||||||
|
ioFlagAtomicP *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kl *keyListener) Read(data []byte) (n int, err error) {
|
||||||
|
n, err = kl.wrappedReader.Read(data)
|
||||||
|
if _, ok := err.(term.EscapeError); ok {
|
||||||
|
log.Debug("Escape code detected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are not supposed to do any IO, then return 0 bytes read. This happens the local
|
||||||
|
// window is smaller than the remote one
|
||||||
|
if atomic.LoadUint32(kl.ioFlagAtomicP) == 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ttyShareClient) updateAndDecideStdoutMuted() {
|
||||||
|
log.Infof("This window: %dx%d. Remote window: %dx%d", c.winSizes.thisW, c.winSizes.thisH, c.winSizes.remoteW, c.winSizes.remoteH)
|
||||||
|
|
||||||
|
if c.winSizes.thisH < c.winSizes.remoteH || c.winSizes.thisW < c.winSizes.remoteW {
|
||||||
|
atomic.StoreUint32(&c.ioFlagAtomic, 0)
|
||||||
|
clearScreen()
|
||||||
|
messageFormat := "\n\rYour window is smaller than the remote window. Please resize or press <C-o C-c> to detach.\n\r\tRemote window: %dx%d \n\r\tYour window: %dx%d \n\r"
|
||||||
|
fmt.Printf(messageFormat, c.winSizes.remoteW, c.winSizes.remoteH, c.winSizes.thisW, c.winSizes.thisH)
|
||||||
|
} else {
|
||||||
|
if atomic.LoadUint32(&c.ioFlagAtomic) == 0 { // clear the screen when changing back to "write"
|
||||||
|
// TODO: notify the remote side to "refresh" the content.
|
||||||
|
clearScreen()
|
||||||
|
}
|
||||||
|
atomic.StoreUint32(&c.ioFlagAtomic, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ttyShareClient) updateThisWinSize() {
|
||||||
|
size, err := term.GetWinsize(os.Stdin.Fd())
|
||||||
|
if err == nil {
|
||||||
|
c.winSizesMutex.Lock()
|
||||||
|
c.winSizes.thisW = size.Width
|
||||||
|
c.winSizes.thisH = size.Height
|
||||||
|
c.winSizesMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ttyShareClient) Run() (err error) {
|
||||||
|
log.Debugf("Connecting as a client to %s ..", c.url)
|
||||||
|
|
||||||
|
resp, err := http.Get(c.url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path of the websockts route from the header
|
||||||
|
ttyWsPath := resp.Header.Get("TTYSHARE-TTY-WSPATH")
|
||||||
|
ttyWSProtocol := resp.Header.Get("TTYSHARE-VERSION")
|
||||||
|
|
||||||
|
ttyTunnelPath := resp.Header.Get("TTYSHARE-TUNNEL-WSPATH")
|
||||||
|
|
||||||
|
// Build the WS URL from the host part of the given http URL and the wsPath
|
||||||
|
httpURL, err := url.Parse(c.url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wsScheme := "ws"
|
||||||
|
if httpURL.Scheme == "https" {
|
||||||
|
wsScheme = "wss"
|
||||||
|
}
|
||||||
|
ttyWsURL := wsScheme + "://" + httpURL.Host + ttyWsPath
|
||||||
|
ttyTunnelURL := wsScheme + "://" + httpURL.Host + ttyTunnelPath
|
||||||
|
|
||||||
|
log.Debugf("Built the WS URL from the headers: %s", ttyWsURL)
|
||||||
|
|
||||||
|
c.ttyWsConn, _, err = websocket.DefaultDialer.Dial(ttyWsURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.ttyWsConn.Close()
|
||||||
|
|
||||||
|
tunnelFunc := func() {
|
||||||
|
if *c.tunnelAddresses == "" {
|
||||||
|
// Don't build a tunnel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ver, err := strconv.Atoi(ttyWSProtocol); err != nil || ver < 2 {
|
||||||
|
log.Fatalf("Cannot create a tunnel. Server too old (protocol %d, required min. 2)", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tunnelWsConn, _, err = websocket.DefaultDialer.Dial(ttyTunnelURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Cannot create a tunnel connection with the server. Server needs to allow that")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.tunnelWsConn.Close()
|
||||||
|
|
||||||
|
a := strings.Split(*c.tunnelAddresses, ":")
|
||||||
|
tunnelRemoteAddress := fmt.Sprintf("%s:%s", a[1], a[2])
|
||||||
|
tunnelLocalAddress := fmt.Sprintf(":%s", a[0])
|
||||||
|
|
||||||
|
initMsg := server.TunInitMsg{
|
||||||
|
Address: tunnelRemoteAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(initMsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not marshal the tunnel init message: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.tunnelWsConn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not initiate the tunnel: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wsWRC := server.WSConnReadWriteCloser{
|
||||||
|
WsConn: c.tunnelWsConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
localListener, err := net.Listen("tcp", tunnelLocalAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not listen locally for the tunnel: %s", err.Error())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tunnelMuxSession, err = yamux.Server(&wsWRC, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not create mux server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
localTunconn, err := localListener.Accept()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Cannot accept local tunnel connections: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
muxClient, err := c.tunnelMuxSession.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Cannot create a muxer to the remote, over ws: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(muxClient, localTunconn)
|
||||||
|
defer localTunconn.Close()
|
||||||
|
defer muxClient.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(localTunconn, muxClient)
|
||||||
|
defer localTunconn.Close()
|
||||||
|
defer muxClient.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detachBytes, err := term.ToBytes(c.detachKeys)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Invalid dettaching keys: %s", c.detachKeys)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := term.MakeRaw(os.Stdin.Fd())
|
||||||
|
defer term.RestoreTerminal(os.Stdin.Fd(), state)
|
||||||
|
clearScreen()
|
||||||
|
|
||||||
|
protoWS := server.NewTTYProtocolWSLocked(c.ttyWsConn)
|
||||||
|
|
||||||
|
monitorWinChanges := func() {
|
||||||
|
// start monitoring the size of the terminal
|
||||||
|
signal.Notify(c.wcChan, syscall.SIGWINCH)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.wcChan:
|
||||||
|
c.updateThisWinSize()
|
||||||
|
c.updateAndDecideStdoutMuted()
|
||||||
|
protoWS.SetWinSize(int(c.winSizes.thisW), int(c.winSizes.thisH))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readLoop := func() {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
err = protoWS.ReadAndHandle(
|
||||||
|
// onWrite
|
||||||
|
func(data []byte) {
|
||||||
|
if atomic.LoadUint32(&c.ioFlagAtomic) != 0 {
|
||||||
|
os.Stdout.Write(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// onWindowSize
|
||||||
|
func(cols, rows int) {
|
||||||
|
c.winSizesMutex.Lock()
|
||||||
|
c.winSizes.remoteW = uint16(cols)
|
||||||
|
c.winSizes.remoteH = uint16(rows)
|
||||||
|
c.winSizesMutex.Unlock()
|
||||||
|
c.updateThisWinSize()
|
||||||
|
c.updateAndDecideStdoutMuted()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error parsing remote message: %s", err.Error())
|
||||||
|
if err == io.EOF {
|
||||||
|
// Remote WS connection closed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLoop := func() {
|
||||||
|
kl := &keyListener{
|
||||||
|
wrappedReader: term.NewEscapeProxy(os.Stdin, detachBytes),
|
||||||
|
ioFlagAtomicP: &c.ioFlagAtomic,
|
||||||
|
}
|
||||||
|
_, err := io.Copy(protoWS, kl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Connection closed: %s", err.Error())
|
||||||
|
c.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go monitorWinChanges()
|
||||||
|
go writeLoop()
|
||||||
|
go tunnelFunc()
|
||||||
|
readLoop()
|
||||||
|
|
||||||
|
clearScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ttyShareClient) Stop() {
|
||||||
|
// if we had a tunnel, close it
|
||||||
|
if c.tunnelMuxSession != nil {
|
||||||
|
c.tunnelMuxSession.Close()
|
||||||
|
c.tunnelWsConn.Close()
|
||||||
|
}
|
||||||
|
c.ttyWsConn.Close()
|
||||||
|
signal.Stop(c.wcChan)
|
||||||
|
}
|
@ -1,24 +1,28 @@
|
|||||||
High level architecture
|
# High level flow
|
||||||
=======================
|
|
||||||
|
|
||||||
|
## Direct terminal sharing
|
||||||
|
|
||||||
```
|
No proxy needed. `tty-share` will start a command, and be ready to serve it's output and input over WS connections.
|
||||||
Alice
|
|
||||||
|
|
||||||
+-tty-share---------------+ +-tty-server----------------+ Bob
|
## Proxy terminal sharing
|
||||||
| | | | https:// +------------+
|
|
||||||
| +------+ +-----+ | TLS | +------+ +---------+ | wss:// |tty-receiver|
|
|
||||||
| | bash | <-+-> |proto| <---------------> | proto| <-> | session | +-----+------> | 1 |
|
|
||||||
| +------+ | +-----+ | | +------+ +---------+ | | +------------+
|
|
||||||
| | | | | |
|
|
||||||
| +-> pty | +---------------------------+ | +------------+
|
|
||||||
+-------------------------+ +------> |tty-receiver|
|
|
||||||
| 2 |
|
|
||||||
+------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
Alice wants to share a terminal session with Bob, so she starts `tty-share` on her machine, inside the terminal. `tty-share` then connects to the `tty-server` and starts inside a `bash` process. It then puts the terminal in which it was started in RAW mode, and the stdin and stdout are multiplexed to/from the `bash` process it started, and the remote connection to the `tty-server`. On the server side, a session is created, which connects the `tty-share` connection with the future `tty-receiver` instances, running in the browser. The `tty-receiver` runs inside the browser, on top of [xterm.js](https://xtermjs.org/), and is served by the server, via a unique session URL. Alice has to send this unique URL to Bob.
|
- the `tty-share` opens a TCP connection to the `tty-proxy`
|
||||||
|
- the `tty-proxy` proxy accepts the connection, generates a session ID, and sends it back to `tty-share`
|
||||||
|
- there is now a direct connection between the two
|
||||||
|
- a session ID to map any web connections to the `tty-share` side
|
||||||
|
|
||||||
Once the connection is established, Bob can then interact inside the browser with the `bash` session started by Alice. When Bob presses, for example, the key `a`, this is detected by `xterm.js` and sent via a websockets connection to the server side. From there, it is sent to the `tty-share` which sends it to the pseudo terminal attached to the `bash` process started inside `tty-share`. Then character `a` is received via the standard output of the `bash` command, and is sent from there both to the standard output of the `tty-share`, so Alice can see it, and also to the `tty-receiver` (via the server), so Bob can see it too.
|
- `tty-share` gets http requests:
|
||||||
|
- `/` (direct, from a listening server): serves the `index.html` - templated for direct requests (e..g: `<script src="/static/tty-receiver.js"></script>`)
|
||||||
|
- `/` (via the `tty-proxy` TCP connection): serves the `index.html` - templated for the respective session (it already has the session). (e.g.: `<script src="<id>/static/tty-receiver.js"></script>`)
|
||||||
|
- `/ws/` (direct, from the listening server): accepts a WS connection and connects it to the command opened
|
||||||
|
- `/ws/` (via the `tty-proxy` TCP connection): accepts a WS connection and connects it to the command opened
|
||||||
|
|
||||||
More specific details on how this is implemented, can be seen in the source code of the `tty-share`.
|
- `tty-proxy` gets HTTP requests:
|
||||||
|
- `/s/<id>/*` - builds a HTTP request forwards it to the TCP connection it has to the `tty-share` for that `<id>`
|
||||||
|
- `/ws/<id>` - forwards the WS connection to the `tty-share`, over the *same* TCP connection as above
|
||||||
|
|
||||||
|
- over the same TCP connection, we have to pass multiple requests + a WS connection
|
||||||
|
- https://godoc.org/github.com/hashicorp/yamux
|
||||||
|
- https://godoc.org/github.com/soheilhy/cmux#pkg-examples
|
||||||
|
|
||||||
|
x
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
@ -0,0 +1,11 @@
|
|||||||
|
# Old version
|
||||||
|
|
||||||
|
The old version of tty-share was working on a different architecture. The `tty-share` command was smaller, and the [tty-server](https://github.com/elisescu/tty-server) was serving the browser code. I decided to change that and make the server side simpler so it only forwards data back and forth, acting exactly like a reverse proxy. The only difference between `tty-proxy` and a reverse proxy, is that it doesn't create the connections to the target by itself, but instead it's the target (the `tty-share` command) that create the TCP connection over which then the connections are proxied to.
|
||||||
|
|
||||||
|
The two main reasons for this change are:
|
||||||
|
1. **non public sessions**: if you want to share a terminal session only in the local network, that is now possible. For this to work, the `tty-share` command has to contain all the browser code too.
|
||||||
|
2. **decoupling** between the public server and the `tty-share` so the user sharing is always in control of what code will end up running in the browser, without relying on the server. This makes backwards compatibility easier to maintain, at least for the browser sessions.
|
||||||
|
|
||||||
|
This change also means that:
|
||||||
|
* the [tty-server]() is now deprecated, and `tty-proxy` should be used instead. I reflected on whether I should have kept the same name or not, but I felt that using a different one might avoid confusion, given that they are two completely different applications.
|
||||||
|
* you will need to use the latest version of the `tty-share` that works with this new architecture.
|
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
After Width: | Height: | Size: 4.6 MiB |
@ -0,0 +1,73 @@
|
|||||||
|
# bash completion for tty-share -*- shell-script -*-
|
||||||
|
|
||||||
|
_tty-share()
|
||||||
|
{
|
||||||
|
local cur prev OPTS
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
case $prev in
|
||||||
|
'-args'|'--args')
|
||||||
|
COMPREPLY=( $(compgen -W "string" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-command'|'--command')
|
||||||
|
COMPREPLY=( $(compgen -W "/bin/bash" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-detach-keys'|'--detach-keys')
|
||||||
|
COMPREPLY=( $(compgen -W "string" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-frontend-path'|'--frontend-path')
|
||||||
|
COMPREPLY=( $(compgen -W "string" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-listen'|'--listen')
|
||||||
|
COMPREPLY=( $(compgen -W "localhost:8000" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-logfile'|'--logfile')
|
||||||
|
COMPREPLY=( $(compgen -W "-" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-no-tls'|'--no-tls')
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-public'|'--public')
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-readonly'|'--readonly')
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-tty-proxy'|'--tty-proxy')
|
||||||
|
COMPREPLY=( $(compgen -W "on.tty-share.com:4567" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-verbose'|'--verbose')
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
'-version'|'--version')
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $cur in
|
||||||
|
-*)
|
||||||
|
OPTS="--args
|
||||||
|
--command
|
||||||
|
--detach-keys
|
||||||
|
--frontend-path
|
||||||
|
--listen
|
||||||
|
--logfile
|
||||||
|
--no-tls
|
||||||
|
--public
|
||||||
|
--readonly
|
||||||
|
--tty-proxy
|
||||||
|
--verbose
|
||||||
|
--version"
|
||||||
|
COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
complete -F _tty-share tty-share
|
@ -1,11 +1,19 @@
|
|||||||
module github.com/elisescu/tty-share
|
module github.com/elisescu/tty-share
|
||||||
|
|
||||||
go 1.13
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/elisescu/pty v1.0.2
|
github.com/creack/pty v1.1.11
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/sirupsen/logrus v1.4.2
|
github.com/gorilla/websocket v1.5.0
|
||||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
|
github.com/hashicorp/yamux v0.1.1
|
||||||
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 // indirect
|
github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c
|
||||||
|
github.com/sirupsen/logrus v1.9.0
|
||||||
|
golang.org/x/crypto v0.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
|
golang.org/x/term v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||||
|
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elisescu/pty v1.0.2 h1:I/wkN5bnPyh00j/bnNAy8wll8yvd8wRLUjPnmcmYI+Q=
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
github.com/elisescu/pty v1.0.2/go.mod h1:tzLUboZf84k7sFZdd2cOvhr/fSxMABV0UTMxnF25R/Y=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
|
github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/POIPtYD5tRAq0qMqCRjQNK+g=
|
||||||
|
github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||||
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 h1:/W9OPMnnpmFXHYkcp2rQsbFUbRlRzfECQjmAFiOyHE8=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProtocolMessageIDType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
MsgIDSenderInitRequest = "SenderInitRequest"
|
|
||||||
MsgIDSenderInitReply = "SenderInitReply"
|
|
||||||
MsgIDSenderNewReceiverConnected = "SenderNewReceiverConnected"
|
|
||||||
MsgIDReceiverInitRequest = "ReceiverInitRequest"
|
|
||||||
MsgIDReceiverInitReply = "ReceiverInitReply"
|
|
||||||
MsgIDWrite = "Write"
|
|
||||||
MsgIDWinSize = "WinSize"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message used to encapsulate the rest of the bessages bellow
|
|
||||||
type MsgAll struct {
|
|
||||||
Type ProtocolMessageIDType
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// These messages are used between the server and the sender/receiver
|
|
||||||
type MsgTTYSenderInitRequest struct {
|
|
||||||
Salt string
|
|
||||||
PasswordVerifierA string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgTTYSenderInitReply struct {
|
|
||||||
ReceiverURLWebReadWrite string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgTTYSenderNewReceiverConnected struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgTTYReceiverInitRequest struct {
|
|
||||||
ChallengeReply string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgTTYReceiverInitReply struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// These messages are not intended for the server, so they are just forwarded by it to the remote
|
|
||||||
// side.
|
|
||||||
type MsgTTYWrite struct {
|
|
||||||
Data []byte
|
|
||||||
Size int
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgTTYWinSize struct {
|
|
||||||
Cols int
|
|
||||||
Rows int
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadAndUnmarshalMsg(reader io.Reader, aMessage interface{}) (err error) {
|
|
||||||
var wrapperMsg MsgAll
|
|
||||||
// Wait here for the right message to come
|
|
||||||
dec := json.NewDecoder(reader)
|
|
||||||
err = dec.Decode(&wrapperMsg)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Cannot decode message: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(wrapperMsg.Data, aMessage)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Cannot decode message: " + err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func MarshalMsg(aMessage interface{}) (_ []byte, err error) {
|
|
||||||
var msg MsgAll
|
|
||||||
|
|
||||||
if initRequestMsg, ok := aMessage.(MsgTTYSenderInitRequest); ok {
|
|
||||||
msg.Type = MsgIDSenderInitRequest
|
|
||||||
msg.Data, err = json.Marshal(initRequestMsg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return json.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if initReplyMsg, ok := aMessage.(MsgTTYSenderInitReply); ok {
|
|
||||||
msg.Type = MsgIDSenderInitReply
|
|
||||||
msg.Data, err = json.Marshal(initReplyMsg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return json.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if writeMsg, ok := aMessage.(MsgTTYWrite); ok {
|
|
||||||
msg.Type = MsgIDWrite
|
|
||||||
msg.Data, err = json.Marshal(writeMsg)
|
|
||||||
//fmt.Printf("Sent write message %s\n", string(writeMsg.Data))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return json.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if winChangedMsg, ok := aMessage.(MsgTTYWinSize); ok {
|
|
||||||
msg.Type = MsgIDWinSize
|
|
||||||
msg.Data, err = json.Marshal(winChangedMsg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return json.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newRcvMsg, ok := aMessage.(MsgTTYSenderNewReceiverConnected); ok {
|
|
||||||
msg.Type = MsgIDSenderNewReceiverConnected
|
|
||||||
msg.Data, err = json.Marshal(newRcvMsg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return json.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MarshalAndWriteMsg(writer io.Writer, aMessage interface{}) (err error) {
|
|
||||||
b, err := MarshalMsg(aMessage)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := writer.Write(b)
|
|
||||||
|
|
||||||
if n != len(b) {
|
|
||||||
err = fmt.Errorf("Unable to write : wrote %d out of %d bytes", n, len(b))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
@ -0,0 +1,150 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelloClient struct {
|
||||||
|
Version string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloServer struct {
|
||||||
|
Version string
|
||||||
|
SessionID string
|
||||||
|
PublicURL string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxyConnection struct {
|
||||||
|
muxSession *yamux.Session
|
||||||
|
backConnAddress string
|
||||||
|
SessionID string
|
||||||
|
PublicURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyConnection(backConnAddrr, proxyAddr string, noTLS bool) (*proxyConnection, error) {
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if noTLS {
|
||||||
|
conn, err = net.Dial("tcp", proxyAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roots, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn, err = tls.Dial("tcp", proxyAddr, &tls.Config{RootCAs: roots})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C -> S: HelloCLient
|
||||||
|
// S -> C: HelloServer {sesionID}
|
||||||
|
je := json.NewEncoder(conn)
|
||||||
|
// TODO: extract these strings constants somewhere at some point
|
||||||
|
helloC := HelloClient{
|
||||||
|
Version: "1",
|
||||||
|
Data: "-",
|
||||||
|
}
|
||||||
|
err = je.Encode(helloC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jd := json.NewDecoder(conn)
|
||||||
|
var helloS HelloServer
|
||||||
|
err = jd.Decode(&helloS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Connected to %s tty-proxy: version=%s, sessionID=%s", helloS.PublicURL, helloS.Version, helloS.SessionID)
|
||||||
|
session, err := yamux.Server(conn, nil)
|
||||||
|
|
||||||
|
return &proxyConnection{
|
||||||
|
muxSession: session,
|
||||||
|
backConnAddress: backConnAddrr,
|
||||||
|
SessionID: helloS.SessionID,
|
||||||
|
PublicURL: helloS.PublicURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyConnection) RunProxy() {
|
||||||
|
for {
|
||||||
|
frontConn, err := p.muxSession.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("tty-proxy connection closed: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer frontConn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
backConn, err := net.Dial("tcp", p.backConnAddress)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Cannot proxy the connection to the target HTTP server: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer backConn.Close()
|
||||||
|
|
||||||
|
pipeConnectionsAndWait(backConn, frontConn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyConnection) Stop() {
|
||||||
|
p.muxSession.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func errToString(err error) string {
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return "nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeConnectionsAndWait(backConn, frontConn net.Conn) error {
|
||||||
|
errChan := make(chan error, 2)
|
||||||
|
|
||||||
|
backConnAddr := backConn.RemoteAddr().String()
|
||||||
|
frontConnAddr := frontConn.RemoteAddr().String()
|
||||||
|
|
||||||
|
log.Debugf("Piping the two conn %s <-> %s ..", backConnAddr, frontConnAddr)
|
||||||
|
|
||||||
|
copyAndNotify := func(dst, src net.Conn, info string) {
|
||||||
|
n, err := io.Copy(dst, src)
|
||||||
|
log.Debugf("%s: piping done with %d bytes, and err %s", info, n, errToString(err))
|
||||||
|
errChan <- err
|
||||||
|
|
||||||
|
// Close both connections when done with copying. Yeah, both will beclosed two
|
||||||
|
// times, but it doesn't matter. By closing them both, we unblock the other copy
|
||||||
|
// call which would block indefinitely otherwise
|
||||||
|
dst.Close()
|
||||||
|
src.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
go copyAndNotify(backConn, frontConn, "front->back")
|
||||||
|
go copyAndNotify(frontConn, backConn, "back->front")
|
||||||
|
err1 := <-errChan
|
||||||
|
err2 := <-errChan
|
||||||
|
|
||||||
|
log.Debugf("Piping finished for %s <-> %s .", backConnAddr, frontConnAddr)
|
||||||
|
|
||||||
|
// Return one of the two error that is not nil
|
||||||
|
if err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
return err2
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
# Proxy
|
||||||
|
|
||||||
|
`tty-share` will open a `TCP` connection to a public proxy.
|
||||||
|
|
||||||
|
This proxy will act like a reverse proxy, but it will not open new connections to any new back
|
||||||
|
server, and instead it will simulate multiple multiplexed connections over the `TCP` connection
|
||||||
|
already opened by `tty-share` mentioned above.
|
||||||
|
|
||||||
|
The proxy will be completely dumb. It will only proxy connections coming from client browsers to the
|
||||||
|
`tty-share`. It will be the `tty-share` command that will serve the content, and the ws
|
||||||
|
connections. Any SSL/WSS from the web clients will be terminated by nginx or other typical reverse
|
||||||
|
proxy.
|
@ -0,0 +1,24 @@
|
|||||||
|
TTY_SERVER_ASSETS=$(wildcard frontend/public/*) frontend/public/index.html
|
||||||
|
|
||||||
|
.PHONY: all frontend clean cleanfront rebuild
|
||||||
|
|
||||||
|
all: assets_bundle.go
|
||||||
|
@echo "Done"
|
||||||
|
|
||||||
|
rebuild: clean all
|
||||||
|
|
||||||
|
assets_bundle.go: $(TTY_SERVER_ASSETS)
|
||||||
|
go install github.com/go-bindata/go-bindata/...
|
||||||
|
go-bindata --prefix frontend/public/ -pkg server -o $@ frontend/public/*
|
||||||
|
|
||||||
|
frontend: cleanfront frontend/public/index.html assets_bundle.go
|
||||||
|
|
||||||
|
frontend/public/index.html:
|
||||||
|
cd frontend && npm install && npm run build && cd -
|
||||||
|
|
||||||
|
cleanfront:
|
||||||
|
rm -fr frontend/public
|
||||||
|
|
||||||
|
clean: cleanfront
|
||||||
|
rm -fr assets_bundle.go
|
||||||
|
@echo "Cleaned"
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
v16.13.0
|
@ -0,0 +1,9 @@
|
|||||||
|
# Readme #
|
||||||
|
|
||||||
|
## Building
|
||||||
|
The frontend uses webpack to build everything in a bundle file. Run:
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
webpack
|
||||||
|
```
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "tty-share",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "",
|
||||||
|
"scripts": {
|
||||||
|
"watch": "TTY_SHARE_ENV=development webpack --watch",
|
||||||
|
"build": "TTY_SHARE_ENV=production webpack"
|
||||||
|
},
|
||||||
|
"author": "elisescu",
|
||||||
|
"license": "elisescu",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"css-loader": "^6.7.2",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"ts-loader": "^9.4.1",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"webpack": "^5.76.0",
|
||||||
|
"webpack-cli": "^5.0.0",
|
||||||
|
"xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,147 @@
|
|||||||
|
@import url(https://fonts.googleapis.com/css?family=Raleway:300,700);
|
||||||
|
|
||||||
|
body {
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
background:#0B486B;
|
||||||
|
font-family: 'Raleway', sans-serif;
|
||||||
|
font-weight:300;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
text-align:center;
|
||||||
|
font-size:40px;
|
||||||
|
margin-top:40px;
|
||||||
|
margin-bottom:-40px;
|
||||||
|
position:relative;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles:after {
|
||||||
|
content:'';
|
||||||
|
display:inline-block;
|
||||||
|
width:100%;
|
||||||
|
height:100px;
|
||||||
|
background:#fff;
|
||||||
|
position:absolute;
|
||||||
|
top:-50px;
|
||||||
|
left:0;
|
||||||
|
transform:skewY(-4deg);
|
||||||
|
-webkit-transform:skewY(-4deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles {
|
||||||
|
background:#fff;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
margin-top:-60px;
|
||||||
|
box-shadow:inset -1px -4px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles p {
|
||||||
|
font-size: 240px;
|
||||||
|
color: #fff;
|
||||||
|
padding-top: 60px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 9;
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles p small {
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles .circle.small {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0B486B;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 80px;
|
||||||
|
left: 50%;
|
||||||
|
animation: 7s smallmove infinite cubic-bezier(1,.22,.71,.98);
|
||||||
|
-webkit-animation: 7s smallmove infinite cubic-bezier(1,.22,.71,.98);
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
-webkit-animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles .circle.med {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0B486B;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 10%;
|
||||||
|
animation: 7s medmove infinite cubic-bezier(.32,.04,.15,.75);
|
||||||
|
-webkit-animation: 7s medmove infinite cubic-bezier(.32,.04,.15,.75);
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
-webkit-animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circles .circle.big {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0B486B;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 200px;
|
||||||
|
right: 0;
|
||||||
|
animation: 8s bigmove infinite;
|
||||||
|
-webkit-animation: 8s bigmove infinite;
|
||||||
|
animation-delay: 3s;
|
||||||
|
-webkit-animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes smallmove {
|
||||||
|
0% { top: 10px; left: 45%; opacity: 1; }
|
||||||
|
25% { top: 300px; left: 40%; opacity:0.7; }
|
||||||
|
50% { top: 240px; left: 55%; opacity:0.4; }
|
||||||
|
75% { top: 100px; left: 40%; opacity:0.6; }
|
||||||
|
100% { top: 10px; left: 45%; opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes smallmove {
|
||||||
|
0% { top: 10px; left: 45%; opacity: 1; }
|
||||||
|
25% { top: 300px; left: 40%; opacity:0.7; }
|
||||||
|
50% { top: 240px; left: 55%; opacity:0.4; }
|
||||||
|
75% { top: 100px; left: 40%; opacity:0.6; }
|
||||||
|
100% { top: 10px; left: 45%; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes medmove {
|
||||||
|
0% { top: 0px; left: 20%; opacity: 1; }
|
||||||
|
25% { top: 300px; left: 80%; opacity:0.7; }
|
||||||
|
50% { top: 240px; left: 55%; opacity:0.4; }
|
||||||
|
75% { top: 100px; left: 40%; opacity:0.6; }
|
||||||
|
100% { top: 0px; left: 20%; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes medmove {
|
||||||
|
0% { top: 0px; left: 20%; opacity: 1; }
|
||||||
|
25% { top: 300px; left: 80%; opacity:0.7; }
|
||||||
|
50% { top: 240px; left: 55%; opacity:0.4; }
|
||||||
|
75% { top: 100px; left: 40%; opacity:0.6; }
|
||||||
|
100% { top: 0px; left: 20%; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes bigmove {
|
||||||
|
0% { top: 0px; right: 4%; opacity: 0.5; }
|
||||||
|
25% { top: 100px; right: 40%; opacity:0.4; }
|
||||||
|
50% { top: 240px; right: 45%; opacity:0.8; }
|
||||||
|
75% { top: 100px; right: 35%; opacity:0.6; }
|
||||||
|
100% { top: 0px; right: 4%; opacity: 0.5; }
|
||||||
|
}
|
||||||
|
@keyframes bigmove {
|
||||||
|
0% { top: 0px; right: 4%; opacity: 0.5; }
|
||||||
|
25% { top: 100px; right: 40%; opacity:0.4; }
|
||||||
|
50% { top: 240px; right: 45%; opacity:0.8; }
|
||||||
|
75% { top: 100px; right: 35%; opacity:0.6; }
|
||||||
|
100% { top: 0px; right: 4%; opacity: 0.5; }
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,22 @@
|
|||||||
|
<!-- This is taken from: https://colorlib.com/wp/free-404-error-page-templates/ -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{.PathPrefix}}/static/404.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="not-found">
|
||||||
|
<div id="title">Something's wrong</div>
|
||||||
|
<div class="circles">
|
||||||
|
<p>404
|
||||||
|
<br>
|
||||||
|
<small>Page not found</small>
|
||||||
|
</p>
|
||||||
|
<span class="circle big"></span>
|
||||||
|
<span class="circle med"></span>
|
||||||
|
<span class="circle small"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>tty-share</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nf-sauce-code-pro@2.1.3/nf-font.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="terminal"></div>
|
||||||
|
<div id="settings"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.ttyInitialData = {
|
||||||
|
wsPath: {{.WSPath}}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="{{.PathPrefix}}/static/tty-share.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"jsx": "react",
|
||||||
|
"allowJs": true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* Base64 encode / decode
|
||||||
|
* http://www.webtoolkit.info/
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
var Base64 = {
|
||||||
|
|
||||||
|
// private property
|
||||||
|
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||||
|
|
||||||
|
// public method for encoding
|
||||||
|
encode: function (input) {
|
||||||
|
var output = "";
|
||||||
|
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
input = Base64._utf8_encode(input);
|
||||||
|
|
||||||
|
while (i < input.length) {
|
||||||
|
|
||||||
|
chr1 = input.charCodeAt(i++);
|
||||||
|
chr2 = input.charCodeAt(i++);
|
||||||
|
chr3 = input.charCodeAt(i++);
|
||||||
|
|
||||||
|
enc1 = chr1 >> 2;
|
||||||
|
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||||
|
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||||
|
enc4 = chr3 & 63;
|
||||||
|
|
||||||
|
if (isNaN(chr2)) {
|
||||||
|
enc3 = enc4 = 64;
|
||||||
|
} else if (isNaN(chr3)) {
|
||||||
|
enc4 = 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output +
|
||||||
|
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
|
||||||
|
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
|
||||||
|
// public method for decoding
|
||||||
|
decode: function (input) {
|
||||||
|
var output = "";
|
||||||
|
var chr1, chr2, chr3;
|
||||||
|
var enc1, enc2, enc3, enc4;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||||
|
|
||||||
|
while (i < input.length) {
|
||||||
|
|
||||||
|
enc1 = this._keyStr.indexOf(input.charAt(i++));
|
||||||
|
enc2 = this._keyStr.indexOf(input.charAt(i++));
|
||||||
|
enc3 = this._keyStr.indexOf(input.charAt(i++));
|
||||||
|
enc4 = this._keyStr.indexOf(input.charAt(i++));
|
||||||
|
|
||||||
|
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||||
|
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||||
|
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||||
|
|
||||||
|
output = output + String.fromCharCode(chr1);
|
||||||
|
|
||||||
|
if (enc3 != 64) {
|
||||||
|
output = output + String.fromCharCode(chr2);
|
||||||
|
}
|
||||||
|
if (enc4 != 64) {
|
||||||
|
output = output + String.fromCharCode(chr3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output = Base64._utf8_decode(output);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// private method for UTF-8 encoding
|
||||||
|
_utf8_encode: function (string) {
|
||||||
|
string = string.replace(/\r\n/g, "\n");
|
||||||
|
var utftext = "";
|
||||||
|
|
||||||
|
for (var n = 0; n < string.length; n++) {
|
||||||
|
|
||||||
|
var c = string.charCodeAt(n);
|
||||||
|
|
||||||
|
if (c < 128) {
|
||||||
|
utftext += String.fromCharCode(c);
|
||||||
|
}
|
||||||
|
else if ((c > 127) && (c < 2048)) {
|
||||||
|
utftext += String.fromCharCode((c >> 6) | 192);
|
||||||
|
utftext += String.fromCharCode((c & 63) | 128);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
utftext += String.fromCharCode((c >> 12) | 224);
|
||||||
|
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||||
|
utftext += String.fromCharCode((c & 63) | 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return utftext;
|
||||||
|
},
|
||||||
|
|
||||||
|
// private method for UTF-8 decoding
|
||||||
|
_utf8_decode: function (utftext) {
|
||||||
|
let string = "";
|
||||||
|
let i = 0;
|
||||||
|
let c = 0;
|
||||||
|
let c1 = 0;
|
||||||
|
let c2 = 0;
|
||||||
|
let c3 = 0;
|
||||||
|
|
||||||
|
while (i < utftext.length) {
|
||||||
|
|
||||||
|
c = utftext.charCodeAt(i);
|
||||||
|
|
||||||
|
if (c < 128) {
|
||||||
|
string += String.fromCharCode(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else if ((c > 191) && (c < 224)) {
|
||||||
|
c2 = utftext.charCodeAt(i + 1);
|
||||||
|
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c2 = utftext.charCodeAt(i + 1);
|
||||||
|
c3 = utftext.charCodeAt(i + 2);
|
||||||
|
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
|
||||||
|
i += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return string;
|
||||||
|
},
|
||||||
|
|
||||||
|
base64ToArrayBuffer: function (input) {
|
||||||
|
var binary_string = window.atob(input);
|
||||||
|
var len = binary_string.length;
|
||||||
|
var bytes = new Uint8Array( len );
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Base64;
|
@ -0,0 +1,13 @@
|
|||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
import './main.css';
|
||||||
|
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
|
||||||
|
import { TTYReceiver } from './tty-receiver';
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
macOptionIsMeta: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let wsAddress = "";
|
||||||
|
if (window.location.protocol === "https:") {
|
||||||
|
wsAddress = 'wss://';
|
||||||
|
} else {
|
||||||
|
wsAddress = "ws://";
|
||||||
|
}
|
||||||
|
|
||||||
|
let ttyWindow = window as any;
|
||||||
|
wsAddress += ttyWindow.location.host + ttyWindow.ttyInitialData.wsPath;
|
||||||
|
|
||||||
|
|
||||||
|
const ttyReceiver = new TTYReceiver(wsAddress, document.getElementById('terminal') as HTMLDivElement);
|
@ -0,0 +1,120 @@
|
|||||||
|
import { Terminal, IEvent, IDisposable } from "xterm";
|
||||||
|
|
||||||
|
import base64 from './base64';
|
||||||
|
|
||||||
|
interface IRectSize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TTYReceiver {
|
||||||
|
private xterminal: Terminal;
|
||||||
|
private containerElement: HTMLElement;
|
||||||
|
|
||||||
|
constructor(wsAddress: string, container: HTMLDivElement) {
|
||||||
|
console.log("Opening WS connection to ", wsAddress)
|
||||||
|
const connection = new WebSocket(wsAddress);
|
||||||
|
|
||||||
|
// TODO: expose some of these options in the UI
|
||||||
|
this.xterminal = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
macOptionIsMeta: true,
|
||||||
|
scrollback: 1000,
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontFamily: 'SauceCodePro MonoWindows, courier-new, monospace',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.containerElement = container;
|
||||||
|
this.xterminal.open(container);
|
||||||
|
|
||||||
|
connection.onclose = (evt: CloseEvent) => {
|
||||||
|
|
||||||
|
this.xterminal.blur();
|
||||||
|
this.xterminal.options.cursorBlink = false
|
||||||
|
this.xterminal.clear();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.xterminal.write('Session closed');
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xterminal.focus();
|
||||||
|
|
||||||
|
const containerPixSize = this.getElementPixelsSize(container);
|
||||||
|
const newFontSize = this.guessNewFontSize(this.xterminal.cols, this.xterminal.rows, containerPixSize.width, containerPixSize.height);
|
||||||
|
this.xterminal.options.fontSize = newFontSize
|
||||||
|
this.xterminal.options.fontFamily= 'SauceCodePro MonoWindows, courier-new, monospace'
|
||||||
|
|
||||||
|
connection.onmessage = (ev: MessageEvent) => {
|
||||||
|
let message = JSON.parse(ev.data)
|
||||||
|
let msgData = base64.decode(message.Data)
|
||||||
|
|
||||||
|
if (message.Type === "Write") {
|
||||||
|
let writeMsg = JSON.parse(msgData)
|
||||||
|
this.xterminal.write(base64.base64ToArrayBuffer(writeMsg.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Type == "WinSize") {
|
||||||
|
let winSizeMsg = JSON.parse(msgData)
|
||||||
|
|
||||||
|
const containerPixSize = this.getElementPixelsSize(container);
|
||||||
|
const newFontSize = this.guessNewFontSize(winSizeMsg.Cols, winSizeMsg.Rows, containerPixSize.width, containerPixSize.height);
|
||||||
|
this.xterminal.options.fontSize = newFontSize
|
||||||
|
|
||||||
|
// Now set the new size.
|
||||||
|
this.xterminal.resize(winSizeMsg.Cols, winSizeMsg.Rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xterminal.onData(function (data:string) {
|
||||||
|
let writeMessage = {
|
||||||
|
Type: "Write",
|
||||||
|
Data: base64.encode(JSON.stringify({ Size: data.length, Data: base64.encode(data)})),
|
||||||
|
}
|
||||||
|
let dataToSend = JSON.stringify(writeMessage)
|
||||||
|
connection.send(dataToSend);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the pixels size of the element, after all CSS was applied. This will be used in an ugly
|
||||||
|
// hack to guess what fontSize to set on the xterm object. Horrible hack, but I feel less bad
|
||||||
|
// about it seeing that VSV does it too:
|
||||||
|
// https://github.com/microsoft/vscode/blob/d14ee7613fcead91c5c3c2bddbf288c0462be876/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts#L363
|
||||||
|
private getElementPixelsSize(element: HTMLElement): IRectSize {
|
||||||
|
const defView = this.containerElement.ownerDocument.defaultView;
|
||||||
|
let width = parseInt(defView.getComputedStyle(element).getPropertyValue('width').replace('px', ''), 10);
|
||||||
|
let height = parseInt(defView.getComputedStyle(element).getPropertyValue('height').replace('px', ''), 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to guess the new font size, for the new terminal size, so that the rendered terminal
|
||||||
|
// will have the newWidth and newHeight dimensions
|
||||||
|
private guessNewFontSize(newCols: number, newRows: number, targetWidth: number, targetHeight: number): number {
|
||||||
|
const cols = this.xterminal.cols;
|
||||||
|
const rows = this.xterminal.rows;
|
||||||
|
const fontSize = this.xterminal.options.fontSize
|
||||||
|
const xtermPixelsSize = this.getElementPixelsSize(this.containerElement.querySelector(".xterm-screen"));
|
||||||
|
|
||||||
|
const newHFontSizeMultiplier = (cols / newCols) * (targetWidth / xtermPixelsSize.width);
|
||||||
|
const newVFontSizeMultiplier = (rows / newRows) * (targetHeight / xtermPixelsSize.height);
|
||||||
|
|
||||||
|
let newFontSize;
|
||||||
|
|
||||||
|
if (newHFontSizeMultiplier > newVFontSizeMultiplier) {
|
||||||
|
newFontSize = Math.floor(fontSize * newVFontSizeMultiplier);
|
||||||
|
} else {
|
||||||
|
newFontSize = Math.floor(fontSize * newHFontSizeMultiplier);
|
||||||
|
}
|
||||||
|
return newFontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
TTYReceiver
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
const webpack = require("webpack");
|
||||||
|
const copyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
|
||||||
|
const develBuild = process.env.TTY_SHARE_ENV === 'development';
|
||||||
|
|
||||||
|
let mainConfig = {
|
||||||
|
entry: {
|
||||||
|
'tty-share': './tty-share/main.ts',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: __dirname + '/public/',
|
||||||
|
filename: '[name].js',
|
||||||
|
},
|
||||||
|
mode: develBuild ? 'development' : 'production',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /node_modules.+xterm.+\.map$/,
|
||||||
|
use: ['ignore-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".tsx", ".js"],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new copyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{from: 'static', },
|
||||||
|
{from: 'templates',},
|
||||||
|
]}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (develBuild) {
|
||||||
|
mainConfig.devtool = 'inline-source-map';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = mainConfig;
|
@ -0,0 +1,308 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorNotFound = iota
|
||||||
|
errorNotAllowed = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type PTYHandler interface {
|
||||||
|
Write(data []byte) (int, error)
|
||||||
|
Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionTemplateModel used for templating
|
||||||
|
type AASessionTemplateModel struct {
|
||||||
|
SessionID string
|
||||||
|
Salt string
|
||||||
|
WSPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTYServerConfig is used to configure the tty server before it is started
|
||||||
|
type TTYServerConfig struct {
|
||||||
|
FrontListenAddress string
|
||||||
|
FrontendPath string
|
||||||
|
PTY PTYHandler
|
||||||
|
SessionID string
|
||||||
|
AllowTunneling bool
|
||||||
|
CrossOrigin bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTYServer represents the instance of a tty server
|
||||||
|
type TTYServer struct {
|
||||||
|
httpServer *http.Server
|
||||||
|
config TTYServerConfig
|
||||||
|
session *ttyShareSession
|
||||||
|
muxTunnelSession *yamux.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) serveContent(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
// If a path to the frontend resources was passed, serve from there, otherwise, serve from the
|
||||||
|
// builtin bundle
|
||||||
|
if server.config.FrontendPath == "" {
|
||||||
|
file, err := Asset(name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctype := mime.TypeByExtension(filepath.Ext(name))
|
||||||
|
if ctype == "" {
|
||||||
|
ctype = http.DetectContentType(file)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", ctype)
|
||||||
|
w.Write(file)
|
||||||
|
} else {
|
||||||
|
filePath := server.config.FrontendPath + string(os.PathSeparator) + name
|
||||||
|
_, err := os.Open(filePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Couldn't find resource: %s at %s", name, filePath)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("Serving %s from %s", name, filePath)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTTYServer creates a new instance
|
||||||
|
func NewTTYServer(config TTYServerConfig) (server *TTYServer) {
|
||||||
|
server = &TTYServer{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
server.httpServer = &http.Server{
|
||||||
|
Addr: config.FrontListenAddress,
|
||||||
|
}
|
||||||
|
routesHandler := mux.NewRouter()
|
||||||
|
|
||||||
|
installHandlers := func(session string) {
|
||||||
|
// This function installs handlers for paths that contain the "session" passed as a
|
||||||
|
// parameter. The paths are for the static files, websockets, and other.
|
||||||
|
staticPath := "/s/" + session + "/static/"
|
||||||
|
ttyWsPath := "/s/" + session + "/ws"
|
||||||
|
tunnelWsPath := "/s/" + session + "/tws"
|
||||||
|
pathPrefix := "/s/" + session
|
||||||
|
|
||||||
|
routesHandler.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath,
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
server.serveContent(w, r, r.URL.Path)
|
||||||
|
})))
|
||||||
|
|
||||||
|
routesHandler.HandleFunc(pathPrefix+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check the frontend/templates/tty-share.in.html file to see where the template applies
|
||||||
|
templateModel := struct {
|
||||||
|
PathPrefix string
|
||||||
|
WSPath string
|
||||||
|
}{pathPrefix, ttyWsPath}
|
||||||
|
|
||||||
|
// TODO Extract these in constants
|
||||||
|
w.Header().Add("TTYSHARE-VERSION", "2")
|
||||||
|
|
||||||
|
// Deprecated HEADER (from prev version)
|
||||||
|
// TODO: Find a proper way to stop handling backward versions
|
||||||
|
w.Header().Add("TTYSHARE-WSPATH", ttyWsPath)
|
||||||
|
|
||||||
|
w.Header().Add("TTYSHARE-TTY-WSPATH", ttyWsPath)
|
||||||
|
w.Header().Add("TTYSHARE-TUNNEL-WSPATH", tunnelWsPath)
|
||||||
|
|
||||||
|
server.handleWithTemplateHtml(w, r, "tty-share.in.html", templateModel)
|
||||||
|
})
|
||||||
|
routesHandler.HandleFunc(ttyWsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
server.handleTTYWebsocket(w, r, config.CrossOrigin)
|
||||||
|
})
|
||||||
|
if server.config.AllowTunneling {
|
||||||
|
// tunnel websockets connection
|
||||||
|
routesHandler.HandleFunc(tunnelWsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
server.handleTunnelWebsocket(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
routesHandler.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templateModel := struct{ PathPrefix string }{fmt.Sprintf("/s/%s", session)}
|
||||||
|
server.handleWithTemplateHtml(w, r, "404.in.html", templateModel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the same routes on both the /local/ and /<SessionID>/. The session ID is received
|
||||||
|
// from the tty-proxy server, if a public session is involved.
|
||||||
|
installHandlers("local")
|
||||||
|
installHandlers(config.SessionID)
|
||||||
|
|
||||||
|
server.httpServer.Handler = routesHandler
|
||||||
|
server.session = newTTYShareSession(config.PTY)
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) handleTTYWebsocket(w http.ResponseWriter, r *http.Request, crossOrigin bool) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
if crossOrigin {
|
||||||
|
upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cannot create the WS connection: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// On a new connection, ask for a refresh/redraw of the terminal app
|
||||||
|
server.config.PTY.Refresh()
|
||||||
|
server.session.HandleWSConnection(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) handleTunnelWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
wsConn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cannot upgrade to WS for tunnel route connection: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer wsConn.Close()
|
||||||
|
|
||||||
|
// Read the first message on this ws route, and expect it to be a json containing the address
|
||||||
|
// to tunnel to. After that first message, will follow the raw connection data
|
||||||
|
_, wsReader, err := wsConn.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cannot read from the tunnel WS connection ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunInitMsg TunInitMsg
|
||||||
|
err = json.NewDecoder(wsReader).Decode(&tunInitMsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cannot decode the tunnel init message ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wsRW := &WSConnReadWriteCloser{
|
||||||
|
WsConn: wsConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
server.muxTunnelSession, err = yamux.Server(wsRW, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not open a mux server: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
muxStream, err := server.muxTunnelSession.Accept()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Warnf("Mux cannot accept new connections: %s", err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localConn, err := net.Dial("tcp", tunInitMsg.Address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cannot create local connection ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(muxStream, localConn)
|
||||||
|
// Not sure yet which of the two io.Copy finishes first, so just close everything in both cases
|
||||||
|
defer localConn.Close()
|
||||||
|
defer muxStream.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(localConn, muxStream)
|
||||||
|
// Not sure yet which of the two io.Copy finishes first, so just close everything in both cases
|
||||||
|
defer muxStream.Close()
|
||||||
|
defer localConn.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func panicIfErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) handleWithTemplateHtml(responseWriter http.ResponseWriter, r *http.Request, templateFile string, templateInterface interface{}) {
|
||||||
|
var t *template.Template
|
||||||
|
var err error
|
||||||
|
if server.config.FrontendPath == "" {
|
||||||
|
templateDta, err := Asset(templateFile)
|
||||||
|
panicIfErr(err)
|
||||||
|
t = template.New(templateFile)
|
||||||
|
_, err = t.Parse(string(templateDta))
|
||||||
|
} else {
|
||||||
|
t, err = template.ParseFiles(server.config.FrontendPath + string(os.PathSeparator) + templateFile)
|
||||||
|
}
|
||||||
|
panicIfErr(err)
|
||||||
|
|
||||||
|
err = t.Execute(responseWriter, templateInterface)
|
||||||
|
panicIfErr(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) Run() (err error) {
|
||||||
|
err = server.httpServer.ListenAndServe()
|
||||||
|
log.Debug("Server finished")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) Write(buff []byte) (written int, err error) {
|
||||||
|
return server.session.Write(buff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) WindowSize(cols, rows int) (err error) {
|
||||||
|
return server.session.WindowSize(cols, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *TTYServer) Stop() error {
|
||||||
|
log.Debug("Stopping the server")
|
||||||
|
if server.muxTunnelSession != nil {
|
||||||
|
server.muxTunnelSession.Close()
|
||||||
|
}
|
||||||
|
return server.httpServer.Close()
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ttyShareSession struct {
|
||||||
|
mainRWLock sync.RWMutex
|
||||||
|
ttyProtoConnections *list.List
|
||||||
|
isAlive bool
|
||||||
|
lastWindowSizeMsg MsgTTYWinSize
|
||||||
|
ptyHandler PTYHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyList(l *list.List) *list.List {
|
||||||
|
newList := list.New()
|
||||||
|
for e := l.Front(); e != nil; e = e.Next() {
|
||||||
|
newList.PushBack(e.Value)
|
||||||
|
}
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTTYShareSession(ptyHandler PTYHandler) *ttyShareSession {
|
||||||
|
|
||||||
|
ttyShareSession := &ttyShareSession{
|
||||||
|
ttyProtoConnections: list.New(),
|
||||||
|
ptyHandler: ptyHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ttyShareSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *ttyShareSession) WindowSize(cols, rows int) error {
|
||||||
|
session.mainRWLock.Lock()
|
||||||
|
session.lastWindowSizeMsg = MsgTTYWinSize{Cols: cols, Rows: rows}
|
||||||
|
session.mainRWLock.Unlock()
|
||||||
|
|
||||||
|
session.forEachReceiverLock(func(rcvConn *TTYProtocolWSLocked) bool {
|
||||||
|
rcvConn.SetWinSize(cols, rows)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *ttyShareSession) Write(data []byte) (int, error) {
|
||||||
|
session.forEachReceiverLock(func(rcvConn *TTYProtocolWSLocked) bool {
|
||||||
|
rcvConn.Write(data)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the callback cb for each of the receivers in the list of the receivers, as it was when
|
||||||
|
// this function was called. Note that there might be receivers which might have lost
|
||||||
|
// the connection since this function was called.
|
||||||
|
// Return false in the callback to not continue for the rest of the receivers
|
||||||
|
func (session *ttyShareSession) forEachReceiverLock(cb func(rcvConn *TTYProtocolWSLocked) bool) {
|
||||||
|
session.mainRWLock.RLock()
|
||||||
|
// TODO: Maybe find a better way?
|
||||||
|
rcvsCopy := copyList(session.ttyProtoConnections)
|
||||||
|
session.mainRWLock.RUnlock()
|
||||||
|
|
||||||
|
for receiverE := rcvsCopy.Front(); receiverE != nil; receiverE = receiverE.Next() {
|
||||||
|
receiver := receiverE.Value.(*TTYProtocolWSLocked)
|
||||||
|
if !cb(receiver) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will run on the TTYReceiver connection go routine (e.g.: on the websockets connection routine)
|
||||||
|
// When HandleWSConnection will exit, the connection to the TTYReceiver will be closed
|
||||||
|
func (session *ttyShareSession) HandleWSConnection(wsConn *websocket.Conn) {
|
||||||
|
protoConn := NewTTYProtocolWSLocked(wsConn)
|
||||||
|
|
||||||
|
session.mainRWLock.Lock()
|
||||||
|
rcvHandleEl := session.ttyProtoConnections.PushBack(protoConn)
|
||||||
|
winSize := session.lastWindowSizeMsg
|
||||||
|
session.mainRWLock.Unlock()
|
||||||
|
|
||||||
|
log.Debugf("New WS connection (%s). Serving ..", wsConn.RemoteAddr().String())
|
||||||
|
|
||||||
|
// Sending the initial size of the window, if we have one
|
||||||
|
protoConn.SetWinSize(winSize.Cols, winSize.Rows)
|
||||||
|
|
||||||
|
// Wait until the TTYReceiver will close the connection on its end
|
||||||
|
for {
|
||||||
|
err := protoConn.ReadAndHandle(
|
||||||
|
func(data []byte) {
|
||||||
|
session.ptyHandler.Write(data)
|
||||||
|
},
|
||||||
|
func(cols, rows int) {
|
||||||
|
session.ptyHandler.Refresh()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Finished the WS reading loop: %s", err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the recevier from the list of the receiver of this session, so we need to write-lock
|
||||||
|
session.mainRWLock.Lock()
|
||||||
|
session.ttyProtoConnections.Remove(rcvHandleEl)
|
||||||
|
session.mainRWLock.Unlock()
|
||||||
|
|
||||||
|
wsConn.Close()
|
||||||
|
log.Debugf("Closed receiver connection")
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MsgIDWrite = "Write"
|
||||||
|
MsgIDWinSize = "WinSize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message used to encapsulate the rest of the bessages bellow
|
||||||
|
type MsgWrapper struct {
|
||||||
|
Type string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgTTYWrite struct {
|
||||||
|
Data []byte
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgTTYWinSize struct {
|
||||||
|
Cols int
|
||||||
|
Rows int
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnMsgWrite func(data []byte)
|
||||||
|
type OnMsgWinSize func(cols, rows int)
|
||||||
|
|
||||||
|
type TTYProtocolWSLocked struct {
|
||||||
|
ws *websocket.Conn
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTTYProtocolWSLocked(ws *websocket.Conn) *TTYProtocolWSLocked {
|
||||||
|
return &TTYProtocolWSLocked{
|
||||||
|
ws: ws,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalMsg(aMessage interface{}) (_ []byte, err error) {
|
||||||
|
var msg MsgWrapper
|
||||||
|
|
||||||
|
if writeMsg, ok := aMessage.(MsgTTYWrite); ok {
|
||||||
|
msg.Type = MsgIDWrite
|
||||||
|
msg.Data, err = json.Marshal(writeMsg)
|
||||||
|
//fmt.Printf("Sent write message %s\n", string(writeMsg.Data))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if winChangedMsg, ok := aMessage.(MsgTTYWinSize); ok {
|
||||||
|
msg.Type = MsgIDWinSize
|
||||||
|
msg.Data, err = json.Marshal(winChangedMsg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return json.Marshal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *TTYProtocolWSLocked) ReadAndHandle(onWrite OnMsgWrite, onWinSize OnMsgWinSize) (err error) {
|
||||||
|
var msg MsgWrapper
|
||||||
|
|
||||||
|
_, r, err := handler.ws.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
// underlaying conn is closed. signal that through io.EOF
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(r).Decode(&msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case MsgIDWrite:
|
||||||
|
var msgWrite MsgTTYWrite
|
||||||
|
err = json.Unmarshal(msg.Data, &msgWrite)
|
||||||
|
if err == nil {
|
||||||
|
onWrite(msgWrite.Data)
|
||||||
|
}
|
||||||
|
case MsgIDWinSize:
|
||||||
|
var msgRemoteWinSize MsgTTYWinSize
|
||||||
|
err = json.Unmarshal(msg.Data, &msgRemoteWinSize)
|
||||||
|
if err == nil {
|
||||||
|
onWinSize(msgRemoteWinSize.Cols, msgRemoteWinSize.Rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *TTYProtocolWSLocked) SetWinSize(cols, rows int) (err error) {
|
||||||
|
msgWinChanged := MsgTTYWinSize{
|
||||||
|
Cols: cols,
|
||||||
|
Rows: rows,
|
||||||
|
}
|
||||||
|
data, err := marshalMsg(msgWinChanged)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.lock.Lock()
|
||||||
|
err = handler.ws.WriteMessage(websocket.TextMessage, data)
|
||||||
|
handler.lock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send data from one the sender to the server and the other way around.
|
||||||
|
func (handler *TTYProtocolWSLocked) Write(buff []byte) (n int, err error) {
|
||||||
|
msgWrite := MsgTTYWrite{
|
||||||
|
Data: buff,
|
||||||
|
Size: len(buff),
|
||||||
|
}
|
||||||
|
data, err := marshalMsg(msgWrite)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.lock.Lock()
|
||||||
|
n, err = len(buff), handler.ws.WriteMessage(websocket.TextMessage, data)
|
||||||
|
handler.lock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TunInitMsg struct {
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSConnReadWriteCloser struct {
|
||||||
|
WsConn *websocket.Conn
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *WSConnReadWriteCloser) Read(p []byte) (n int, err error) {
|
||||||
|
// Weird method here, as we need to do a few things:
|
||||||
|
// - re-use the WS reader between different calls of this function. If the existing reader
|
||||||
|
// has no more data, then get another reader (NextReader())
|
||||||
|
// - if we get a CloseAbnormalClosure, or CloseGoingAway error message from WS, we need to
|
||||||
|
// transform that into a io.EOF, otherwise yamux will complain. We use yamux on top of this
|
||||||
|
// reader interface, in order to multiplex multiple streams
|
||||||
|
// More here:
|
||||||
|
// https://github.com/hashicorp/yamux/blob/574fd304fd659b0dfdd79e221f4e34f6b7cd9ed2/session.go#L554
|
||||||
|
// https://github.com/gorilla/websocket/blob/b65e62901fc1c0d968042419e74789f6af455eb9/examples/chat/client.go#L67
|
||||||
|
// https://stackoverflow.com/questions/61108552/go-websocket-error-close-1006-abnormal-closure-unexpected-eof
|
||||||
|
|
||||||
|
filterErr := func() {
|
||||||
|
|
||||||
|
if err != nil && !websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||||
|
// if we have an error != nil, and it's one of the two, then return EOF
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer filterErr()
|
||||||
|
|
||||||
|
if conn.reader != nil {
|
||||||
|
n, err = conn.reader.Read(p)
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
// if this reader has no more data, get the next reader
|
||||||
|
_, conn.reader, err = conn.WsConn.NextReader()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// and read in this same call as well
|
||||||
|
return conn.reader.Read(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, conn.reader, err = conn.WsConn.NextReader()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *WSConnReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||||
|
return len(p), conn.WsConn.WriteMessage(websocket.BinaryMessage, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *WSConnReadWriteCloser) Close() error {
|
||||||
|
return conn.WsConn.Close()
|
||||||
|
}
|
@ -1,118 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerSessionInfo struct {
|
|
||||||
URLWebReadWrite string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceiverSessionInfo struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type SenderSessionInfo struct {
|
|
||||||
Salt string
|
|
||||||
PasswordVerifierA string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TTYProtocolConn is the interface used to communicate with the sending (master) side of the TTY session
|
|
||||||
type TTYProtocolConn struct {
|
|
||||||
netConnection io.ReadWriteCloser
|
|
||||||
jsonDecoder *json.Decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTTYProtocolConn(conn io.ReadWriteCloser) *TTYProtocolConn {
|
|
||||||
return &TTYProtocolConn{
|
|
||||||
netConnection: conn,
|
|
||||||
jsonDecoder: json.NewDecoder(conn),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) ReadMessage() (msg MsgAll, err error) {
|
|
||||||
// TODO: perhaps read here the error, and transform it to something that's understandable
|
|
||||||
// from the outside in the context of this object
|
|
||||||
err = protoConn.jsonDecoder.Decode(&msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) SetWinSize(cols, rows int) error {
|
|
||||||
msgWinChanged := MsgTTYWinSize{
|
|
||||||
Cols: cols,
|
|
||||||
Rows: rows,
|
|
||||||
}
|
|
||||||
return MarshalAndWriteMsg(protoConn.netConnection, msgWinChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) Close() error {
|
|
||||||
return protoConn.netConnection.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to send data from one the sender to the server and the other way around.
|
|
||||||
func (protoConn *TTYProtocolConn) Write(buff []byte) (int, error) {
|
|
||||||
msgWrite := MsgTTYWrite{
|
|
||||||
Data: buff,
|
|
||||||
Size: len(buff),
|
|
||||||
}
|
|
||||||
return len(buff), MarshalAndWriteMsg(protoConn.netConnection, msgWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) WriteRawData(buff []byte) (int, error) {
|
|
||||||
return protoConn.netConnection.Write(buff)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to be called on the sender side, and which blocks until the protocol has been
|
|
||||||
// initialised
|
|
||||||
func (protoConn *TTYProtocolConn) InitSender(senderInfo SenderSessionInfo) (serverInfo ServerSessionInfo, err error) {
|
|
||||||
var replyMsg MsgTTYSenderInitReply
|
|
||||||
|
|
||||||
msgInitReq := MsgTTYSenderInitRequest{
|
|
||||||
Salt: senderInfo.Salt,
|
|
||||||
PasswordVerifierA: senderInfo.PasswordVerifierA,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the InitRequest message
|
|
||||||
if err = MarshalAndWriteMsg(protoConn.netConnection, msgInitReq); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait here for the InitReply message
|
|
||||||
if err = ReadAndUnmarshalMsg(protoConn.netConnection, &replyMsg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serverInfo = ServerSessionInfo{
|
|
||||||
URLWebReadWrite: replyMsg.ReceiverURLWebReadWrite,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) InitServer(serverInfo ServerSessionInfo) (senderInfo SenderSessionInfo, err error) {
|
|
||||||
var requestMsg MsgTTYSenderInitRequest
|
|
||||||
|
|
||||||
// Wait here and expect a InitRequest message
|
|
||||||
if err = ReadAndUnmarshalMsg(protoConn.netConnection, &requestMsg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send back a InitReply message
|
|
||||||
if err = MarshalAndWriteMsg(protoConn.netConnection, MsgTTYSenderInitReply{
|
|
||||||
ReceiverURLWebReadWrite: serverInfo.URLWebReadWrite}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
senderInfo = SenderSessionInfo{
|
|
||||||
Salt: requestMsg.Salt,
|
|
||||||
PasswordVerifierA: requestMsg.PasswordVerifierA,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) InitServerReceiverConn(serverInfo ServerSessionInfo) (receiverInfo ReceiverSessionInfo, err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (protoConn *TTYProtocolConn) InitReceiverServerConn(receiverInfo ReceiverSessionInfo) (serverInfo ServerSessionInfo, err error) {
|
|
||||||
return
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Microsoft Corporation
|
||||||
|
|
||||||
|
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.
|
@ -0,0 +1,12 @@
|
|||||||
|
# go-ansiterm
|
||||||
|
|
||||||
|
This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent.
|
||||||
|
|
||||||
|
For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position.
|
||||||
|
|
||||||
|
The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go).
|
||||||
|
|
||||||
|
See parser_test.go for examples exercising the state machine and generating appropriate function calls.
|
||||||
|
|
||||||
|
-----
|
||||||
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
@ -0,0 +1,188 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
const LogEnv = "DEBUG_TERMINAL"
|
||||||
|
|
||||||
|
// ANSI constants
|
||||||
|
// References:
|
||||||
|
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
|
||||||
|
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||||
|
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||||
|
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
|
// -- http://vt100.net/emu/dec_ansi_parser
|
||||||
|
// -- http://vt100.net/emu/vt500_parser.svg
|
||||||
|
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
// -- http://www.inwap.com/pdp10/ansicode.txt
|
||||||
|
const (
|
||||||
|
// ECMA-48 Set Graphics Rendition
|
||||||
|
// Note:
|
||||||
|
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
|
||||||
|
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
|
||||||
|
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
|
||||||
|
ANSI_SGR_RESET = 0
|
||||||
|
ANSI_SGR_BOLD = 1
|
||||||
|
ANSI_SGR_DIM = 2
|
||||||
|
_ANSI_SGR_ITALIC = 3
|
||||||
|
ANSI_SGR_UNDERLINE = 4
|
||||||
|
_ANSI_SGR_BLINKSLOW = 5
|
||||||
|
_ANSI_SGR_BLINKFAST = 6
|
||||||
|
ANSI_SGR_REVERSE = 7
|
||||||
|
_ANSI_SGR_INVISIBLE = 8
|
||||||
|
_ANSI_SGR_LINETHROUGH = 9
|
||||||
|
_ANSI_SGR_FONT_00 = 10
|
||||||
|
_ANSI_SGR_FONT_01 = 11
|
||||||
|
_ANSI_SGR_FONT_02 = 12
|
||||||
|
_ANSI_SGR_FONT_03 = 13
|
||||||
|
_ANSI_SGR_FONT_04 = 14
|
||||||
|
_ANSI_SGR_FONT_05 = 15
|
||||||
|
_ANSI_SGR_FONT_06 = 16
|
||||||
|
_ANSI_SGR_FONT_07 = 17
|
||||||
|
_ANSI_SGR_FONT_08 = 18
|
||||||
|
_ANSI_SGR_FONT_09 = 19
|
||||||
|
_ANSI_SGR_FONT_10 = 20
|
||||||
|
_ANSI_SGR_DOUBLEUNDERLINE = 21
|
||||||
|
ANSI_SGR_BOLD_DIM_OFF = 22
|
||||||
|
_ANSI_SGR_ITALIC_OFF = 23
|
||||||
|
ANSI_SGR_UNDERLINE_OFF = 24
|
||||||
|
_ANSI_SGR_BLINK_OFF = 25
|
||||||
|
_ANSI_SGR_RESERVED_00 = 26
|
||||||
|
ANSI_SGR_REVERSE_OFF = 27
|
||||||
|
_ANSI_SGR_INVISIBLE_OFF = 28
|
||||||
|
_ANSI_SGR_LINETHROUGH_OFF = 29
|
||||||
|
ANSI_SGR_FOREGROUND_BLACK = 30
|
||||||
|
ANSI_SGR_FOREGROUND_RED = 31
|
||||||
|
ANSI_SGR_FOREGROUND_GREEN = 32
|
||||||
|
ANSI_SGR_FOREGROUND_YELLOW = 33
|
||||||
|
ANSI_SGR_FOREGROUND_BLUE = 34
|
||||||
|
ANSI_SGR_FOREGROUND_MAGENTA = 35
|
||||||
|
ANSI_SGR_FOREGROUND_CYAN = 36
|
||||||
|
ANSI_SGR_FOREGROUND_WHITE = 37
|
||||||
|
_ANSI_SGR_RESERVED_01 = 38
|
||||||
|
ANSI_SGR_FOREGROUND_DEFAULT = 39
|
||||||
|
ANSI_SGR_BACKGROUND_BLACK = 40
|
||||||
|
ANSI_SGR_BACKGROUND_RED = 41
|
||||||
|
ANSI_SGR_BACKGROUND_GREEN = 42
|
||||||
|
ANSI_SGR_BACKGROUND_YELLOW = 43
|
||||||
|
ANSI_SGR_BACKGROUND_BLUE = 44
|
||||||
|
ANSI_SGR_BACKGROUND_MAGENTA = 45
|
||||||
|
ANSI_SGR_BACKGROUND_CYAN = 46
|
||||||
|
ANSI_SGR_BACKGROUND_WHITE = 47
|
||||||
|
_ANSI_SGR_RESERVED_02 = 48
|
||||||
|
ANSI_SGR_BACKGROUND_DEFAULT = 49
|
||||||
|
// 50 - 65: Unsupported
|
||||||
|
|
||||||
|
ANSI_MAX_CMD_LENGTH = 4096
|
||||||
|
|
||||||
|
MAX_INPUT_EVENTS = 128
|
||||||
|
DEFAULT_WIDTH = 80
|
||||||
|
DEFAULT_HEIGHT = 24
|
||||||
|
|
||||||
|
ANSI_BEL = 0x07
|
||||||
|
ANSI_BACKSPACE = 0x08
|
||||||
|
ANSI_TAB = 0x09
|
||||||
|
ANSI_LINE_FEED = 0x0A
|
||||||
|
ANSI_VERTICAL_TAB = 0x0B
|
||||||
|
ANSI_FORM_FEED = 0x0C
|
||||||
|
ANSI_CARRIAGE_RETURN = 0x0D
|
||||||
|
ANSI_ESCAPE_PRIMARY = 0x1B
|
||||||
|
ANSI_ESCAPE_SECONDARY = 0x5B
|
||||||
|
ANSI_OSC_STRING_ENTRY = 0x5D
|
||||||
|
ANSI_COMMAND_FIRST = 0x40
|
||||||
|
ANSI_COMMAND_LAST = 0x7E
|
||||||
|
DCS_ENTRY = 0x90
|
||||||
|
CSI_ENTRY = 0x9B
|
||||||
|
OSC_STRING = 0x9D
|
||||||
|
ANSI_PARAMETER_SEP = ";"
|
||||||
|
ANSI_CMD_G0 = '('
|
||||||
|
ANSI_CMD_G1 = ')'
|
||||||
|
ANSI_CMD_G2 = '*'
|
||||||
|
ANSI_CMD_G3 = '+'
|
||||||
|
ANSI_CMD_DECPNM = '>'
|
||||||
|
ANSI_CMD_DECPAM = '='
|
||||||
|
ANSI_CMD_OSC = ']'
|
||||||
|
ANSI_CMD_STR_TERM = '\\'
|
||||||
|
|
||||||
|
KEY_CONTROL_PARAM_2 = ";2"
|
||||||
|
KEY_CONTROL_PARAM_3 = ";3"
|
||||||
|
KEY_CONTROL_PARAM_4 = ";4"
|
||||||
|
KEY_CONTROL_PARAM_5 = ";5"
|
||||||
|
KEY_CONTROL_PARAM_6 = ";6"
|
||||||
|
KEY_CONTROL_PARAM_7 = ";7"
|
||||||
|
KEY_CONTROL_PARAM_8 = ";8"
|
||||||
|
KEY_ESC_CSI = "\x1B["
|
||||||
|
KEY_ESC_N = "\x1BN"
|
||||||
|
KEY_ESC_O = "\x1BO"
|
||||||
|
|
||||||
|
FILL_CHARACTER = ' '
|
||||||
|
)
|
||||||
|
|
||||||
|
func getByteRange(start byte, end byte) []byte {
|
||||||
|
bytes := make([]byte, 0, 32)
|
||||||
|
for i := start; i <= end; i++ {
|
||||||
|
bytes = append(bytes, byte(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
var toGroundBytes = getToGroundBytes()
|
||||||
|
var executors = getExecuteBytes()
|
||||||
|
|
||||||
|
// SPACE 20+A0 hex Always and everywhere a blank space
|
||||||
|
// Intermediate 20-2F hex !"#$%&'()*+,-./
|
||||||
|
var intermeds = getByteRange(0x20, 0x2F)
|
||||||
|
|
||||||
|
// Parameters 30-3F hex 0123456789:;<=>?
|
||||||
|
// CSI Parameters 30-39, 3B hex 0123456789;
|
||||||
|
var csiParams = getByteRange(0x30, 0x3F)
|
||||||
|
|
||||||
|
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
|
||||||
|
|
||||||
|
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
var upperCase = getByteRange(0x40, 0x5F)
|
||||||
|
|
||||||
|
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
|
||||||
|
var lowerCase = getByteRange(0x60, 0x7E)
|
||||||
|
|
||||||
|
// Alphabetics 40-7E hex (all of upper and lower case)
|
||||||
|
var alphabetics = append(upperCase, lowerCase...)
|
||||||
|
|
||||||
|
var printables = getByteRange(0x20, 0x7F)
|
||||||
|
|
||||||
|
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
|
||||||
|
var escapeToGroundBytes = getEscapeToGroundBytes()
|
||||||
|
|
||||||
|
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
|
||||||
|
// byte ranges below
|
||||||
|
|
||||||
|
func getEscapeToGroundBytes() []byte {
|
||||||
|
escapeToGroundBytes := getByteRange(0x30, 0x4F)
|
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
|
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
|
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
|
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
|
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
|
||||||
|
return escapeToGroundBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecuteBytes() []byte {
|
||||||
|
executeBytes := getByteRange(0x00, 0x17)
|
||||||
|
executeBytes = append(executeBytes, 0x19)
|
||||||
|
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
|
||||||
|
return executeBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToGroundBytes() []byte {
|
||||||
|
groundBytes := []byte{0x18}
|
||||||
|
groundBytes = append(groundBytes, 0x1A)
|
||||||
|
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
|
||||||
|
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
|
||||||
|
groundBytes = append(groundBytes, 0x99)
|
||||||
|
groundBytes = append(groundBytes, 0x9A)
|
||||||
|
groundBytes = append(groundBytes, 0x9C)
|
||||||
|
return groundBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 7F hex Always and everywhere ignored
|
||||||
|
// C1 Control 80-9F hex 32 additional control characters
|
||||||
|
// G1 Displayable A1-FE hex 94 additional displayable characters
|
||||||
|
// Special A0+FF hex Same as SPACE and DELETE
|
@ -0,0 +1,7 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type ansiContext struct {
|
||||||
|
currentChar byte
|
||||||
|
paramBuffer []byte
|
||||||
|
interBuffer []byte
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type csiEntryState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||||
|
csiState.parser.logf("CsiEntry::Handle %#x", b)
|
||||||
|
|
||||||
|
nextState, err := csiState.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sliceContains(alphabetics, b):
|
||||||
|
return csiState.parser.ground, nil
|
||||||
|
case sliceContains(csiCollectables, b):
|
||||||
|
return csiState.parser.csiParam, nil
|
||||||
|
case sliceContains(executors, b):
|
||||||
|
return csiState, csiState.parser.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
return csiState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csiState csiEntryState) Transition(s state) error {
|
||||||
|
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
|
||||||
|
csiState.baseState.Transition(s)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case csiState.parser.ground:
|
||||||
|
return csiState.parser.csiDispatch()
|
||||||
|
case csiState.parser.csiParam:
|
||||||
|
switch {
|
||||||
|
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||||
|
csiState.parser.collectParam()
|
||||||
|
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||||
|
csiState.parser.collectInter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csiState csiEntryState) Enter() error {
|
||||||
|
csiState.parser.clear()
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type csiParamState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||||
|
csiState.parser.logf("CsiParam::Handle %#x", b)
|
||||||
|
|
||||||
|
nextState, err := csiState.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sliceContains(alphabetics, b):
|
||||||
|
return csiState.parser.ground, nil
|
||||||
|
case sliceContains(csiCollectables, b):
|
||||||
|
csiState.parser.collectParam()
|
||||||
|
return csiState, nil
|
||||||
|
case sliceContains(executors, b):
|
||||||
|
return csiState, csiState.parser.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
return csiState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csiState csiParamState) Transition(s state) error {
|
||||||
|
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
|
||||||
|
csiState.baseState.Transition(s)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case csiState.parser.ground:
|
||||||
|
return csiState.parser.csiDispatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type escapeIntermediateState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||||
|
escState.parser.logf("escapeIntermediateState::Handle %#x", b)
|
||||||
|
nextState, err := escState.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sliceContains(intermeds, b):
|
||||||
|
return escState, escState.parser.collectInter()
|
||||||
|
case sliceContains(executors, b):
|
||||||
|
return escState, escState.parser.execute()
|
||||||
|
case sliceContains(escapeIntermediateToGroundBytes, b):
|
||||||
|
return escState.parser.ground, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return escState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (escState escapeIntermediateState) Transition(s state) error {
|
||||||
|
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
|
||||||
|
escState.baseState.Transition(s)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case escState.parser.ground:
|
||||||
|
return escState.parser.escDispatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type escapeState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||||
|
escState.parser.logf("escapeState::Handle %#x", b)
|
||||||
|
nextState, err := escState.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case b == ANSI_ESCAPE_SECONDARY:
|
||||||
|
return escState.parser.csiEntry, nil
|
||||||
|
case b == ANSI_OSC_STRING_ENTRY:
|
||||||
|
return escState.parser.oscString, nil
|
||||||
|
case sliceContains(executors, b):
|
||||||
|
return escState, escState.parser.execute()
|
||||||
|
case sliceContains(escapeToGroundBytes, b):
|
||||||
|
return escState.parser.ground, nil
|
||||||
|
case sliceContains(intermeds, b):
|
||||||
|
return escState.parser.escapeIntermediate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return escState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (escState escapeState) Transition(s state) error {
|
||||||
|
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
|
||||||
|
escState.baseState.Transition(s)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case escState.parser.ground:
|
||||||
|
return escState.parser.escDispatch()
|
||||||
|
case escState.parser.escapeIntermediate:
|
||||||
|
return escState.parser.collectInter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (escState escapeState) Enter() error {
|
||||||
|
escState.parser.clear()
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type AnsiEventHandler interface {
|
||||||
|
// Print
|
||||||
|
Print(b byte) error
|
||||||
|
|
||||||
|
// Execute C0 commands
|
||||||
|
Execute(b byte) error
|
||||||
|
|
||||||
|
// CUrsor Up
|
||||||
|
CUU(int) error
|
||||||
|
|
||||||
|
// CUrsor Down
|
||||||
|
CUD(int) error
|
||||||
|
|
||||||
|
// CUrsor Forward
|
||||||
|
CUF(int) error
|
||||||
|
|
||||||
|
// CUrsor Backward
|
||||||
|
CUB(int) error
|
||||||
|
|
||||||
|
// Cursor to Next Line
|
||||||
|
CNL(int) error
|
||||||
|
|
||||||
|
// Cursor to Previous Line
|
||||||
|
CPL(int) error
|
||||||
|
|
||||||
|
// Cursor Horizontal position Absolute
|
||||||
|
CHA(int) error
|
||||||
|
|
||||||
|
// Vertical line Position Absolute
|
||||||
|
VPA(int) error
|
||||||
|
|
||||||
|
// CUrsor Position
|
||||||
|
CUP(int, int) error
|
||||||
|
|
||||||
|
// Horizontal and Vertical Position (depends on PUM)
|
||||||
|
HVP(int, int) error
|
||||||
|
|
||||||
|
// Text Cursor Enable Mode
|
||||||
|
DECTCEM(bool) error
|
||||||
|
|
||||||
|
// Origin Mode
|
||||||
|
DECOM(bool) error
|
||||||
|
|
||||||
|
// 132 Column Mode
|
||||||
|
DECCOLM(bool) error
|
||||||
|
|
||||||
|
// Erase in Display
|
||||||
|
ED(int) error
|
||||||
|
|
||||||
|
// Erase in Line
|
||||||
|
EL(int) error
|
||||||
|
|
||||||
|
// Insert Line
|
||||||
|
IL(int) error
|
||||||
|
|
||||||
|
// Delete Line
|
||||||
|
DL(int) error
|
||||||
|
|
||||||
|
// Insert Character
|
||||||
|
ICH(int) error
|
||||||
|
|
||||||
|
// Delete Character
|
||||||
|
DCH(int) error
|
||||||
|
|
||||||
|
// Set Graphics Rendition
|
||||||
|
SGR([]int) error
|
||||||
|
|
||||||
|
// Pan Down
|
||||||
|
SU(int) error
|
||||||
|
|
||||||
|
// Pan Up
|
||||||
|
SD(int) error
|
||||||
|
|
||||||
|
// Device Attributes
|
||||||
|
DA([]string) error
|
||||||
|
|
||||||
|
// Set Top and Bottom Margins
|
||||||
|
DECSTBM(int, int) error
|
||||||
|
|
||||||
|
// Index
|
||||||
|
IND() error
|
||||||
|
|
||||||
|
// Reverse Index
|
||||||
|
RI() error
|
||||||
|
|
||||||
|
// Flush updates from previous commands
|
||||||
|
Flush() error
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type groundState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs groundState) Handle(b byte) (s state, e error) {
|
||||||
|
gs.parser.context.currentChar = b
|
||||||
|
|
||||||
|
nextState, err := gs.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sliceContains(printables, b):
|
||||||
|
return gs, gs.parser.print()
|
||||||
|
|
||||||
|
case sliceContains(executors, b):
|
||||||
|
return gs, gs.parser.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
return gs, nil
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type oscStringState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oscState oscStringState) Handle(b byte) (s state, e error) {
|
||||||
|
oscState.parser.logf("OscString::Handle %#x", b)
|
||||||
|
nextState, err := oscState.baseState.Handle(b)
|
||||||
|
if nextState != nil || err != nil {
|
||||||
|
return nextState, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isOscStringTerminator(b):
|
||||||
|
return oscState.parser.ground, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return oscState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// See below for OSC string terminators for linux
|
||||||
|
// http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||||
|
func isOscStringTerminator(b byte) bool {
|
||||||
|
|
||||||
|
if b == ANSI_BEL || b == 0x5C {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnsiParser struct {
|
||||||
|
currState state
|
||||||
|
eventHandler AnsiEventHandler
|
||||||
|
context *ansiContext
|
||||||
|
csiEntry state
|
||||||
|
csiParam state
|
||||||
|
dcsEntry state
|
||||||
|
escape state
|
||||||
|
escapeIntermediate state
|
||||||
|
error state
|
||||||
|
ground state
|
||||||
|
oscString state
|
||||||
|
stateMap []state
|
||||||
|
|
||||||
|
logf func(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*AnsiParser)
|
||||||
|
|
||||||
|
func WithLogf(f func(string, ...interface{})) Option {
|
||||||
|
return func(ap *AnsiParser) {
|
||||||
|
ap.logf = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser {
|
||||||
|
ap := &AnsiParser{
|
||||||
|
eventHandler: evtHandler,
|
||||||
|
context: &ansiContext{},
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(ap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
|
||||||
|
logFile, _ := os.Create("ansiParser.log")
|
||||||
|
logger := log.New(logFile, "", log.LstdFlags)
|
||||||
|
if ap.logf != nil {
|
||||||
|
l := ap.logf
|
||||||
|
ap.logf = func(s string, v ...interface{}) {
|
||||||
|
l(s, v...)
|
||||||
|
logger.Printf(s, v...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ap.logf = logger.Printf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ap.logf == nil {
|
||||||
|
ap.logf = func(string, ...interface{}) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}
|
||||||
|
ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}}
|
||||||
|
ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}}
|
||||||
|
ap.escape = escapeState{baseState{name: "Escape", parser: ap}}
|
||||||
|
ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}}
|
||||||
|
ap.error = errorState{baseState{name: "Error", parser: ap}}
|
||||||
|
ap.ground = groundState{baseState{name: "Ground", parser: ap}}
|
||||||
|
ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}}
|
||||||
|
|
||||||
|
ap.stateMap = []state{
|
||||||
|
ap.csiEntry,
|
||||||
|
ap.csiParam,
|
||||||
|
ap.dcsEntry,
|
||||||
|
ap.escape,
|
||||||
|
ap.escapeIntermediate,
|
||||||
|
ap.error,
|
||||||
|
ap.ground,
|
||||||
|
ap.oscString,
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.currState = getState(initialState, ap.stateMap)
|
||||||
|
|
||||||
|
ap.logf("CreateParser: parser %p", ap)
|
||||||
|
return ap
|
||||||
|
}
|
||||||
|
|
||||||
|
func getState(name string, states []state) state {
|
||||||
|
for _, el := range states {
|
||||||
|
if el.Name() == name {
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
|
||||||
|
for i, b := range bytes {
|
||||||
|
if err := ap.handle(b); err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(bytes), ap.eventHandler.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) handle(b byte) error {
|
||||||
|
ap.context.currentChar = b
|
||||||
|
newState, err := ap.currState.Handle(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if newState == nil {
|
||||||
|
ap.logf("WARNING: newState is nil")
|
||||||
|
return errors.New("New state of 'nil' is invalid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if newState != ap.currState {
|
||||||
|
if err := ap.changeState(newState); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) changeState(newState state) error {
|
||||||
|
ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
|
||||||
|
|
||||||
|
// Exit old state
|
||||||
|
if err := ap.currState.Exit(); err != nil {
|
||||||
|
ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform transition action
|
||||||
|
if err := ap.currState.Transition(newState); err != nil {
|
||||||
|
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter new state
|
||||||
|
if err := newState.Enter(); err != nil {
|
||||||
|
ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.currState = newState
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseParams(bytes []byte) ([]string, error) {
|
||||||
|
paramBuff := make([]byte, 0, 0)
|
||||||
|
params := []string{}
|
||||||
|
|
||||||
|
for _, v := range bytes {
|
||||||
|
if v == ';' {
|
||||||
|
if len(paramBuff) > 0 {
|
||||||
|
// Completed parameter, append it to the list
|
||||||
|
s := string(paramBuff)
|
||||||
|
params = append(params, s)
|
||||||
|
paramBuff = make([]byte, 0, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paramBuff = append(paramBuff, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last parameter may not be terminated with ';'
|
||||||
|
if len(paramBuff) > 0 {
|
||||||
|
s := string(paramBuff)
|
||||||
|
params = append(params, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCmd(context ansiContext) (string, error) {
|
||||||
|
return string(context.currentChar), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInt(params []string, dflt int) int {
|
||||||
|
i := getInts(params, 1, dflt)[0]
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInts(params []string, minCount int, dflt int) []int {
|
||||||
|
ints := []int{}
|
||||||
|
|
||||||
|
for _, v := range params {
|
||||||
|
i, _ := strconv.Atoi(v)
|
||||||
|
// Zero is mapped to the default value in VT100.
|
||||||
|
if i == 0 {
|
||||||
|
i = dflt
|
||||||
|
}
|
||||||
|
ints = append(ints, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ints) < minCount {
|
||||||
|
remaining := minCount - len(ints)
|
||||||
|
for i := 0; i < remaining; i++ {
|
||||||
|
ints = append(ints, dflt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) modeDispatch(param string, set bool) error {
|
||||||
|
switch param {
|
||||||
|
case "?3":
|
||||||
|
return ap.eventHandler.DECCOLM(set)
|
||||||
|
case "?6":
|
||||||
|
return ap.eventHandler.DECOM(set)
|
||||||
|
case "?25":
|
||||||
|
return ap.eventHandler.DECTCEM(set)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) hDispatch(params []string) error {
|
||||||
|
if len(params) == 1 {
|
||||||
|
return ap.modeDispatch(params[0], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) lDispatch(params []string) error {
|
||||||
|
if len(params) == 1 {
|
||||||
|
return ap.modeDispatch(params[0], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEraseParam(params []string) int {
|
||||||
|
param := getInt(params, 0)
|
||||||
|
if param < 0 || 3 < param {
|
||||||
|
param = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return param
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
func (ap *AnsiParser) collectParam() error {
|
||||||
|
currChar := ap.context.currentChar
|
||||||
|
ap.logf("collectParam %#x", currChar)
|
||||||
|
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) collectInter() error {
|
||||||
|
currChar := ap.context.currentChar
|
||||||
|
ap.logf("collectInter %#x", currChar)
|
||||||
|
ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) escDispatch() error {
|
||||||
|
cmd, _ := parseCmd(*ap.context)
|
||||||
|
intermeds := ap.context.interBuffer
|
||||||
|
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
|
||||||
|
ap.logf("escDispatch: %v(%v)", cmd, intermeds)
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "D": // IND
|
||||||
|
return ap.eventHandler.IND()
|
||||||
|
case "E": // NEL, equivalent to CRLF
|
||||||
|
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
|
||||||
|
if err == nil {
|
||||||
|
err = ap.eventHandler.Execute(ANSI_LINE_FEED)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
case "M": // RI
|
||||||
|
return ap.eventHandler.RI()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) csiDispatch() error {
|
||||||
|
cmd, _ := parseCmd(*ap.context)
|
||||||
|
params, _ := parseParams(ap.context.paramBuffer)
|
||||||
|
ap.logf("Parsed params: %v with length: %d", params, len(params))
|
||||||
|
|
||||||
|
ap.logf("csiDispatch: %v(%v)", cmd, params)
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "@":
|
||||||
|
return ap.eventHandler.ICH(getInt(params, 1))
|
||||||
|
case "A":
|
||||||
|
return ap.eventHandler.CUU(getInt(params, 1))
|
||||||
|
case "B":
|
||||||
|
return ap.eventHandler.CUD(getInt(params, 1))
|
||||||
|
case "C":
|
||||||
|
return ap.eventHandler.CUF(getInt(params, 1))
|
||||||
|
case "D":
|
||||||
|
return ap.eventHandler.CUB(getInt(params, 1))
|
||||||
|
case "E":
|
||||||
|
return ap.eventHandler.CNL(getInt(params, 1))
|
||||||
|
case "F":
|
||||||
|
return ap.eventHandler.CPL(getInt(params, 1))
|
||||||
|
case "G":
|
||||||
|
return ap.eventHandler.CHA(getInt(params, 1))
|
||||||
|
case "H":
|
||||||
|
ints := getInts(params, 2, 1)
|
||||||
|
x, y := ints[0], ints[1]
|
||||||
|
return ap.eventHandler.CUP(x, y)
|
||||||
|
case "J":
|
||||||
|
param := getEraseParam(params)
|
||||||
|
return ap.eventHandler.ED(param)
|
||||||
|
case "K":
|
||||||
|
param := getEraseParam(params)
|
||||||
|
return ap.eventHandler.EL(param)
|
||||||
|
case "L":
|
||||||
|
return ap.eventHandler.IL(getInt(params, 1))
|
||||||
|
case "M":
|
||||||
|
return ap.eventHandler.DL(getInt(params, 1))
|
||||||
|
case "P":
|
||||||
|
return ap.eventHandler.DCH(getInt(params, 1))
|
||||||
|
case "S":
|
||||||
|
return ap.eventHandler.SU(getInt(params, 1))
|
||||||
|
case "T":
|
||||||
|
return ap.eventHandler.SD(getInt(params, 1))
|
||||||
|
case "c":
|
||||||
|
return ap.eventHandler.DA(params)
|
||||||
|
case "d":
|
||||||
|
return ap.eventHandler.VPA(getInt(params, 1))
|
||||||
|
case "f":
|
||||||
|
ints := getInts(params, 2, 1)
|
||||||
|
x, y := ints[0], ints[1]
|
||||||
|
return ap.eventHandler.HVP(x, y)
|
||||||
|
case "h":
|
||||||
|
return ap.hDispatch(params)
|
||||||
|
case "l":
|
||||||
|
return ap.lDispatch(params)
|
||||||
|
case "m":
|
||||||
|
return ap.eventHandler.SGR(getInts(params, 1, 0))
|
||||||
|
case "r":
|
||||||
|
ints := getInts(params, 2, 1)
|
||||||
|
top, bottom := ints[0], ints[1]
|
||||||
|
return ap.eventHandler.DECSTBM(top, bottom)
|
||||||
|
default:
|
||||||
|
ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) print() error {
|
||||||
|
return ap.eventHandler.Print(ap.context.currentChar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) clear() error {
|
||||||
|
ap.context = &ansiContext{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *AnsiParser) execute() error {
|
||||||
|
return ap.eventHandler.Execute(ap.context.currentChar)
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
type stateID int
|
||||||
|
|
||||||
|
type state interface {
|
||||||
|
Enter() error
|
||||||
|
Exit() error
|
||||||
|
Handle(byte) (state, error)
|
||||||
|
Name() string
|
||||||
|
Transition(state) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseState struct {
|
||||||
|
name string
|
||||||
|
parser *AnsiParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (base baseState) Enter() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (base baseState) Exit() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (base baseState) Handle(b byte) (s state, e error) {
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case b == CSI_ENTRY:
|
||||||
|
return base.parser.csiEntry, nil
|
||||||
|
case b == DCS_ENTRY:
|
||||||
|
return base.parser.dcsEntry, nil
|
||||||
|
case b == ANSI_ESCAPE_PRIMARY:
|
||||||
|
return base.parser.escape, nil
|
||||||
|
case b == OSC_STRING:
|
||||||
|
return base.parser.oscString, nil
|
||||||
|
case sliceContains(toGroundBytes, b):
|
||||||
|
return base.parser.ground, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (base baseState) Name() string {
|
||||||
|
return base.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (base baseState) Transition(s state) error {
|
||||||
|
if s == base.parser.ground {
|
||||||
|
execBytes := []byte{0x18}
|
||||||
|
execBytes = append(execBytes, 0x1A)
|
||||||
|
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
|
||||||
|
execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
|
||||||
|
execBytes = append(execBytes, 0x99)
|
||||||
|
execBytes = append(execBytes, 0x9A)
|
||||||
|
|
||||||
|
if sliceContains(execBytes, base.parser.context.currentChar) {
|
||||||
|
return base.parser.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dcsEntryState struct {
|
||||||
|
baseState
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorState struct {
|
||||||
|
baseState
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package ansiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sliceContains(bytes []byte, b byte) bool {
|
||||||
|
for _, v := range bytes {
|
||||||
|
if v == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBytesToInteger(bytes []byte) int {
|
||||||
|
s := string(bytes)
|
||||||
|
i, _ := strconv.Atoi(s)
|
||||||
|
return i
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/Azure/go-ansiterm"
|
||||||
|
windows "golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows keyboard constants
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
|
||||||
|
const (
|
||||||
|
VK_PRIOR = 0x21 // PAGE UP key
|
||||||
|
VK_NEXT = 0x22 // PAGE DOWN key
|
||||||
|
VK_END = 0x23 // END key
|
||||||
|
VK_HOME = 0x24 // HOME key
|
||||||
|
VK_LEFT = 0x25 // LEFT ARROW key
|
||||||
|
VK_UP = 0x26 // UP ARROW key
|
||||||
|
VK_RIGHT = 0x27 // RIGHT ARROW key
|
||||||
|
VK_DOWN = 0x28 // DOWN ARROW key
|
||||||
|
VK_SELECT = 0x29 // SELECT key
|
||||||
|
VK_PRINT = 0x2A // PRINT key
|
||||||
|
VK_EXECUTE = 0x2B // EXECUTE key
|
||||||
|
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
|
||||||
|
VK_INSERT = 0x2D // INS key
|
||||||
|
VK_DELETE = 0x2E // DEL key
|
||||||
|
VK_HELP = 0x2F // HELP key
|
||||||
|
VK_F1 = 0x70 // F1 key
|
||||||
|
VK_F2 = 0x71 // F2 key
|
||||||
|
VK_F3 = 0x72 // F3 key
|
||||||
|
VK_F4 = 0x73 // F4 key
|
||||||
|
VK_F5 = 0x74 // F5 key
|
||||||
|
VK_F6 = 0x75 // F6 key
|
||||||
|
VK_F7 = 0x76 // F7 key
|
||||||
|
VK_F8 = 0x77 // F8 key
|
||||||
|
VK_F9 = 0x78 // F9 key
|
||||||
|
VK_F10 = 0x79 // F10 key
|
||||||
|
VK_F11 = 0x7A // F11 key
|
||||||
|
VK_F12 = 0x7B // F12 key
|
||||||
|
|
||||||
|
RIGHT_ALT_PRESSED = 0x0001
|
||||||
|
LEFT_ALT_PRESSED = 0x0002
|
||||||
|
RIGHT_CTRL_PRESSED = 0x0004
|
||||||
|
LEFT_CTRL_PRESSED = 0x0008
|
||||||
|
SHIFT_PRESSED = 0x0010
|
||||||
|
NUMLOCK_ON = 0x0020
|
||||||
|
SCROLLLOCK_ON = 0x0040
|
||||||
|
CAPSLOCK_ON = 0x0080
|
||||||
|
ENHANCED_KEY = 0x0100
|
||||||
|
)
|
||||||
|
|
||||||
|
type ansiCommand struct {
|
||||||
|
CommandBytes []byte
|
||||||
|
Command string
|
||||||
|
Parameters []string
|
||||||
|
IsSpecial bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnsiCommand(command []byte) *ansiCommand {
|
||||||
|
|
||||||
|
if isCharacterSelectionCmdChar(command[1]) {
|
||||||
|
// Is Character Set Selection commands
|
||||||
|
return &ansiCommand{
|
||||||
|
CommandBytes: command,
|
||||||
|
Command: string(command),
|
||||||
|
IsSpecial: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// last char is command character
|
||||||
|
lastCharIndex := len(command) - 1
|
||||||
|
|
||||||
|
ac := &ansiCommand{
|
||||||
|
CommandBytes: command,
|
||||||
|
Command: string(command[lastCharIndex]),
|
||||||
|
IsSpecial: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// more than a single escape
|
||||||
|
if lastCharIndex != 0 {
|
||||||
|
start := 1
|
||||||
|
// skip if double char escape sequence
|
||||||
|
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
// convert this to GetNextParam method
|
||||||
|
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
|
||||||
|
if index < 0 || index >= len(ac.Parameters) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return int16(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *ansiCommand) String() string {
|
||||||
|
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
|
||||||
|
bytesToHex(ac.CommandBytes),
|
||||||
|
ac.Command,
|
||||||
|
strings.Join(ac.Parameters, "\",\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
|
||||||
|
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
|
||||||
|
func isAnsiCommandChar(b byte) bool {
|
||||||
|
switch {
|
||||||
|
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
|
||||||
|
return true
|
||||||
|
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
|
||||||
|
// non-CSI escape sequence terminator
|
||||||
|
return true
|
||||||
|
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
|
||||||
|
// String escape sequence terminator
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isXtermOscSequence(command []byte, current byte) bool {
|
||||||
|
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCharacterSelectionCmdChar(b byte) bool {
|
||||||
|
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytesToHex converts a slice of bytes to a human-readable string.
|
||||||
|
func bytesToHex(b []byte) string {
|
||||||
|
hex := make([]string, len(b))
|
||||||
|
for i, ch := range b {
|
||||||
|
hex[i] = fmt.Sprintf("%X", ch)
|
||||||
|
}
|
||||||
|
return strings.Join(hex, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
|
||||||
|
// the passed min / max range.
|
||||||
|
func ensureInRange(n int16, min int16, max int16) int16 {
|
||||||
|
if n < min {
|
||||||
|
return min
|
||||||
|
} else if n > max {
|
||||||
|
return max
|
||||||
|
} else {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStdFile(nFile int) (*os.File, uintptr) {
|
||||||
|
var file *os.File
|
||||||
|
|
||||||
|
// syscall uses negative numbers
|
||||||
|
// windows package uses very big uint32
|
||||||
|
// Keep these switches split so we don't have to convert ints too much.
|
||||||
|
switch uint32(nFile) {
|
||||||
|
case windows.STD_INPUT_HANDLE:
|
||||||
|
file = os.Stdin
|
||||||
|
case windows.STD_OUTPUT_HANDLE:
|
||||||
|
file = os.Stdout
|
||||||
|
case windows.STD_ERROR_HANDLE:
|
||||||
|
file = os.Stderr
|
||||||
|
default:
|
||||||
|
switch nFile {
|
||||||
|
case syscall.STD_INPUT_HANDLE:
|
||||||
|
file = os.Stdin
|
||||||
|
case syscall.STD_OUTPUT_HANDLE:
|
||||||
|
file = os.Stdout
|
||||||
|
case syscall.STD_ERROR_HANDLE:
|
||||||
|
file = os.Stderr
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := syscall.GetStdHandle(nFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, uintptr(fd)
|
||||||
|
}
|
@ -0,0 +1,327 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//===========================================================================================================
|
||||||
|
// IMPORTANT NOTE:
|
||||||
|
//
|
||||||
|
// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
|
||||||
|
// Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
|
||||||
|
// variables) the pointers reference *before* the API completes.
|
||||||
|
//
|
||||||
|
// As a result, in those cases, the code must hint that the variables remain in active by invoking the
|
||||||
|
// dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
|
||||||
|
// require unsafe pointers.
|
||||||
|
//
|
||||||
|
// If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
|
||||||
|
// the garbage collector the variables remain in use if:
|
||||||
|
//
|
||||||
|
// -- The value is not a pointer (e.g., int32, struct)
|
||||||
|
// -- The value is not referenced by the method after passing the pointer to Windows
|
||||||
|
//
|
||||||
|
// See http://golang.org/doc/go1.3.
|
||||||
|
//===========================================================================================================
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
|
||||||
|
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
|
||||||
|
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
|
||||||
|
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
|
||||||
|
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
|
||||||
|
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
|
||||||
|
scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
|
||||||
|
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
|
||||||
|
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
|
||||||
|
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
|
||||||
|
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
|
||||||
|
waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows Console constants
|
||||||
|
const (
|
||||||
|
// Console modes
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||||
|
ENABLE_PROCESSED_INPUT = 0x0001
|
||||||
|
ENABLE_LINE_INPUT = 0x0002
|
||||||
|
ENABLE_ECHO_INPUT = 0x0004
|
||||||
|
ENABLE_WINDOW_INPUT = 0x0008
|
||||||
|
ENABLE_MOUSE_INPUT = 0x0010
|
||||||
|
ENABLE_INSERT_MODE = 0x0020
|
||||||
|
ENABLE_QUICK_EDIT_MODE = 0x0040
|
||||||
|
ENABLE_EXTENDED_FLAGS = 0x0080
|
||||||
|
ENABLE_AUTO_POSITION = 0x0100
|
||||||
|
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
||||||
|
|
||||||
|
ENABLE_PROCESSED_OUTPUT = 0x0001
|
||||||
|
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
||||||
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||||
|
DISABLE_NEWLINE_AUTO_RETURN = 0x0008
|
||||||
|
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
|
||||||
|
|
||||||
|
// Character attributes
|
||||||
|
// Note:
|
||||||
|
// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
|
||||||
|
// Clearing all foreground or background colors results in black; setting all creates white.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
|
||||||
|
FOREGROUND_BLUE uint16 = 0x0001
|
||||||
|
FOREGROUND_GREEN uint16 = 0x0002
|
||||||
|
FOREGROUND_RED uint16 = 0x0004
|
||||||
|
FOREGROUND_INTENSITY uint16 = 0x0008
|
||||||
|
FOREGROUND_MASK uint16 = 0x000F
|
||||||
|
|
||||||
|
BACKGROUND_BLUE uint16 = 0x0010
|
||||||
|
BACKGROUND_GREEN uint16 = 0x0020
|
||||||
|
BACKGROUND_RED uint16 = 0x0040
|
||||||
|
BACKGROUND_INTENSITY uint16 = 0x0080
|
||||||
|
BACKGROUND_MASK uint16 = 0x00F0
|
||||||
|
|
||||||
|
COMMON_LVB_MASK uint16 = 0xFF00
|
||||||
|
COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000
|
||||||
|
COMMON_LVB_UNDERSCORE uint16 = 0x8000
|
||||||
|
|
||||||
|
// Input event types
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||||
|
KEY_EVENT = 0x0001
|
||||||
|
MOUSE_EVENT = 0x0002
|
||||||
|
WINDOW_BUFFER_SIZE_EVENT = 0x0004
|
||||||
|
MENU_EVENT = 0x0008
|
||||||
|
FOCUS_EVENT = 0x0010
|
||||||
|
|
||||||
|
// WaitForSingleObject return codes
|
||||||
|
WAIT_ABANDONED = 0x00000080
|
||||||
|
WAIT_FAILED = 0xFFFFFFFF
|
||||||
|
WAIT_SIGNALED = 0x0000000
|
||||||
|
WAIT_TIMEOUT = 0x00000102
|
||||||
|
|
||||||
|
// WaitForSingleObject wait duration
|
||||||
|
WAIT_INFINITE = 0xFFFFFFFF
|
||||||
|
WAIT_ONE_SECOND = 1000
|
||||||
|
WAIT_HALF_SECOND = 500
|
||||||
|
WAIT_QUARTER_SECOND = 250
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows API Console types
|
||||||
|
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
|
||||||
|
// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
|
||||||
|
type (
|
||||||
|
CHAR_INFO struct {
|
||||||
|
UnicodeChar uint16
|
||||||
|
Attributes uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLE_CURSOR_INFO struct {
|
||||||
|
Size uint32
|
||||||
|
Visible int32
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLE_SCREEN_BUFFER_INFO struct {
|
||||||
|
Size COORD
|
||||||
|
CursorPosition COORD
|
||||||
|
Attributes uint16
|
||||||
|
Window SMALL_RECT
|
||||||
|
MaximumWindowSize COORD
|
||||||
|
}
|
||||||
|
|
||||||
|
COORD struct {
|
||||||
|
X int16
|
||||||
|
Y int16
|
||||||
|
}
|
||||||
|
|
||||||
|
SMALL_RECT struct {
|
||||||
|
Left int16
|
||||||
|
Top int16
|
||||||
|
Right int16
|
||||||
|
Bottom int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||||
|
INPUT_RECORD struct {
|
||||||
|
EventType uint16
|
||||||
|
KeyEvent KEY_EVENT_RECORD
|
||||||
|
}
|
||||||
|
|
||||||
|
KEY_EVENT_RECORD struct {
|
||||||
|
KeyDown int32
|
||||||
|
RepeatCount uint16
|
||||||
|
VirtualKeyCode uint16
|
||||||
|
VirtualScanCode uint16
|
||||||
|
UnicodeChar uint16
|
||||||
|
ControlKeyState uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
WINDOW_BUFFER_SIZE struct {
|
||||||
|
Size COORD
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// boolToBOOL converts a Go bool into a Windows int32.
|
||||||
|
func boolToBOOL(f bool) int32 {
|
||||||
|
if f {
|
||||||
|
return int32(1)
|
||||||
|
} else {
|
||||||
|
return int32(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
|
||||||
|
func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
|
||||||
|
r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
|
||||||
|
func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
|
||||||
|
r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleCursorPosition location of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
|
||||||
|
func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
|
||||||
|
r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
|
||||||
|
use(coord)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConsoleMode gets the console mode for given file descriptor
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
|
||||||
|
func GetConsoleMode(handle uintptr) (mode uint32, err error) {
|
||||||
|
err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
|
||||||
|
return mode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleMode sets the console mode for given file descriptor
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||||
|
func SetConsoleMode(handle uintptr, mode uint32) error {
|
||||||
|
r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
|
||||||
|
use(mode)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
|
||||||
|
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||||
|
info := CONSOLE_SCREEN_BUFFER_INFO{}
|
||||||
|
err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
|
||||||
|
r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
|
||||||
|
use(scrollRect)
|
||||||
|
use(clipRect)
|
||||||
|
use(destOrigin)
|
||||||
|
use(char)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleScreenBufferSize sets the size of the console screen buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
|
||||||
|
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
|
||||||
|
r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
|
||||||
|
use(coord)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleTextAttribute sets the attributes of characters written to the
|
||||||
|
// console screen buffer by the WriteFile or WriteConsole function.
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
|
||||||
|
func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
|
||||||
|
r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
|
||||||
|
use(attribute)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
|
||||||
|
// Note that the size and location must be within and no larger than the backing console screen buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
|
||||||
|
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
|
||||||
|
r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
|
||||||
|
use(isAbsolute)
|
||||||
|
use(rect)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
|
||||||
|
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
|
||||||
|
r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
|
||||||
|
use(buffer)
|
||||||
|
use(bufferSize)
|
||||||
|
use(bufferCoord)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConsoleInput reads (and removes) data from the console input buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
|
||||||
|
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
|
||||||
|
r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
|
||||||
|
use(buffer)
|
||||||
|
return checkError(r1, r2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForSingleObject waits for the passed handle to be signaled.
|
||||||
|
// It returns true if the handle was signaled; false otherwise.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
|
||||||
|
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
|
||||||
|
r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait)))
|
||||||
|
switch r1 {
|
||||||
|
case WAIT_ABANDONED, WAIT_TIMEOUT:
|
||||||
|
return false, nil
|
||||||
|
case WAIT_SIGNALED:
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
use(msWait)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String helpers
|
||||||
|
func (info CONSOLE_SCREEN_BUFFER_INFO) String() string {
|
||||||
|
return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (coord COORD) String() string {
|
||||||
|
return fmt.Sprintf("%v,%v", coord.X, coord.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rect SMALL_RECT) String() string {
|
||||||
|
return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkError evaluates the results of a Windows API call and returns the error if it failed.
|
||||||
|
func checkError(r1, r2 uintptr, err error) error {
|
||||||
|
// Windows APIs return non-zero to indicate success
|
||||||
|
if r1 != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the error if provided, otherwise default to EINVAL
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
// coordToPointer converts a COORD into a uintptr (by fooling the type system).
|
||||||
|
func coordToPointer(c COORD) uintptr {
|
||||||
|
// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
|
||||||
|
return uintptr(*((*uint32)(unsafe.Pointer(&c))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// use is a no-op, but the compiler cannot see that it is.
|
||||||
|
// Calling use(p) ensures that p is kept live until that point.
|
||||||
|
func use(p interface{}) {}
|
@ -0,0 +1,100 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
import "github.com/Azure/go-ansiterm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||||
|
BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||||
|
)
|
||||||
|
|
||||||
|
// collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the
|
||||||
|
// request represented by the passed ANSI mode.
|
||||||
|
func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) {
|
||||||
|
switch ansiMode {
|
||||||
|
|
||||||
|
// Mode styles
|
||||||
|
case ansiterm.ANSI_SGR_BOLD:
|
||||||
|
windowsMode = windowsMode | FOREGROUND_INTENSITY
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF:
|
||||||
|
windowsMode &^= FOREGROUND_INTENSITY
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_UNDERLINE:
|
||||||
|
windowsMode = windowsMode | COMMON_LVB_UNDERSCORE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_REVERSE:
|
||||||
|
inverted = true
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_REVERSE_OFF:
|
||||||
|
inverted = false
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_UNDERLINE_OFF:
|
||||||
|
windowsMode &^= COMMON_LVB_UNDERSCORE
|
||||||
|
|
||||||
|
// Foreground colors
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK)
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_BLACK:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK)
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_RED:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_GREEN:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_YELLOW:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_BLUE:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_CYAN:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_WHITE:
|
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
|
||||||
|
|
||||||
|
// Background colors
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT:
|
||||||
|
// Black with no intensity
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK)
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_BLACK:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK)
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_RED:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_GREEN:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_YELLOW:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_BLUE:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_CYAN:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_WHITE:
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
|
||||||
|
}
|
||||||
|
|
||||||
|
return windowsMode, inverted
|
||||||
|
}
|
||||||
|
|
||||||
|
// invertAttributes inverts the foreground and background colors of a Windows attributes value
|
||||||
|
func invertAttributes(windowsMode uint16) uint16 {
|
||||||
|
return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4)
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
const (
|
||||||
|
horizontal = iota
|
||||||
|
vertical
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT {
|
||||||
|
if h.originMode {
|
||||||
|
sr := h.effectiveSr(info.Window)
|
||||||
|
return SMALL_RECT{
|
||||||
|
Top: sr.top,
|
||||||
|
Bottom: sr.bottom,
|
||||||
|
Left: 0,
|
||||||
|
Right: info.Size.X - 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return SMALL_RECT{
|
||||||
|
Top: info.Window.Top,
|
||||||
|
Bottom: info.Window.Bottom,
|
||||||
|
Left: 0,
|
||||||
|
Right: info.Size.X - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCursorPosition sets the cursor to the specified position, bounded to the screen size
|
||||||
|
func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error {
|
||||||
|
position.X = ensureInRange(position.X, window.Left, window.Right)
|
||||||
|
position.Y = ensureInRange(position.Y, window.Top, window.Bottom)
|
||||||
|
err := SetConsoleCursorPosition(h.fd, position)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("Cursor position set: (%d, %d)", position.X, position.Y)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error {
|
||||||
|
return h.moveCursor(vertical, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error {
|
||||||
|
return h.moveCursor(horizontal, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
position := info.CursorPosition
|
||||||
|
switch moveMode {
|
||||||
|
case horizontal:
|
||||||
|
position.X += int16(param)
|
||||||
|
case vertical:
|
||||||
|
position.Y += int16(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorLine(param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
position := info.CursorPosition
|
||||||
|
position.X = 0
|
||||||
|
position.Y += int16(param)
|
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
position := info.CursorPosition
|
||||||
|
position.X = int16(param) - 1
|
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
import "github.com/Azure/go-ansiterm"
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error {
|
||||||
|
// Ignore an invalid (negative area) request
|
||||||
|
if toCoord.Y < fromCoord.Y {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var coordStart = COORD{}
|
||||||
|
var coordEnd = COORD{}
|
||||||
|
|
||||||
|
xCurrent, yCurrent := fromCoord.X, fromCoord.Y
|
||||||
|
xEnd, yEnd := toCoord.X, toCoord.Y
|
||||||
|
|
||||||
|
// Clear any partial initial line
|
||||||
|
if xCurrent > 0 {
|
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yCurrent
|
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xCurrent = 0
|
||||||
|
yCurrent += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear intervening rectangular section
|
||||||
|
if yCurrent < yEnd {
|
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yEnd-1
|
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xCurrent = 0
|
||||||
|
yCurrent = yEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear remaining partial ending line
|
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent
|
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yEnd
|
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error {
|
||||||
|
region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X}
|
||||||
|
width := toCoord.X - fromCoord.X + 1
|
||||||
|
height := toCoord.Y - fromCoord.Y + 1
|
||||||
|
size := uint32(width) * uint32(height)
|
||||||
|
|
||||||
|
if size <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]CHAR_INFO, size)
|
||||||
|
|
||||||
|
char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes}
|
||||||
|
for i := 0; i < int(size); i++ {
|
||||||
|
buffer[i] = char
|
||||||
|
}
|
||||||
|
|
||||||
|
err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
// effectiveSr gets the current effective scroll region in buffer coordinates
|
||||||
|
func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion {
|
||||||
|
top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom)
|
||||||
|
bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom)
|
||||||
|
if top >= bottom {
|
||||||
|
top = window.Top
|
||||||
|
bottom = window.Bottom
|
||||||
|
}
|
||||||
|
return scrollRegion{top: top, bottom: bottom}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) scrollUp(param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sr := h.effectiveSr(info.Window)
|
||||||
|
return h.scroll(param, sr, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) scrollDown(param int) error {
|
||||||
|
return h.scrollUp(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) deleteLines(param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
start := info.CursorPosition.Y
|
||||||
|
sr := h.effectiveSr(info.Window)
|
||||||
|
// Lines cannot be inserted or deleted outside the scrolling region.
|
||||||
|
if start >= sr.top && start <= sr.bottom {
|
||||||
|
sr.top = start
|
||||||
|
return h.scroll(param, sr, info)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) insertLines(param int) error {
|
||||||
|
return h.deleteLines(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates.
|
||||||
|
func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error {
|
||||||
|
h.logf("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom)
|
||||||
|
h.logf("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom)
|
||||||
|
|
||||||
|
// Copy from and clip to the scroll region (full buffer width)
|
||||||
|
scrollRect := SMALL_RECT{
|
||||||
|
Top: sr.top,
|
||||||
|
Bottom: sr.bottom,
|
||||||
|
Left: 0,
|
||||||
|
Right: info.Size.X - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin to which area should be copied
|
||||||
|
destOrigin := COORD{
|
||||||
|
X: 0,
|
||||||
|
Y: sr.top - int16(param),
|
||||||
|
}
|
||||||
|
|
||||||
|
char := CHAR_INFO{
|
||||||
|
UnicodeChar: ' ',
|
||||||
|
Attributes: h.attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) deleteCharacters(param int) error {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.scrollLine(param, info.CursorPosition, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) insertCharacters(param int) error {
|
||||||
|
return h.deleteCharacters(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrollLine scrolls a line horizontally starting at the provided position by a number of columns.
|
||||||
|
func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error {
|
||||||
|
// Copy from and clip to the scroll region (full buffer width)
|
||||||
|
scrollRect := SMALL_RECT{
|
||||||
|
Top: position.Y,
|
||||||
|
Bottom: position.Y,
|
||||||
|
Left: position.X,
|
||||||
|
Right: info.Size.X - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin to which area should be copied
|
||||||
|
destOrigin := COORD{
|
||||||
|
X: position.X - int16(columns),
|
||||||
|
Y: position.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
char := CHAR_INFO{
|
||||||
|
UnicodeChar: ' ',
|
||||||
|
Attributes: h.attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
// AddInRange increments a value by the passed quantity while ensuring the values
|
||||||
|
// always remain within the supplied min / max range.
|
||||||
|
func addInRange(n int16, increment int16, min int16, max int16) int16 {
|
||||||
|
return ensureInRange(n+increment, min, max)
|
||||||
|
}
|
@ -0,0 +1,743 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Azure/go-ansiterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type windowsAnsiEventHandler struct {
|
||||||
|
fd uintptr
|
||||||
|
file *os.File
|
||||||
|
infoReset *CONSOLE_SCREEN_BUFFER_INFO
|
||||||
|
sr scrollRegion
|
||||||
|
buffer bytes.Buffer
|
||||||
|
attributes uint16
|
||||||
|
inverted bool
|
||||||
|
wrapNext bool
|
||||||
|
drewMarginByte bool
|
||||||
|
originMode bool
|
||||||
|
marginByte byte
|
||||||
|
curInfo *CONSOLE_SCREEN_BUFFER_INFO
|
||||||
|
curPos COORD
|
||||||
|
logf func(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*windowsAnsiEventHandler)
|
||||||
|
|
||||||
|
func WithLogf(f func(string, ...interface{})) Option {
|
||||||
|
return func(w *windowsAnsiEventHandler) {
|
||||||
|
w.logf = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWinEventHandler(fd uintptr, file *os.File, opts ...Option) ansiterm.AnsiEventHandler {
|
||||||
|
infoReset, err := GetConsoleScreenBufferInfo(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &windowsAnsiEventHandler{
|
||||||
|
fd: fd,
|
||||||
|
file: file,
|
||||||
|
infoReset: infoReset,
|
||||||
|
attributes: infoReset.Attributes,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" {
|
||||||
|
logFile, _ := os.Create("winEventHandler.log")
|
||||||
|
logger := log.New(logFile, "", log.LstdFlags)
|
||||||
|
if h.logf != nil {
|
||||||
|
l := h.logf
|
||||||
|
h.logf = func(s string, v ...interface{}) {
|
||||||
|
l(s, v...)
|
||||||
|
logger.Printf(s, v...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.logf = logger.Printf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.logf == nil {
|
||||||
|
h.logf = func(string, ...interface{}) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type scrollRegion struct {
|
||||||
|
top int16
|
||||||
|
bottom int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the
|
||||||
|
// current cursor position and scroll region settings, in which case it returns
|
||||||
|
// true. If no special handling is necessary, then it does nothing and returns
|
||||||
|
// false.
|
||||||
|
//
|
||||||
|
// In the false case, the caller should ensure that a carriage return
|
||||||
|
// and line feed are inserted or that the text is otherwise wrapped.
|
||||||
|
func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) {
|
||||||
|
if h.wrapNext {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
h.clearWrap()
|
||||||
|
}
|
||||||
|
pos, info, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
sr := h.effectiveSr(info.Window)
|
||||||
|
if pos.Y == sr.bottom {
|
||||||
|
// Scrolling is necessary. Let Windows automatically scroll if the scrolling region
|
||||||
|
// is the full window.
|
||||||
|
if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom {
|
||||||
|
if includeCR {
|
||||||
|
pos.X = 0
|
||||||
|
h.updatePos(pos)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A custom scroll region is active. Scroll the window manually to simulate
|
||||||
|
// the LF.
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
h.logf("Simulating LF inside scroll region")
|
||||||
|
if err := h.scrollUp(1); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if includeCR {
|
||||||
|
pos.X = 0
|
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
} else if pos.Y < info.Window.Bottom {
|
||||||
|
// Let Windows handle the LF.
|
||||||
|
pos.Y++
|
||||||
|
if includeCR {
|
||||||
|
pos.X = 0
|
||||||
|
}
|
||||||
|
h.updatePos(pos)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
// The cursor is at the bottom of the screen but outside the scroll
|
||||||
|
// region. Skip the LF.
|
||||||
|
h.logf("Simulating LF outside scroll region")
|
||||||
|
if includeCR {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
pos.X = 0
|
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeLF executes a LF without a CR.
|
||||||
|
func (h *windowsAnsiEventHandler) executeLF() error {
|
||||||
|
handled, err := h.simulateLF(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !handled {
|
||||||
|
// Windows LF will reset the cursor column position. Write the LF
|
||||||
|
// and restore the cursor position.
|
||||||
|
pos, _, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
|
||||||
|
if pos.X != 0 {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("Resetting cursor position for LF without CR")
|
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Print(b byte) error {
|
||||||
|
if h.wrapNext {
|
||||||
|
h.buffer.WriteByte(h.marginByte)
|
||||||
|
h.clearWrap()
|
||||||
|
if _, err := h.simulateLF(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos, info, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pos.X == info.Size.X-1 {
|
||||||
|
h.wrapNext = true
|
||||||
|
h.marginByte = b
|
||||||
|
} else {
|
||||||
|
pos.X++
|
||||||
|
h.updatePos(pos)
|
||||||
|
h.buffer.WriteByte(b)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Execute(b byte) error {
|
||||||
|
switch b {
|
||||||
|
case ansiterm.ANSI_TAB:
|
||||||
|
h.logf("Execute(TAB)")
|
||||||
|
// Move to the next tab stop, but preserve auto-wrap if already set.
|
||||||
|
if !h.wrapNext {
|
||||||
|
pos, info, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pos.X = (pos.X + 8) - pos.X%8
|
||||||
|
if pos.X >= info.Size.X {
|
||||||
|
pos.X = info.Size.X - 1
|
||||||
|
}
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case ansiterm.ANSI_BEL:
|
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_BEL)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case ansiterm.ANSI_BACKSPACE:
|
||||||
|
if h.wrapNext {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.clearWrap()
|
||||||
|
}
|
||||||
|
pos, _, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pos.X > 0 {
|
||||||
|
pos.X--
|
||||||
|
h.updatePos(pos)
|
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED:
|
||||||
|
// Treat as true LF.
|
||||||
|
return h.executeLF()
|
||||||
|
|
||||||
|
case ansiterm.ANSI_LINE_FEED:
|
||||||
|
// Simulate a CR and LF for now since there is no way in go-ansiterm
|
||||||
|
// to tell if the LF should include CR (and more things break when it's
|
||||||
|
// missing than when it's incorrectly added).
|
||||||
|
handled, err := h.simulateLF(true)
|
||||||
|
if handled || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
|
||||||
|
|
||||||
|
case ansiterm.ANSI_CARRIAGE_RETURN:
|
||||||
|
if h.wrapNext {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.clearWrap()
|
||||||
|
}
|
||||||
|
pos, _, err := h.getCurrentInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pos.X != 0 {
|
||||||
|
pos.X = 0
|
||||||
|
h.updatePos(pos)
|
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUU(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CUU: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorVertical(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUD(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CUD: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorVertical(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUF(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CUF: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorHorizontal(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUB(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CUB: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorHorizontal(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CNL(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CNL: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorLine(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CPL(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CPL: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorLine(-param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CHA(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CHA: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.moveCursorColumn(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) VPA(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("VPA: [[%d]]", param)
|
||||||
|
h.clearWrap()
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
window := h.getCursorWindow(info)
|
||||||
|
position := info.CursorPosition
|
||||||
|
position.Y = window.Top + int16(param) - 1
|
||||||
|
return h.setCursorPosition(position, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUP(row int, col int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("CUP: [[%d %d]]", row, col)
|
||||||
|
h.clearWrap()
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
window := h.getCursorWindow(info)
|
||||||
|
position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1}
|
||||||
|
return h.setCursorPosition(position, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) HVP(row int, col int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("HVP: [[%d %d]]", row, col)
|
||||||
|
h.clearWrap()
|
||||||
|
return h.CUP(row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DECTCEM: [%v]", []string{strconv.FormatBool(visible)})
|
||||||
|
h.clearWrap()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECOM(enable bool) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DECOM: [%v]", []string{strconv.FormatBool(enable)})
|
||||||
|
h.clearWrap()
|
||||||
|
h.originMode = enable
|
||||||
|
return h.CUP(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DECCOLM: [%v]", []string{strconv.FormatBool(use132)})
|
||||||
|
h.clearWrap()
|
||||||
|
if err := h.ED(2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetWidth := int16(80)
|
||||||
|
if use132 {
|
||||||
|
targetWidth = 132
|
||||||
|
}
|
||||||
|
if info.Size.X < targetWidth {
|
||||||
|
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||||
|
h.logf("set buffer failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window := info.Window
|
||||||
|
window.Left = 0
|
||||||
|
window.Right = targetWidth - 1
|
||||||
|
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
|
||||||
|
h.logf("set window failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Size.X > targetWidth {
|
||||||
|
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
|
||||||
|
h.logf("set buffer failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SetConsoleCursorPosition(h.fd, COORD{0, 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) ED(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("ED: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
|
||||||
|
// [J -- Erases from the cursor to the end of the screen, including the cursor position.
|
||||||
|
// [1J -- Erases from the beginning of the screen to the cursor, including the cursor position.
|
||||||
|
// [2J -- Erases the complete display. The cursor does not move.
|
||||||
|
// Notes:
|
||||||
|
// -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var start COORD
|
||||||
|
var end COORD
|
||||||
|
|
||||||
|
switch param {
|
||||||
|
case 0:
|
||||||
|
start = info.CursorPosition
|
||||||
|
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
start = COORD{0, 0}
|
||||||
|
end = info.CursorPosition
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
start = COORD{0, 0}
|
||||||
|
end = COORD{info.Size.X - 1, info.Size.Y - 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.clearRange(h.attributes, start, end)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the whole buffer was cleared, move the window to the top while preserving
|
||||||
|
// the window-relative cursor position.
|
||||||
|
if param == 2 {
|
||||||
|
pos := info.CursorPosition
|
||||||
|
window := info.Window
|
||||||
|
pos.Y -= window.Top
|
||||||
|
window.Bottom -= window.Top
|
||||||
|
window.Top = 0
|
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) EL(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("EL: [%v]", strconv.Itoa(param))
|
||||||
|
h.clearWrap()
|
||||||
|
|
||||||
|
// [K -- Erases from the cursor to the end of the line, including the cursor position.
|
||||||
|
// [1K -- Erases from the beginning of the line to the cursor, including the cursor position.
|
||||||
|
// [2K -- Erases the complete line.
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var start COORD
|
||||||
|
var end COORD
|
||||||
|
|
||||||
|
switch param {
|
||||||
|
case 0:
|
||||||
|
start = info.CursorPosition
|
||||||
|
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
start = COORD{0, info.CursorPosition.Y}
|
||||||
|
end = info.CursorPosition
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
start = COORD{0, info.CursorPosition.Y}
|
||||||
|
end = COORD{info.Size.X, info.CursorPosition.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.clearRange(h.attributes, start, end)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) IL(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("IL: [%v]", strconv.Itoa(param))
|
||||||
|
h.clearWrap()
|
||||||
|
return h.insertLines(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DL(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DL: [%v]", strconv.Itoa(param))
|
||||||
|
h.clearWrap()
|
||||||
|
return h.deleteLines(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) ICH(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("ICH: [%v]", strconv.Itoa(param))
|
||||||
|
h.clearWrap()
|
||||||
|
return h.insertCharacters(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DCH(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DCH: [%v]", strconv.Itoa(param))
|
||||||
|
h.clearWrap()
|
||||||
|
return h.deleteCharacters(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SGR(params []int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
strings := []string{}
|
||||||
|
for _, v := range params {
|
||||||
|
strings = append(strings, strconv.Itoa(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logf("SGR: [%v]", strings)
|
||||||
|
|
||||||
|
if len(params) <= 0 {
|
||||||
|
h.attributes = h.infoReset.Attributes
|
||||||
|
h.inverted = false
|
||||||
|
} else {
|
||||||
|
for _, attr := range params {
|
||||||
|
|
||||||
|
if attr == ansiterm.ANSI_SGR_RESET {
|
||||||
|
h.attributes = h.infoReset.Attributes
|
||||||
|
h.inverted = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := h.attributes
|
||||||
|
if h.inverted {
|
||||||
|
attributes = invertAttributes(attributes)
|
||||||
|
}
|
||||||
|
err := SetConsoleTextAttribute(h.fd, attributes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SU(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("SU: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.scrollUp(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SD(param int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("SD: [%v]", []string{strconv.Itoa(param)})
|
||||||
|
h.clearWrap()
|
||||||
|
return h.scrollDown(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DA(params []string) error {
|
||||||
|
h.logf("DA: [%v]", params)
|
||||||
|
// DA cannot be implemented because it must send data on the VT100 input stream,
|
||||||
|
// which is not available to go-ansiterm.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("DECSTBM: [%d, %d]", top, bottom)
|
||||||
|
|
||||||
|
// Windows is 0 indexed, Linux is 1 indexed
|
||||||
|
h.sr.top = int16(top - 1)
|
||||||
|
h.sr.bottom = int16(bottom - 1)
|
||||||
|
|
||||||
|
// This command also moves the cursor to the origin.
|
||||||
|
h.clearWrap()
|
||||||
|
return h.CUP(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) RI() error {
|
||||||
|
if err := h.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.logf("RI: []")
|
||||||
|
h.clearWrap()
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sr := h.effectiveSr(info.Window)
|
||||||
|
if info.CursorPosition.Y == sr.top {
|
||||||
|
return h.scrollDown(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.moveCursorVertical(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) IND() error {
|
||||||
|
h.logf("IND: []")
|
||||||
|
return h.executeLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Flush() error {
|
||||||
|
h.curInfo = nil
|
||||||
|
if h.buffer.Len() > 0 {
|
||||||
|
h.logf("Flush: [%s]", h.buffer.Bytes())
|
||||||
|
if _, err := h.buffer.WriteTo(h.file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.wrapNext && !h.drewMarginByte {
|
||||||
|
h.logf("Flush: drawing margin byte '%c'", h.marginByte)
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}}
|
||||||
|
size := COORD{1, 1}
|
||||||
|
position := COORD{0, 0}
|
||||||
|
region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y}
|
||||||
|
if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.drewMarginByte = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheConsoleInfo ensures that the current console screen information has been queried
|
||||||
|
// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos.
|
||||||
|
func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||||
|
if h.curInfo == nil {
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd)
|
||||||
|
if err != nil {
|
||||||
|
return COORD{}, nil, err
|
||||||
|
}
|
||||||
|
h.curInfo = info
|
||||||
|
h.curPos = info.CursorPosition
|
||||||
|
}
|
||||||
|
return h.curPos, h.curInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) updatePos(pos COORD) {
|
||||||
|
if h.curInfo == nil {
|
||||||
|
panic("failed to call getCurrentInfo before calling updatePos")
|
||||||
|
}
|
||||||
|
h.curPos = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearWrap clears the state where the cursor is in the margin
|
||||||
|
// waiting for the next character before wrapping the line. This must
|
||||||
|
// be done before most operations that act on the cursor.
|
||||||
|
func (h *windowsAnsiEventHandler) clearWrap() {
|
||||||
|
h.wrapNext = false
|
||||||
|
h.drewMarginByte = false
|
||||||
|
}
|
0
vendor/github.com/elisescu/pty/.gitignore → vendor/github.com/creack/pty/.gitignore
generated
vendored
0
vendor/github.com/elisescu/pty/.gitignore → vendor/github.com/creack/pty/.gitignore
generated
vendored
@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:1.13
|
||||||
|
|
||||||
|
# Clone and complie a riscv compatible version of the go compiler.
|
||||||
|
RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go
|
||||||
|
# riscvdev branch HEAD as of 2019-06-29.
|
||||||
|
RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf
|
||||||
|
ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH
|
||||||
|
RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash
|
||||||
|
ENV GOROOT=/riscv-go
|
||||||
|
|
||||||
|
# Make sure we compile.
|
||||||
|
WORKDIR pty
|
||||||
|
ADD . .
|
||||||
|
RUN GOOS=linux GOARCH=riscv go build
|
@ -0,0 +1,100 @@
|
|||||||
|
# pty
|
||||||
|
|
||||||
|
Pty is a Go package for using unix pseudo-terminals.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
go get github.com/creack/pty
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := exec.Command("grep", "--color=auto", "bar")
|
||||||
|
f, err := pty.Start(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
f.Write([]byte("foo\n"))
|
||||||
|
f.Write([]byte("bar\n"))
|
||||||
|
f.Write([]byte("baz\n"))
|
||||||
|
f.Write([]byte{4}) // EOT
|
||||||
|
}()
|
||||||
|
io.Copy(os.Stdout, f)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func test() error {
|
||||||
|
// Create arbitrary command.
|
||||||
|
c := exec.Command("bash")
|
||||||
|
|
||||||
|
// Start the command with a pty.
|
||||||
|
ptmx, err := pty.Start(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Make sure to close the pty at the end.
|
||||||
|
defer func() { _ = ptmx.Close() }() // Best effort.
|
||||||
|
|
||||||
|
// Handle pty size.
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGWINCH)
|
||||||
|
go func() {
|
||||||
|
for range ch {
|
||||||
|
if err := pty.InheritSize(os.Stdin, ptmx); err != nil {
|
||||||
|
log.Printf("error resizing pty: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ch <- syscall.SIGWINCH // Initial resize.
|
||||||
|
|
||||||
|
// Set stdin in raw mode.
|
||||||
|
oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
|
||||||
|
|
||||||
|
// Copy stdin to the pty and the pty to stdout.
|
||||||
|
go func() { _, _ = io.Copy(ptmx, os.Stdin) }()
|
||||||
|
_, _ = io.Copy(os.Stdout, ptmx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := test(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
2
vendor/github.com/elisescu/pty/ioctl.go → vendor/github.com/creack/pty/ioctl.go
generated
vendored
2
vendor/github.com/elisescu/pty/ioctl.go → vendor/github.com/creack/pty/ioctl.go
generated
vendored
@ -1,4 +1,4 @@
|
|||||||
// +build !windows
|
// +build !windows,!solaris
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
@ -0,0 +1,30 @@
|
|||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// see /usr/include/sys/stropts.h
|
||||||
|
I_PUSH = uintptr((int32('S')<<8 | 002))
|
||||||
|
I_STR = uintptr((int32('S')<<8 | 010))
|
||||||
|
I_FIND = uintptr((int32('S')<<8 | 013))
|
||||||
|
// see /usr/include/sys/ptms.h
|
||||||
|
ISPTM = (int32('P') << 8) | 1
|
||||||
|
UNLKPT = (int32('P') << 8) | 2
|
||||||
|
PTSSTTY = (int32('P') << 8) | 3
|
||||||
|
ZONEPT = (int32('P') << 8) | 4
|
||||||
|
OWNERPT = (int32('P') << 8) | 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type strioctl struct {
|
||||||
|
ic_cmd int32
|
||||||
|
ic_timout int32
|
||||||
|
ic_len int32
|
||||||
|
ic_dp unsafe.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctl(fd, cmd, ptr uintptr) error {
|
||||||
|
return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr))
|
||||||
|
}
|
11
vendor/github.com/elisescu/pty/pty_linux.go → vendor/github.com/creack/pty/pty_linux.go
generated
vendored
11
vendor/github.com/elisescu/pty/pty_linux.go → vendor/github.com/creack/pty/pty_linux.go
generated
vendored
@ -0,0 +1,33 @@
|
|||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func open() (pty, tty *os.File, err error) {
|
||||||
|
/*
|
||||||
|
* from ptm(4):
|
||||||
|
* The PTMGET command allocates a free pseudo terminal, changes its
|
||||||
|
* ownership to the caller, revokes the access privileges for all previous
|
||||||
|
* users, opens the file descriptors for the pty and tty devices and
|
||||||
|
* returns them to the caller in struct ptmget.
|
||||||
|
*/
|
||||||
|
|
||||||
|
p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
var ptm ptmget
|
||||||
|
if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm")
|
||||||
|
tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm")
|
||||||
|
|
||||||
|
return pty, tty, nil
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package pty
|
||||||
|
|
||||||
|
/* based on:
|
||||||
|
http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/pt.c
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NODEV = ^uint64(0)
|
||||||
|
|
||||||
|
func open() (pty, tty *os.File, err error) {
|
||||||
|
masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|unix.O_NOCTTY, 0)
|
||||||
|
//masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC|unix.O_NOCTTY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
p := os.NewFile(uintptr(masterfd), "/dev/ptmx")
|
||||||
|
|
||||||
|
sname, err := ptsname(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = grantpt(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unlockpt(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
slavefd, err := syscall.Open(sname, os.O_RDWR|unix.O_NOCTTY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
t := os.NewFile(uintptr(slavefd), sname)
|
||||||
|
|
||||||
|
// pushing terminal driver STREAMS modules as per pts(7)
|
||||||
|
for _, mod := range([]string{"ptem", "ldterm", "ttcompat"}) {
|
||||||
|
err = streams_push(t, mod)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func minor(x uint64) uint64 {
|
||||||
|
return x & 0377
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptsdev(fd uintptr) uint64 {
|
||||||
|
istr := strioctl{ISPTM, 0, 0, nil}
|
||||||
|
err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr)))
|
||||||
|
if err != nil {
|
||||||
|
return NODEV
|
||||||
|
}
|
||||||
|
var status unix.Stat_t
|
||||||
|
err = unix.Fstat(int(fd), &status)
|
||||||
|
if err != nil {
|
||||||
|
return NODEV
|
||||||
|
}
|
||||||
|
return uint64(minor(status.Rdev))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptsname(f *os.File) (string, error) {
|
||||||
|
dev := ptsdev(f.Fd())
|
||||||
|
if dev == NODEV {
|
||||||
|
return "", errors.New("not a master pty")
|
||||||
|
}
|
||||||
|
fn := "/dev/pts/" + strconv.FormatInt(int64(dev), 10)
|
||||||
|
// access(2) creates the slave device (if the pty exists)
|
||||||
|
// F_OK == 0 (unistd.h)
|
||||||
|
err := unix.Access(fn, 0)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pt_own struct {
|
||||||
|
pto_ruid int32
|
||||||
|
pto_rgid int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func grantpt(f *os.File) error {
|
||||||
|
if ptsdev(f.Fd()) == NODEV {
|
||||||
|
return errors.New("not a master pty")
|
||||||
|
}
|
||||||
|
var pto pt_own
|
||||||
|
pto.pto_ruid = int32(os.Getuid())
|
||||||
|
// XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty"
|
||||||
|
pto.pto_rgid = int32(os.Getgid())
|
||||||
|
var istr strioctl
|
||||||
|
istr.ic_cmd = OWNERPT
|
||||||
|
istr.ic_timout = 0
|
||||||
|
istr.ic_len = int32(unsafe.Sizeof(istr))
|
||||||
|
istr.ic_dp = unsafe.Pointer(&pto)
|
||||||
|
err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("access denied")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlockpt(f *os.File) error {
|
||||||
|
istr := strioctl{UNLKPT, 0, 0, nil}
|
||||||
|
return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// push STREAMS modules if not already done so
|
||||||
|
func streams_push(f *os.File, mod string) error {
|
||||||
|
var err error
|
||||||
|
buf := []byte(mod)
|
||||||
|
// XXX I_FIND is not returning an error when the module
|
||||||
|
// is already pushed even though truss reports a return
|
||||||
|
// value of 1. A bug in the Go Solaris syscall interface?
|
||||||
|
// XXX without this we are at risk of the issue
|
||||||
|
// https://www.illumos.org/issues/9042
|
||||||
|
// but since we are not using libc or XPG4.2, we should not be
|
||||||
|
// double-pushing modules
|
||||||
|
|
||||||
|
err = ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0])))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0])))
|
||||||
|
return err
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// +build !linux,!darwin,!freebsd,!dragonfly
|
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
@ -0,0 +1,74 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
||||||
|
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
||||||
|
// corresponding pty.
|
||||||
|
//
|
||||||
|
// Starts the process in a new session and sets the controlling terminal.
|
||||||
|
func Start(c *exec.Cmd) (pty *os.File, err error) {
|
||||||
|
return StartWithSize(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
||||||
|
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
||||||
|
// corresponding pty.
|
||||||
|
//
|
||||||
|
// This will resize the pty to the specified size before starting the command.
|
||||||
|
// Starts the process in a new session and sets the controlling terminal.
|
||||||
|
func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) {
|
||||||
|
if c.SysProcAttr == nil {
|
||||||
|
c.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
}
|
||||||
|
c.SysProcAttr.Setsid = true
|
||||||
|
c.SysProcAttr.Setctty = true
|
||||||
|
return StartWithAttrs(c, sz, c.SysProcAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWithAttrs assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
|
||||||
|
// and c.Stderr, calls c.Start, and returns the File of the tty's
|
||||||
|
// corresponding pty.
|
||||||
|
//
|
||||||
|
// This will resize the pty to the specified size before starting the command if a size is provided.
|
||||||
|
// The `attrs` parameter overrides the one set in c.SysProcAttr.
|
||||||
|
//
|
||||||
|
// This should generally not be needed. Used in some edge cases where it is needed to create a pty
|
||||||
|
// without a controlling terminal.
|
||||||
|
func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (pty *os.File, err error) {
|
||||||
|
pty, tty, err := Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tty.Close()
|
||||||
|
|
||||||
|
if sz != nil {
|
||||||
|
if err := Setsize(pty, sz); err != nil {
|
||||||
|
pty.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Stdout == nil {
|
||||||
|
c.Stdout = tty
|
||||||
|
}
|
||||||
|
if c.Stderr == nil {
|
||||||
|
c.Stderr = tty
|
||||||
|
}
|
||||||
|
if c.Stdin == nil {
|
||||||
|
c.Stdin = tty
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SysProcAttr = attrs
|
||||||
|
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
_ = pty.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pty, err
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Test script checking that all expected os/arch compile properly.
|
||||||
|
# Does not actually test the logic, just the compilation so we make sure we don't break code depending on the lib.
|
||||||
|
|
||||||
|
echo2() {
|
||||||
|
echo $@ >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
trap end 0
|
||||||
|
end() {
|
||||||
|
[ "$?" = 0 ] && echo2 "Pass." || (echo2 "Fail."; exit 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cross() {
|
||||||
|
os=$1
|
||||||
|
shift
|
||||||
|
echo2 "Build for $os."
|
||||||
|
for arch in $@; do
|
||||||
|
echo2 " - $os/$arch"
|
||||||
|
GOOS=$os GOARCH=$arch go build
|
||||||
|
done
|
||||||
|
echo2
|
||||||
|
}
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cross linux amd64 386 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le
|
||||||
|
cross darwin amd64 386 arm arm64
|
||||||
|
cross freebsd amd64 386 arm
|
||||||
|
cross netbsd amd64 386 arm
|
||||||
|
cross openbsd amd64 386 arm arm64
|
||||||
|
cross dragonfly amd64
|
||||||
|
cross solaris amd64
|
||||||
|
|
||||||
|
# Not expected to work but should still compile.
|
||||||
|
cross windows amd64 386 arm
|
||||||
|
|
||||||
|
# TODO: Fix compilation error on openbsd/arm.
|
||||||
|
# TODO: Merge the solaris PR.
|
||||||
|
|
||||||
|
# Some os/arch require a different compiler. Run in docker.
|
||||||
|
if ! hash docker; then
|
||||||
|
# If docker is not present, stop here.
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo2 "Build for linux."
|
||||||
|
echo2 " - linux/riscv"
|
||||||
|
docker build -t test -f Dockerfile.riscv .
|
@ -0,0 +1,64 @@
|
|||||||
|
// +build !windows,!solaris
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InheritSize applies the terminal size of pty to tty. This should be run
|
||||||
|
// in a signal handler for syscall.SIGWINCH to automatically resize the tty when
|
||||||
|
// the pty receives a window size change notification.
|
||||||
|
func InheritSize(pty, tty *os.File) error {
|
||||||
|
size, err := GetsizeFull(pty)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = Setsize(tty, size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setsize resizes t to s.
|
||||||
|
func Setsize(t *os.File, ws *Winsize) error {
|
||||||
|
return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetsizeFull returns the full terminal size description.
|
||||||
|
func GetsizeFull(t *os.File) (size *Winsize, err error) {
|
||||||
|
var ws Winsize
|
||||||
|
err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ)
|
||||||
|
return &ws, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getsize returns the number of rows (lines) and cols (positions
|
||||||
|
// in each line) in terminal t.
|
||||||
|
func Getsize(t *os.File) (rows, cols int, err error) {
|
||||||
|
ws, err := GetsizeFull(t)
|
||||||
|
return int(ws.Rows), int(ws.Cols), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Winsize describes the terminal size.
|
||||||
|
type Winsize struct {
|
||||||
|
Rows uint16 // ws_row: Number of rows (in cells)
|
||||||
|
Cols uint16 // ws_col: Number of columns (in cells)
|
||||||
|
X uint16 // ws_xpixel: Width in pixels
|
||||||
|
Y uint16 // ws_ypixel: Height in pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowRectCall(ws *Winsize, fd, a2 uintptr) error {
|
||||||
|
_, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
fd,
|
||||||
|
a2,
|
||||||
|
uintptr(unsafe.Pointer(ws)),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return syscall.Errno(errno)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TIOCGWINSZ = 21608 // 'T' << 8 | 104
|
||||||
|
TIOCSWINSZ = 21607 // 'T' << 8 | 103
|
||||||
|
)
|
||||||
|
|
||||||
|
// Winsize describes the terminal size.
|
||||||
|
type Winsize struct {
|
||||||
|
Rows uint16 // ws_row: Number of rows (in cells)
|
||||||
|
Cols uint16 // ws_col: Number of columns (in cells)
|
||||||
|
X uint16 // ws_xpixel: Width in pixels
|
||||||
|
Y uint16 // ws_ypixel: Height in pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetsizeFull returns the full terminal size description.
|
||||||
|
func GetsizeFull(t *os.File) (size *Winsize, err error) {
|
||||||
|
var wsz *unix.Winsize
|
||||||
|
wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &Winsize{wsz.Row, wsz.Col, wsz.Xpixel, wsz.Ypixel}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Windows Size
|
||||||
|
func Getsize(t *os.File) (rows, cols int, err error) {
|
||||||
|
var wsz *unix.Winsize
|
||||||
|
wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 80, 25, err
|
||||||
|
} else {
|
||||||
|
return int(wsz.Row), int(wsz.Col), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setsize resizes t to s.
|
||||||
|
func Setsize(t *os.File, ws *Winsize) error {
|
||||||
|
wsz := unix.Winsize{ws.Rows, ws.Cols, ws.X, ws.Y}
|
||||||
|
return unix.IoctlSetWinsize(int(t.Fd()), TIOCSWINSZ, &wsz)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
|
||||||
|
// cgo -godefs types_freebsd.go
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
const (
|
||||||
|
_C_SPECNAMELEN = 0xff
|
||||||
|
)
|
||||||
|
|
||||||
|
type fiodgnameArg struct {
|
||||||
|
Len int32
|
||||||
|
Buf *byte
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
// +build openbsd
|
||||||
|
// +build 386 amd64 arm arm64
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
type ptmget struct {
|
||||||
|
Cfd int32
|
||||||
|
Sfd int32
|
||||||
|
Cn [16]int8
|
||||||
|
Sn [16]int8
|
||||||
|
}
|
||||||
|
|
||||||
|
var ioctl_PTMGET = 0x40287401
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue