Compare commits

..

No commits in common. 'master' and 'v0.6.0' have entirely different histories.

@ -1,2 +0,0 @@
**/.git
**/node_modules

@ -1,61 +0,0 @@
---
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_*

@ -1,65 +0,0 @@
---
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"

5
.gitignore vendored

@ -1,9 +1,6 @@
server/frontend/node_modules
server/frontend/public
out
.DS_Store
playground/
.vscode/
tty-share
tmp-*

@ -1,9 +1,9 @@
language: go
go:
- 1.14.x
- 1.12.x
before_install:
- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
script: go build -mod=vendor -ldflags "-X main.version=0.0.1" && test $(./tty-share --version) = 0.0.1
script: dep ensure && go build

@ -1,18 +0,0 @@
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"]

56
Gopkg.lock generated

@ -0,0 +1,56 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:255fa3951022f430c29617c640c2bb3bd8b2957d669984e3cac107071c363f26"
name = "github.com/elisescu/pty"
packages = ["."]
pruneopts = "UT"
revision = "b36ef7cd2bd60d67596d5086f9560f76642c1a59"
version = "v1.0.2"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]]
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
version = "v1.4.2"
[[projects]]
branch = "master"
digest = "1:bbe51412d9915d64ffaa96b51d409e070665efc5194fcf145c4a27d4133107a4"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
pruneopts = "UT"
revision = "53104e6ec876ad4e22ad27cce588b01392043c1b"
[[projects]]
branch = "master"
digest = "1:0cbf59a8e2cbbe6601b6f6c940d29aab7020f9f42c6b697fee4335a973bcce09"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "a1369afcdac740082c63165b07ec83b531884be2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/elisescu/pty",
"github.com/sirupsen/logrus",
"golang.org/x/crypto/ssh/terminal",
]
solver-name = "gps-cdcl"
solver-version = 1

@ -0,0 +1,15 @@
[[constraint]]
name = "github.com/elisescu/pty"
version = "1.0.2"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.4.2"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[prune]
go-tests = true
unused-packages = true

@ -1,84 +1,61 @@
# tty-share
[tty-share](https://tty-share.com) is a very simple tool used to share your Linux/OSX terminal over the Internet. It is written in GO, results in a static cross-platform binary with no dependencies, and therefore will also work on your Raspberry Pi.
The remote participant needs **not setup**, and they can join the session from the browser or from the terminal (`tty-share <secret URL>`). The session can be shared either over the Internet, or only in the local network. When sharing it over the Internet (outside your NAT), `tty-share` will connect to [proxy server](https://github.com/elisescu/tty-proxy) that will mediate the communication between the participants. An instance of this server runs at [tty-share.com](https://tty-share.com), but you can run your own.
[![Build Status](https://travis-ci.com/elisescu/tty-share.svg?branch=master)](https://travis-ci.com/elisescu/tty-share)
## Demos
# tty-share
*Local network session*
![demo](doc/local.gif)
It is a very simple command line tool that gives remote access to a UNIX terminal session. It's using the [PTY](https://en.wikipedia.org/wiki/Pseudoterminal) system, so it should work on any *UNIX* system (Linux, OSX). Because it's written in GO, the tool will be a single binary, with no dependencies, which will also work on your ARM Raspberry Pi.
*Public session*
![demo](doc/public.gif)
The most important part about it is that it requires **no setup** on the remote end. All I need to give remote access to the terminal (a bash/shell session) is the binary tool, and the remote person only needs to open a secret URL in their browser.
*Join a session from another terminal*
![demo](doc/terminal.gif)
The project consists of two command line utilities: `tty-share` and `tty-server`. The server side has been moved to [github.com/elisescu/tty-server](https://github.com/elisescu/tty-server) repo, so it's easier to build the `tty-share` tool separately.
## Installing and running
The `tty-share` is used on the machine that wants to share the terminal, and it connects to the server to generate a secret URL, over which the terminal can be viewed in the browser.
**Docker**
An instance of the server runs at [tty-share.com](https://tty-share.com), so you only need the `tty-server` binary if you want to host it yourself.
If you only want to try it out, there's a Docker image you can use:
```bash
docker run -it elisescu/tty-share --public
```
![demo](doc/demo.gif)
**Brew**
## More documentation
If you are on OSX and use [brew](https://brew.sh/), then you can simply do a `brew install tty-share`.
The documentation is very poor now. More will follow. [This](doc/architecture.md) describes briefly some thoughts behind the architecture of the project.
**Binary releases**
## Running
Otherwise, download the latest `tty-share` binary [release](https://github.com/elisescu/tty-share/releases).
Download the latest `tty-share` binary [release](https://github.com/elisescu/tty-share/releases), and run it:
See package name for your system package manager at [Repology](https://repology.org/project/tty-share/information).
**Running it**
```
~ $ tty-share --public
public session: https://on.tty-share.com/s/L8d2ECvHLhU8CXEBaEF5WKV8O3jsZkS5sXwG1__--2_jnFSlGonzXBe0qxd7tZeRvQM/
local session: http://localhost:8000/s/local/
Press Enter to continue!
bash$ tty-share
Web terminal: https://go.tty-share.com/s/J5U6FAwChWNP0I9VQ9XyPqVD6m6IpI8-sBLRiz98XMA=
~ $
bash$
```
Sessions can be created as read only, with the `--readonly` flag. See `--help` for more.
**Join a session**
You can join a session by opening the session URLs in the browser, or with another `tty-share` command:
```bash
~ $ tty-share https://on.tty-share.com/s/L8d2ECvHLhU8CXEBaEF5WKV8O3jsZkS5sXwG1__--2_jnFSlGonzXBe0qxd7tZeRvQM/
```
## Building `tty-share` locally
**Join a session with TCP port forwarding**
If you want to just build the tool that shares your terminal, and not the server, then simply do a
You can use the `-L` option to create a TCP tunnel, similarly to how you would do it with `ssh`:
```
tty-share -L 1234:example.com:4567 https://on.tty-share.com/s/L8d2ECvHLhU8CXEBaEF5WKV8O3jsZkS5sXwG1__--2_jnFSlGonzXBe0qxd7tZeRvQM/
```
This will make `tty-share` listen locally on port `1234` and forward all connections to `example.com:4567` from the remote side.
The server needs to allow this, by using the `-A` flag.
## Building
Simply run
```
go get github.com/elisescu/tty-share
```
The frontend code (the code that runs in the browser session) lives under `server/frontend`, and it is compiled into `server/assets_bundle.go` go file, committed to this git repo. To rebuild this bundle of web resources, make sure you have `node` and `npm` installed, and then run: `make -C server frontend`. Unless you change the browser/frontend code, you don't need to do this - the code is already precompiled and bundled in `assets_bundle.go`.
For cross-compilation you can use the GO building [environment variables](https://golang.org/doc/install/source#environment). For example, to build the `tty-share` for raspberrypi, you can do `GOOS=linux GOARCH=arm GOARM=6 go build` (check your raspberrypi arch with `uname -a`).
For cross-compilation you can use the GO building [environment variables](https://golang.org/doc/install/source#environment). For example, to build the `tty-share` for raspberrypi, you can do `GOOS=linux GOARCH=arm GOARM=6 make out/tty-share` (you can check your raspberrypi arch with `uname -a`).
## Security
`tty-share` connects over a TLS connection to the server, which uses a proxy for the SSL termination, and the browser terminal is served over HTTPS. The communication on both sides is encrypted and secured, in the same way as other similar tools are doing it (e.g. tmate, VSC, etc).
However, end-to-end encryption is on the TODO list. Otherwise, if you don't trust my [tty-proxy](https://github.com/elisescu/tty-proxy) installation, you can run your own.
However, end-to-end encryption is still desired, so nothing but the sender and receiver can decrypt the data passed around.
## TODO
There are several improvements, and additions that can be done further:
* Update and write more tests.
* Add support for listening on sender connections over TLS.
* React on the `tty-receiver` window size as well. For now, the size of the terminal window is decided by the `tty-share`, but perhaps both the sender and receiver should have a say in this.
* Read only sessions, where the `tty_receiver` side can only watch, and cannot interact with the terminal session.
* Command line `tty_receiver` can be implemented as well, without the need of a browser.
* End-to-end encryption. Right now, the server can see what messages the sender and receiver are exchanging, but an end-to-end encryption layer can be built on top of this. It has been thought from the beginning, but it's just not implemented. The terminal IO messages are packed in protocol messages, and the payload can be easily encrypted with a shared key derived from a password that only the sender and receiver know.
* Notify the `tty-share` user when a `tty-receiver` got connected (when the remote person opened the URL in their browser).
* Many other
## Similar solutions
@ -93,7 +70,7 @@ However, the two disadvantages with this tool are the need of logging in with a
This is a great tool, and I used it quite a few times before. At the time I started my project, [tmate.io](https://tmate.io) didn't have the option to join the session from the browser, and one had to use `ssh`. In most cases, `ssh` is not a problem at all - in fact it's even preferred, but there are cases when you just don't have easy access to an `ssh` client, e.g.: joining from a Windows machine, or from your smartphone. In the meantime, the project added some support for joining a terminal session in the browser too.
Perhaps one downside with *tmate* is that it comes with quite a few dependencies which can make your life complicated if you want to compile it for ARM, for example. Running it on a raspberry pi might not be as simple as you want it, unless you use Debian.
Perhaps one downside with *tmate* is that it comes with quite a few dependencies which can make your life complicated if you want to compile it for ARM, for example. Running it on my raspberry pi might not be as simple as you want it, unless you use Debian.
## Credits

@ -1,311 +0,0 @@
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,28 +1,24 @@
# High level flow
High level architecture
=======================
## 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
## Proxy terminal sharing
+-tty-share---------------+ +-tty-server----------------+ Bob
| | | | https:// +------------+
| +------+ +-----+ | TLS | +------+ +---------+ | wss:// |tty-receiver|
| | bash | <-+-> |proto| <---------------> | proto| <-> | session | +-----+------> | 1 |
| +------+ | +-----+ | | +------+ +---------+ | | +------------+
| | | | | |
| +-> pty | +---------------------------+ | +------------+
+-------------------------+ +------> |tty-receiver|
| 2 |
+------------+
```
- 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
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.
- `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
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-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
More specific details on how this is implemented, can be seen in the source code of the `tty-share`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

@ -1,11 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

@ -1,73 +0,0 @@
# 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,19 +0,0 @@
module github.com/elisescu/tty-share
go 1.18
require (
github.com/creack/pty v1.1.11
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1
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,36 +0,0 @@
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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,98 +1,32 @@
package main
import (
"bufio"
"crypto/tls"
"crypto/x509"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"github.com/elisescu/tty-share/proxy"
"github.com/elisescu/tty-share/server"
ttyServer "github.com/elisescu/tty-share/server"
log "github.com/sirupsen/logrus"
logrus "github.com/sirupsen/logrus"
)
var log = logrus.New()
var version string = "0.0.0"
func createServer(frontListenAddress string, frontendPath string, pty server.PTYHandler, sessionID string, allowTunneling bool, crossOrigin bool) *server.TTYServer {
config := ttyServer.TTYServerConfig{
FrontListenAddress: frontListenAddress,
FrontendPath: frontendPath,
PTY: pty,
SessionID: sessionID,
AllowTunneling: allowTunneling,
CrossOrigin: crossOrigin,
}
server := ttyServer.NewTTYServer(config)
return server
}
type nilPTY struct {
}
func (nw *nilPTY) Write(data []byte) (int, error) {
return len(data), nil
}
func (nw *nilPTY) Refresh() {
}
func main() {
usageString := `
Usage:
tty-share creates a session to a terminal application with remote participants. The session can be joined either from the browser, or by tty-share command itself.
tty-share [[--args <"args">] --command <executable>] # share the terminal and get a session URL, as a server
[--logfile <file name>] [--listen <[ip]:port>]
[--frontend-path <path>] [--tty-proxy <host:port>]
[--readonly] [--public] [no-tls] [--verbose] [--version]
tty-share [--verbose] [--logfile <file name>] [-L <local_port>:<remote_host>:<remote_port>]
[--detach-keys] <session URL> # connect to an existing session, as a client
Examples:
Start bash and create a public sharing session, so it's accessible outside the local network, and make the session read only:
tty-share --public --readonly --command bash
Join a remote session by providing the URL created another tty-share command:
tty-share http://localhost:8000/s/local/
Flags:
[c] - flags that are used only by the client
[s] - flags that are used only by the server
`
commandName := flag.String("command", os.Getenv("SHELL"), "[s] The command to run")
commandName := flag.String("command", os.Getenv("SHELL"), "The command to run")
if *commandName == "" {
*commandName = "bash"
}
commandArgs := flag.String("args", "", "[s] The command arguments")
commandArgs := flag.String("args", "", "The command arguments")
logFileName := flag.String("logfile", "-", "The name of the file to log")
listenAddress := flag.String("listen", "localhost:8000", "[s] tty-server address")
useTLS := flag.Bool("useTLS", true, "Use TLS to connect to the server")
server := flag.String("server", "go.tty-share.com:7654", "tty-server address")
versionFlag := flag.Bool("version", false, "Print the tty-share version")
frontendPath := flag.String("frontend-path", "", "[s] The path to the frontend resources. By default, these resources are included in the server binary, so you only need this path if you don't want to use the bundled ones.")
proxyServerAddress := flag.String("tty-proxy", "on.tty-share.com:4567", "[s] Address of the proxy for public facing connections")
readOnly := flag.Bool("readonly", false, "[s] Start a read only session")
publicSession := flag.Bool("public", false, "[s] Create a public session")
noTLS := flag.Bool("no-tls", false, "[s] Don't use TLS to connect to the tty-proxy server. Useful for local debugging")
noWaitEnter := flag.Bool("no-wait", false, "[s] Don't wait for the Enter press before starting the session")
headless := flag.Bool("headless", false, "[s] Don't expect an interactive terminal at stdin")
headlessCols := flag.Int("headless-cols", 80, "[s] Number of cols for the allocated pty when running headless")
headlessRows := flag.Int("headless-rows", 25, "[s] Number of rows for the allocated pty when running headless")
detachKeys := flag.String("detach-keys", "ctrl-o,ctrl-c", "[c] Sequence of keys to press for closing the connection. Supported: https://godoc.org/github.com/moby/term#pkg-variables.")
allowTunneling := flag.Bool("A", false, "[s] Allow clients to create a TCP tunnel")
tunnelConfig := flag.String("L", "", "[c] TCP tunneling addresses: local_port:remote_host:remote_port. The client will listen on local_port for TCP connections, and will forward those to the from the server side to remote_host:remote_port")
crossOrgin := flag.Bool("cross-origin", false, "[s] Allow cross origin requests to the server")
verbose := flag.Bool("verbose", false, "Verbose logging")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "%s", usageString)
flag.PrintDefaults()
fmt.Fprintf(flag.CommandLine.Output(), "\n")
}
flag.Parse()
if *versionFlag {
@ -100,144 +34,106 @@ Flags:
return
}
// Log setup
log.SetLevel(log.WarnLevel)
if *verbose {
log.SetLevel(log.DebugLevel)
}
log.Level = logrus.ErrorLevel
if *logFileName != "-" {
fmt.Printf("Writing logs to: %s\n", *logFileName)
logFile, err := os.Create(*logFileName)
if err != nil {
fmt.Printf("Can't open %s for writing logs\n", *logFileName)
}
log.SetOutput(logFile)
log.Level = logrus.DebugLevel
log.Out = logFile
}
// tty-share can work in two modes: either starting a command to be shared by acting as a
// server, or by acting as a client for the remote side If we have an argument, that is not
// a flag, passed to tty-share, we expect that to be the URl to connect to, as a
// client. Otherwise, tty-share will act as the server.
args := flag.Args()
if len(args) == 1 {
connectURL := args[0]
// TODO: check we are running inside a tty environment, and exit if not
client := newTtyShareClient(connectURL, *detachKeys, tunnelConfig)
err := client.Run()
var rawConnection io.ReadWriteCloser
if *useTLS {
roots, err := x509.SystemCertPool()
if err != nil {
fmt.Printf("Cannot connect to the remote session. Make sure the URL points to a valid tty-share session.\n")
fmt.Printf("Cannot connect to the server (%s): %s", *server, err.Error())
return
}
fmt.Printf("\ntty-share disconnected\n\n")
return
}
// tty-share works as a server, from here on
if !isStdinTerminal() && !*headless {
fmt.Printf("Input not a tty\n")
os.Exit(1)
}
sessionID := ""
publicURL := ""
if *publicSession {
proxy, err := proxy.NewProxyConnection(*listenAddress, *proxyServerAddress, *noTLS)
rawConnection, err = tls.Dial("tcp", *server, &tls.Config{RootCAs: roots})
if err != nil {
log.Errorf("Can't connect to the proxy: %s\n", err.Error())
fmt.Printf("Cannot connect (TLS) to the server (%s): %s", *server, err.Error())
return
}
} else {
var err error
rawConnection, err = net.Dial("tcp", *server)
if err != nil {
fmt.Printf("Cannot connect to the server (%s): %s", *server, err.Error())
return
}
go proxy.RunProxy()
sessionID = proxy.SessionID
publicURL = proxy.PublicURL
defer proxy.Stop()
}
envVars := os.Environ()
envVars = append(envVars,
fmt.Sprintf("TTY_SHARE_LOCAL_URL=http://%s", *listenAddress),
fmt.Sprintf("TTY_SHARE=1", os.Getpid()),
)
if publicURL != "" {
envVars = append(envVars,
fmt.Sprintf("TTY_SHARE_PUBLIC_URL=%s", publicURL),
)
}
serverConnection := NewTTYProtocolConn(rawConnection)
reply, err := serverConnection.InitSender(SenderSessionInfo{
Salt: "salt",
PasswordVerifierA: "PV_A",
})
ptyMaster := ptyMasterNew(*headless, *headlessCols, *headlessRows)
err := ptyMaster.Start(*commandName, strings.Fields(*commandArgs), envVars)
if err != nil {
log.Errorf("Cannot start the %s command: %s", *commandName, err.Error())
fmt.Printf("Cannot initialise the protocol connection: %s", err.Error())
return
}
log.Infof("Web terminal: %s", reply.URLWebReadWrite)
// Display the session information to the user, before showing any output from the command.
// Wait until the user presses Enter
if publicURL != "" {
fmt.Printf("public session: %s\n", publicURL)
}
fmt.Printf("local session: http://%s/s/local/\n", *listenAddress)
if !*noWaitEnter && !*headless {
fmt.Printf("Press Enter to continue!\n")
bufio.NewReader(os.Stdin).ReadString('\n')
}
stopPtyAndRestore := func() {
ptyMaster.Stop()
ptyMaster.Restore()
}
ptyMaster.MakeRaw()
defer stopPtyAndRestore()
var pty server.PTYHandler = ptyMaster
if *readOnly {
pty = &nilPTY{}
}
fmt.Printf("Web terminal: %s\n\n\r", reply.URLWebReadWrite)
//TODO: if the user on the remote side presses keys, and so messages are sent back to the
// tty sender, they will be delivered all at once, after Enter has been pressed. Fix that.
ptyMaster := ptyMasterNew()
ptyMaster.Start(*commandName, strings.Fields(*commandArgs), func(cols, rows int) {
log.Infof("New window size: %dx%d", cols, rows)
serverConnection.SetWinSize(cols, rows)
})
server := createServer(*listenAddress, *frontendPath, pty, sessionID, *allowTunneling, *crossOrgin)
if cols, rows, e := ptyMaster.GetWinSize(); e == nil {
server.WindowSize(cols, rows)
serverConnection.SetWinSize(cols, rows)
}
ptyMaster.SetWinChangeCB(func(cols, rows int) {
log.Debugf("New window size: %dx%d", cols, rows)
server.WindowSize(cols, rows)
})
var mw io.Writer
mw = server
if !*headless {
mw = io.MultiWriter(os.Stdout, server)
}
allWriter := io.MultiWriter(os.Stdout, serverConnection)
go func() {
err := server.Run()
_, err := io.Copy(allWriter, ptyMaster)
if err != nil {
stopPtyAndRestore()
log.Errorf("Server finished: %s", err.Error())
log.Error("Lost connection with the server.\n")
ptyMaster.Stop()
}
}()
go func() {
_, err := io.Copy(mw, ptyMaster)
if err != nil {
stopPtyAndRestore()
}
}()
for {
msg, err := serverConnection.ReadMessage()
if !*headless {
go func() {
_, err := io.Copy(ptyMaster, os.Stdin)
if err != nil {
stopPtyAndRestore()
fmt.Printf(" -- Finishing the server connection with error: %s", err.Error())
break
}
}()
}
if msg.Type == MsgIDWrite {
var msgWrite MsgTTYWrite
json.Unmarshal(msg.Data, &msgWrite)
ptyMaster.Write(msgWrite.Data[:msgWrite.Size])
}
if msg.Type == MsgIDSenderNewReceiverConnected {
var msgReceiverConnected MsgTTYSenderNewReceiverConnected
json.Unmarshal(msg.Data, &msgReceiverConnected)
ptyMaster.Refresh()
fmt.Printf("New receiver connected: %s ", msgReceiverConnected.Name)
}
}
}()
go func() {
io.Copy(ptyMaster, os.Stdin)
}()
ptyMaster.Wait()
fmt.Printf("tty-share finished\n\n\r")
server.Stop()
}

@ -0,0 +1,150 @@
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
}

@ -1,150 +0,0 @@
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
}

@ -1,12 +0,0 @@
# 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.

@ -7,89 +7,63 @@ import (
"syscall"
"time"
ptyDevice "github.com/creack/pty"
log "github.com/sirupsen/logrus"
ptyDevice "github.com/elisescu/pty"
"golang.org/x/crypto/ssh/terminal"
)
type onWindowChangedCB func(int, int)
type onWindowChangesCB func(int, int)
// This defines a PTY Master whih will encapsulate the command we want to run, and provide simple
// access to the command, to write and read IO, but also to control the window size.
type ptyMaster struct {
ptyFile *os.File
command *exec.Cmd
windowChangedCB onWindowChangesCB
terminalInitState *terminal.State
headless bool
headlessCols int
headlessRows int
}
func ptyMasterNew(headless bool, headlessCols, headlessRows int) *ptyMaster {
return &ptyMaster{headless: headless, headlessCols: headlessCols, headlessRows: headlessRows}
func ptyMasterNew() *ptyMaster {
return &ptyMaster{}
}
func isStdinTerminal() bool {
return terminal.IsTerminal(0)
}
func (pty *ptyMaster) Start(command string, args []string, winChangedCB onWindowChangesCB) (err error) {
pty.windowChangedCB = winChangedCB
// Save the initial state of the terminal, before making it RAW. Note that this terminal is the
// terminal under which the tty-share command has been started, and it's identified via the
// stdin file descriptor (0 in this case)
// We need to make this terminal RAW so that when the command (passed here as a string, a shell
// usually), is receiving all the input, including the special characters:
// so no SIGINT for Ctrl-C, but the RAW character data, so no line discipline.
// Read more here: https://www.linusakesson.net/programming/tty/
pty.terminalInitState, err = terminal.MakeRaw(0)
func (pty *ptyMaster) Start(command string, args []string, envVars []string) (err error) {
pty.command = exec.Command(command, args...)
pty.command.Env = envVars
pty.ptyFile, err = ptyDevice.Start(pty.command)
if err != nil {
return
}
// Set the initial window size
cols, rows := pty.headlessCols, pty.headlessRows
// Start listening for window changes
go onWindowChanges(func(cols, rows int) {
// TODO:policy: should the server decide here if we care about the size and set it
// right here?
pty.SetWinSize(rows, cols)
if !pty.headless {
cols, rows, err = terminal.GetSize(0)
}
// Notify the ptyMaster user of the window changes, to be sent to the remote side
pty.windowChangedCB(cols, rows)
})
// Set the initial window size
cols, rows, err := terminal.GetSize(0)
pty.SetWinSize(rows, cols)
return
}
func (pty *ptyMaster) MakeRaw() (err error) {
// don't do anything if running headless
if pty.headless {
return nil
}
// Save the initial state of the terminal, before making it RAW. Note that this terminal is the
// terminal under which the tty-share command has been started, and it's identified via the
// stdin file descriptor (0 in this case)
// We need to make this terminal RAW so that when the command (passed here as a string, a shell
// usually), is receiving all the input, including the special characters:
// so no SIGINT for Ctrl-C, but the RAW character data, so no line discipline.
// Read more here: https://www.linusakesson.net/programming/tty/
pty.terminalInitState, err = terminal.MakeRaw(int(os.Stdin.Fd()))
return
}
func (pty *ptyMaster) SetWinChangeCB(winChangedCB onWindowChangedCB) {
// Start listening for window changes if not running headless
if !pty.headless {
go onWindowChanges(func(cols, rows int) {
// TODO:policy: should the server decide here if we care about the size and set it
// right here?
pty.SetWinSize(rows, cols)
// Notify the ptyMaster user of the window changes, to be sent to the remote side
winChangedCB(cols, rows)
})
}
}
func (pty *ptyMaster) GetWinSize() (int, int, error) {
if pty.headless {
return pty.headlessCols, pty.headlessRows, nil
} else {
return terminal.GetSize(0)
}
cols, rows, err := terminal.GetSize(0)
return cols, rows, err
}
func (pty *ptyMaster) Write(b []byte) (int, error) {
@ -101,11 +75,7 @@ func (pty *ptyMaster) Read(b []byte) (int, error) {
}
func (pty *ptyMaster) SetWinSize(rows, cols int) {
winSize := ptyDevice.Winsize{
Rows: uint16(rows),
Cols: uint16(cols),
}
ptyDevice.Setsize(pty.ptyFile, &winSize)
ptyDevice.Setsize(pty.ptyFile, rows, cols)
}
func (pty *ptyMaster) Refresh() {
@ -127,13 +97,8 @@ func (pty *ptyMaster) Refresh() {
func (pty *ptyMaster) Wait() (err error) {
err = pty.command.Wait()
return
}
func (pty *ptyMaster) Restore() {
if !pty.headless {
terminal.Restore(0, pty.terminalInitState)
}
// The terminal has to be restored from the RAW state, to its initial state
terminal.Restore(0, pty.terminalInitState)
return
}
@ -148,9 +113,9 @@ func (pty *ptyMaster) Stop() (err error) {
return
}
func onWindowChanges(wcCB onWindowChangedCB) {
wcChan := make(chan os.Signal, 1)
signal.Notify(wcChan, syscall.SIGWINCH)
func onWindowChanges(winChangedCb func(cols, rows int)) {
winChangedSig := make(chan os.Signal, 1)
signal.Notify(winChangedSig, syscall.SIGWINCH)
// The interface for getting window changes from the pty slave to its process, is via signals.
// In our case here, the tty-share command (built in this project) is the client, which should
// get notified if the terminal window in which it runs has changed. To get that, it needs to
@ -163,10 +128,10 @@ func onWindowChanges(wcCB onWindowChangedCB) {
for {
select {
case <-wcChan:
case <-winChangedSig:
cols, rows, err := terminal.GetSize(0)
if err == nil {
wcCB(cols, rows)
winChangedCb(cols, rows)
} else {
log.Warnf("Can't get window size: %s", err.Error())
}

@ -3,9 +3,9 @@ VERSION=$(git describe --tags `git rev-list --tags --max-count=1` | awk '{print
OUTDIR=out
mkdir -p ${OUTDIR} && \
GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.rpi && \
GOOS=linux go build -mod=vendor -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.lin && \
GOOS=darwin go build -mod=vendor -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.mac && \
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.rpi && \
GOOS=linux go build -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.lin && \
GOOS=darwin go build -ldflags "-X main.version=${VERSION}" -o ${OUTDIR}/tty-share.mac && \
zip ${OUTDIR}/tty-share.rpi.zip ${OUTDIR}/tty-share.rpi && \
zip ${OUTDIR}/tty-share.lin.zip ${OUTDIR}/tty-share.lin && \
zip ${OUTDIR}/tty-share.mac.zip ${OUTDIR}/tty-share.mac

@ -1,24 +0,0 @@
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

@ -1,9 +0,0 @@
# 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

@ -1,22 +0,0 @@
{
"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"
}
}

@ -1,147 +0,0 @@
@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

@ -1,22 +0,0 @@
<!-- 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>

@ -1,19 +0,0 @@
<!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>

@ -1,10 +0,0 @@
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}

@ -1,156 +0,0 @@
/**
*
* 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;

@ -1,13 +0,0 @@
html, body {
width: 100%;
height: 100%;
margin: 0;
}
#terminal {
position: fixed;
top: 0;
width: 100%;
height: 100%;
}

@ -1,24 +0,0 @@
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);

@ -1,120 +0,0 @@
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
}

@ -1,48 +0,0 @@
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;

@ -1,308 +0,0 @@
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()
}

@ -1,114 +0,0 @@
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")
}

@ -1,134 +0,0 @@
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
}

@ -1,64 +0,0 @@
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()
}

@ -0,0 +1,118 @@
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
}

@ -1,21 +0,0 @@
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.

@ -1,12 +0,0 @@
# 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.

@ -1,188 +0,0 @@
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

@ -1,7 +0,0 @@
package ansiterm
type ansiContext struct {
currentChar byte
paramBuffer []byte
interBuffer []byte
}

@ -1,49 +0,0 @@
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
}

@ -1,38 +0,0 @@
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
}

@ -1,36 +0,0 @@
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
}

@ -1,47 +0,0 @@
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
}

@ -1,90 +0,0 @@
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
}

@ -1,24 +0,0 @@
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
}

@ -1,31 +0,0 @@
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
}

@ -1,151 +0,0 @@
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
}

@ -1,99 +0,0 @@
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
}

@ -1,119 +0,0 @@
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)
}

@ -1,71 +0,0 @@
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
}

@ -1,21 +0,0 @@
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
}

@ -1,196 +0,0 @@
// +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)
}

@ -1,327 +0,0 @@
// +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{}) {}

@ -1,100 +0,0 @@
// +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)
}

@ -1,101 +0,0 @@
// +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
}

@ -1,84 +0,0 @@
// +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}, &region)
if err != nil {
return err
}
return nil
}

@ -1,118 +0,0 @@
// +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
}

@ -1,9 +0,0 @@
// +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)
}

@ -1,743 +0,0 @@
// +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, &region); 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
}

@ -1,14 +0,0 @@
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

@ -1,100 +0,0 @@
# 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)
}
}
```

@ -1,30 +0,0 @@
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))
}

@ -1,33 +0,0 @@
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
}

@ -1,139 +0,0 @@
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,74 +0,0 @@
// +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
}

@ -1,50 +0,0 @@
#!/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 .

@ -1,64 +0,0 @@
// +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
}

@ -1,51 +0,0 @@
//
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)
}

@ -1,13 +0,0 @@
// 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
}

@ -1,13 +0,0 @@
// +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

@ -1,11 +0,0 @@
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
// cgo -godefs types.go
// +build riscv riscv64
package pty
type (
_C_int int32
_C_uint uint32
)

@ -0,0 +1,36 @@
# pty
Pty is a Go package for using unix pseudo-terminals.
## Install
go get github.com/kr/pty
## Example
```go
package main
import (
"github.com/kr/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)
}
```

@ -1,4 +1,4 @@
// +build !windows,!solaris
// +build !windows
package pty

@ -13,7 +13,7 @@ GODEFS="go tool cgo -godefs"
$GODEFS types.go |gofmt > ztypes_$GOARCH.go
case $GOOS in
freebsd|dragonfly|openbsd)
freebsd|dragonfly)
$GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go
;;
esac

@ -13,23 +13,19 @@ func open() (pty, tty *os.File, err error) {
return nil, nil, err
}
p := os.NewFile(uintptr(pFD), "/dev/ptmx")
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := grantpt(p); err != nil {
err = grantpt(p)
if err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
err = unlockpt(p)
if err != nil {
return nil, nil, err
}

@ -14,23 +14,19 @@ func open() (pty, tty *os.File, err error) {
if err != nil {
return nil, nil, err
}
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := grantpt(p); err != nil {
err = grantpt(p)
if err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
err = unlockpt(p)
if err != nil {
return nil, nil, err
}

@ -7,28 +7,22 @@ import (
"unsafe"
)
func posixOpenpt(oflag int) (fd int, err error) {
func posix_openpt(oflag int) (fd int, err error) {
r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0)
fd = int(r0)
if e1 != 0 {
err = e1
}
return fd, err
return
}
func open() (pty, tty *os.File, err error) {
fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC)
fd, err := posix_openpt(syscall.O_RDWR | syscall.O_CLOEXEC)
if err != nil {
return nil, nil, err
}
p := os.NewFile(uintptr(fd), "/dev/pts")
// In case of error after this point, make sure we close the pts fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
p := os.NewFile(uintptr(fd), "/dev/pts")
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
@ -48,7 +42,7 @@ func isptmaster(fd uintptr) (bool, error) {
var (
emptyFiodgnameArg fiodgnameArg
ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
ioctl_FIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
)
func ptsname(f *os.File) (string, error) {
@ -65,7 +59,8 @@ func ptsname(f *os.File) (string, error) {
buf = make([]byte, n)
arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))}
)
if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil {
err = ioctl(f.Fd(), ioctl_FIODGNAME, uintptr(unsafe.Pointer(&arg)))
if err != nil {
return "", err
}

@ -12,19 +12,14 @@ func open() (pty, tty *os.File, err error) {
if err != nil {
return nil, nil, err
}
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
err = unlockpt(p)
if err != nil {
return nil, nil, err
}
@ -46,6 +41,6 @@ func ptsname(f *os.File) (string, error) {
func unlockpt(f *os.File) error {
var u _C_int
// use TIOCSPTLCK with a pointer to zero to clear the lock
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}

@ -1,4 +1,4 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris
// +build !linux,!darwin,!freebsd,!dragonfly
package pty

@ -0,0 +1,34 @@
// +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.
func Start(c *exec.Cmd) (pty *os.File, err error) {
pty, tty, err := Open()
if err != nil {
return nil, err
}
defer tty.Close()
c.Stdout = tty
c.Stdin = tty
c.Stderr = tty
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Setctty = true
c.SysProcAttr.Setsid = true
err = c.Start()
if err != nil {
pty.Close()
return nil, err
}
return pty, err
}

@ -0,0 +1,10 @@
// +build ignore
package pty
import "C"
type (
_C_int C.int
_C_uint C.uint
)

@ -0,0 +1,17 @@
// +build ignore
package pty
/*
#define _KERNEL
#include <sys/conf.h>
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodname_args

@ -0,0 +1,15 @@
// +build ignore
package pty
/*
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodgname_arg

@ -0,0 +1,64 @@
// +build !windows
package pty
import (
"os"
"syscall"
"unsafe"
)
// 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) {
var ws winsize
err = getwindowrect(&ws, t.Fd())
return int(ws.ws_row), int(ws.ws_col), err
}
// Setsize sets the number of rows (lines) and cols (positions
// in each line) in terminal t. Both rows and cols have to be
// positive integers.
func Setsize(t *os.File, rows, cols int) error {
ws := winsize{
ws_col: uint16(cols),
ws_row: uint16(rows),
ws_xpixel: uint16(0), // not used
ws_ypixel: uint16(0), // not used
}
return setwindowrect(&ws, t.Fd())
}
type winsize struct {
ws_row uint16
ws_col uint16
ws_xpixel uint16
ws_ypixel uint16
}
func getwindowrect(ws *winsize, fd uintptr) error {
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
fd,
syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(ws)),
)
if errno != 0 {
return syscall.Errno(errno)
}
return nil
}
func setwindowrect(ws *winsize, fd uintptr) error {
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
fd,
syscall.TIOCSWINSZ,
uintptr(unsafe.Pointer(ws)),
)
if errno != 0 {
return syscall.Errno(errno)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save