Implemented basic platform abstraction layer for Pty and Process creation;

Added a platform package that exposes a few interfaces and provides different implementations for different platforms;
Windows build and dev env setup instructions;
Setup Travis Linux to run tests and build sources.
This commit is contained in:
nikitar020 2018-12-03 17:30:04 +03:00 committed by Max Risuhin
parent bf90bfb334
commit 92a0bf245c
9 changed files with 572 additions and 18 deletions

25
.travis.yml Normal file
View File

@ -0,0 +1,25 @@
language: go
go:
- "1.10.5"
- "1.11.2"
- "master" # find out if an upcoming change in Go is going to break us
matrix:
# We only want to fail the build based on stable versions of Go
allow_failures:
- go: master
fast_finish: true
go_import_path: github.com/liamg/aminal
before_install:
- sudo apt-get install -y xorg-dev libgl1-mesa-dev
script:
- make test
- make build-linux
env:
global:
secure: MpOY0NYnuf8hgDVP4MjSSrrzWDdPlJ5JnB/TiiJxptjN2qn0OKPVBeZl2OsJ3MlZfjNM3wZ3mNN6p/3TwJmfp1g+3SU9MfRul4XMFfJKiN9u5/y5lamophs+vkgrawG00xwMd4KvKOqXVP2vX2s+koVfUzUXEot5IJDMerQltQPqbonvk/tWJHmtu5NoMQgGO+2pvmy+74u5HMfjoWSmTBiJgTUZ8c/a1nK3MsG2mafsEB8sLa8uMWAp9ISnfxLzTTUFQeVR64i1z7duz5fJ0B96JqYIlwnR9vzeB1gr+3+cSdDUPfcTgWEDhDn9Kglqe71K+9vUaP3cOkEMThFPoj1wb2vyIV2LYVqsAVAmxBFiyMS8HVkp7z0M7f34wRhd3blN2KaxYuiFlSg3MwnUp615NHnnS8kA0So/E7xYcXnsfFx9f1ZZqaAuHz1DrD2UHeRKtcekdcxTMG4LMJbGv+HsqL6mUFIND/tN9h/QV+w+GbmKUUBt/xMmyoaLqBwKz+m1aLvBSZ9IaUIHRKCNnCfm9QPXoMfeXvh2JCX7leM/0FDHUp8v1J1Rw5E0eiWpBS0OlQS2OeR8zAlJh8mZ4SpApUEHDEHBbcVSP0ZM+fSMpq1OVESlmEO308WZY0IlmQ9UkDIpixLuizDB4IlwOPVXlRSyHd9spAmqx4ZGfR4=

15
main.go
View File

@ -3,12 +3,10 @@ package main
import (
"fmt"
"os"
"os/exec"
"runtime"
"syscall"
"github.com/kr/pty"
"github.com/liamg/aminal/gui"
"github.com/liamg/aminal/platform"
"github.com/liamg/aminal/terminal"
"github.com/riywo/loginshell"
)
@ -26,7 +24,8 @@ func main() {
defer logger.Sync()
logger.Infof("Allocating pty...")
pty, tty, err := pty.Open()
pty, err := platform.NewPty(80, 25)
if err != nil {
logger.Fatalf("Failed to allocate pty: %s", err)
}
@ -43,12 +42,8 @@ func main() {
os.Setenv("TERM", "xterm-256color") // controversial! easier than installing terminfo everywhere, but obviously going to be slightly different to xterm functionality, so we'll see...
os.Setenv("COLORTERM", "truecolor")
shell := exec.Command(shellStr)
shell.Stdout = tty
shell.Stdin = tty
shell.Stderr = tty
shell.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
if err := shell.Start(); err != nil {
_, err = pty.CreateGuestProcess(shellStr)
if err != nil {
pty.Close()
logger.Fatalf("Failed to start your shell: %s", err)
}

24
platform/cmdProc.go Normal file
View File

@ -0,0 +1,24 @@
package platform
import (
"os/exec"
)
type cmdProc struct {
cmd *exec.Cmd
}
func newCmdProc(c *exec.Cmd) *cmdProc {
return &cmdProc{
cmd: c,
}
}
func (p *cmdProc) Close() error {
if p == nil || p.cmd == nil || p.cmd.Process == nil {
return nil
}
ret := p.cmd.Process.Kill()
p.cmd = nil
return ret
}

20
platform/interfaces.go Normal file
View File

@ -0,0 +1,20 @@
package platform
import (
"io"
)
// Process represents a child process by pid or HPROCESS in a platform-independent way
type Process interface {
io.Closer
// TODO: make useful stuff here
}
// Pty represents a pseudo-terminal either by pty/tty file pair or by HCON
type Pty interface {
io.ReadWriteCloser
Resize(x int, y int) error
CreateGuestProcess(imagePath string) (Process, error)
}

93
platform/unixpty.go Normal file
View File

@ -0,0 +1,93 @@
//+build !windows
package platform
import (
"errors"
"os"
"os/exec"
"syscall"
"unsafe"
"github.com/kr/pty"
)
type unixPty struct {
pty *os.File
tty *os.File
}
type winsize struct {
Height uint16
Width uint16
x uint16 //ignored, but necessary for ioctl calls
y uint16 //ignored, but necessary for ioctl calls
}
func (p *unixPty) Read(b []byte) (int, error) {
if p == nil || p.pty == nil {
return 0, errors.New("Attempted to read from a deallocated pty")
}
return p.pty.Read(b)
}
func (p *unixPty) Write(b []byte) (int, error) {
if p == nil || p.pty == nil {
return 0, errors.New("Attempted to write to a deallocated pty")
}
return p.pty.Write(b)
}
func (p *unixPty) Close() error {
if p == nil || p.pty == nil {
return nil
}
ret := p.pty.Close()
p.pty = nil
p.tty = nil
return ret
}
func (p *unixPty) Resize(x, y int) error {
size := winsize{
Height: uint16(y),
Width: uint16(x),
x: 0,
y: 0,
}
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(p.pty.Fd()),
uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(&size)))
if errno != 0 {
return errors.New(errno.Error())
}
return nil
}
func (p *unixPty) CreateGuestProcess(imagePath string) (Process, error) {
if p == nil || p.tty == nil {
return nil, errors.New("Attempted to create a process on a deallocated pty")
}
shell := newCmdProc(exec.Command(imagePath))
shell.cmd.Stdout = p.tty
shell.cmd.Stdin = p.tty
shell.cmd.Stderr = p.tty
shell.cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
if err := shell.cmd.Start(); err != nil {
return nil, err
}
return shell, nil
}
func NewPty(x, y int) (Pty, error) {
innerPty, innerTty, err := pty.Open()
if err != nil {
return nil, err
}
return &unixPty{
pty: innerPty,
tty: innerTty,
}, nil
}

184
platform/winproc.go Normal file
View File

@ -0,0 +1,184 @@
// +build windows,cgo
package platform
// #include "Windows.h"
//
// /* Until we can specify the platform SDK and target version for Windows.h *
// * without breaking our ability to gracefully display an error message, these *
// * definitions will be copied from the platform SDK headers and made to work. *
// */
//
// typedef BOOL (* InitializeProcThreadAttributeListProcType)(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T);
// typedef BOOL (* UpdateProcThreadAttributeProcType)(
// LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// DWORD dwFlags,
// DWORD_PTR Attribute,
// PVOID lpValue,
// SIZE_T cbSize,
// PVOID lpPreviousValue,
// PSIZE_T lpReturnSize
// );
//
// InitializeProcThreadAttributeListProcType pfnInitializeProcThreadAttributeList = NULL;
// UpdateProcThreadAttributeProcType pfnUpdateProcThreadAttribute = NULL;
//
// #define ProcThreadAttributePseudoConsole_copy 22
//
// #define PROC_THREAD_ATTRIBUTE_NUMBER_copy 0x0000FFFF
// #define PROC_THREAD_ATTRIBUTE_THREAD_copy 0x00010000 // Attribute may be used with thread creation
// #define PROC_THREAD_ATTRIBUTE_INPUT_copy 0x00020000 // Attribute is input only
// #define PROC_THREAD_ATTRIBUTE_ADDITIVE_copy 0x00040000 // Attribute may be "accumulated," e.g. bitmasks, counters, etc.
//
// #define ProcThreadAttributeValue_copy(Number, Thread, Input, Additive) \
// (((Number) & PROC_THREAD_ATTRIBUTE_NUMBER_copy) | \
// ((Thread != FALSE) ? PROC_THREAD_ATTRIBUTE_THREAD_copy : 0) | \
// ((Input != FALSE) ? PROC_THREAD_ATTRIBUTE_INPUT_copy : 0) | \
// ((Additive != FALSE) ? PROC_THREAD_ATTRIBUTE_ADDITIVE_copy : 0))
//
// #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE_copy ProcThreadAttributeValue_copy (ProcThreadAttributePseudoConsole_copy, FALSE, TRUE, FALSE)
//
// typedef struct _STARTUPINFOEXW_copy {
// STARTUPINFOW StartupInfo;
// LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
// } STARTUPINFOEXW_copy, *LPSTARTUPINFOEXW_copy;
//
// HMODULE hLibKernel32_Proc = NULL;
//
// DWORD initProcKernFuncs()
// {
// hLibKernel32_Proc = LoadLibrary( "kernel32.dll" );
// if( hLibKernel32_Proc == NULL )
// {
// return -1;
// }
//
// pfnInitializeProcThreadAttributeList = (InitializeProcThreadAttributeListProcType) GetProcAddress(hLibKernel32_Proc, "InitializeProcThreadAttributeList" );
// if( pfnInitializeProcThreadAttributeList == NULL )
// {
// return -1;
// }
//
// pfnUpdateProcThreadAttribute = (UpdateProcThreadAttributeProcType) GetProcAddress(hLibKernel32_Proc, "UpdateProcThreadAttribute" );
// if( pfnUpdateProcThreadAttribute == NULL )
// {
// return -1;
// }
//
// return 0;
// }
//
// DWORD createGuestProcHelper( uintptr_t hpc, LPCWSTR imagePath, uintptr_t * hProcess )
// {
// STARTUPINFOEXW_copy si;
// ZeroMemory( &si, sizeof(si) );
// si.StartupInfo.cb = sizeof(si);
//
// size_t bytesRequired;
// (*pfnInitializeProcThreadAttributeList)( NULL, 1, 0, &bytesRequired );
//
// si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, bytesRequired);
// if( !si.lpAttributeList )
// {
// return E_OUTOFMEMORY;
// }
//
// if (!(*pfnInitializeProcThreadAttributeList)(si.lpAttributeList, 1, 0, &bytesRequired))
// {
// HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
// return HRESULT_FROM_WIN32(GetLastError());
// }
//
// if (!(*pfnUpdateProcThreadAttribute)(si.lpAttributeList,
// 0,
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE_copy,
// (PVOID) hpc,
// sizeof(hpc),
// NULL,
// NULL))
// {
// HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
// return HRESULT_FROM_WIN32(GetLastError());
// }
//
// bytesRequired = (wcslen(imagePath) + 1) * sizeof(wchar_t); // +1 null terminator
// PWSTR cmdLineMutable = (PWSTR)HeapAlloc(GetProcessHeap(), 0, bytesRequired);
//
// if (!cmdLineMutable)
// {
// HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
// return E_OUTOFMEMORY;
// }
//
// wcscpy_s(cmdLineMutable, bytesRequired, imagePath);
//
// PROCESS_INFORMATION pi;
// ZeroMemory(&pi, sizeof(pi));
//
// if (!CreateProcessW(NULL,
// cmdLineMutable,
// NULL,
// NULL,
// FALSE,
// EXTENDED_STARTUPINFO_PRESENT,
// NULL,
// NULL,
// &si.StartupInfo,
// &pi))
// {
// HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
// HeapFree(GetProcessHeap(), 0, cmdLineMutable);
// return HRESULT_FROM_WIN32(GetLastError());
// }
//
// *hProcess = (uintptr_t) pi.hProcess;
//
// HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
// HeapFree(GetProcessHeap(), 0, cmdLineMutable);
// return S_OK;
// }
//
// int hr_succeeded( DWORD hResult );
import "C"
import (
"errors"
"syscall"
"unicode/utf16"
)
var procsInitSucceeded = false
func init() {
ret := int(C.initProcKernFuncs())
procsInitSucceeded = (ret == 0)
}
type winProcess struct {
hproc uintptr
}
func createPtyChildProcess(imagePath string, hcon uintptr) (Process, error) {
path16 := utf16.Encode([]rune(imagePath))
cpath16 := C.calloc(C.size_t(len(path16)+1), 2)
pp := (*[1 << 30]uint16)(cpath16)
copy(pp[:], path16)
hproc := C.uintptr_t(0)
hr := C.createGuestProcHelper(C.uintptr_t(hcon), (C.LPCWSTR)(cpath16), &hproc)
C.free(cpath16)
if int(C.hr_succeeded(hr)) == 0 {
return nil, errors.New("Failed to create process: " + imagePath)
}
return &winProcess{
hproc: uintptr(hproc),
}, nil
}
func (process *winProcess) Close() error {
return syscall.CloseHandle(syscall.Handle(process.hproc))
}

187
platform/winpty.go Normal file
View File

@ -0,0 +1,187 @@
// +build windows,cgo
package platform
import (
"errors"
"syscall"
"github.com/MaxRis/w32"
)
// #include "Windows.h"
//
// /* Until we can specify the platform SDK and target version for Windows.h *
// * without breaking our ability to gracefully display an error message, these *
// * definitions will be copied from the platform SDK headers and made to work. *
// */
//
// typedef HRESULT (* CreatePseudoConsoleProcType)( COORD, HANDLE, HANDLE, DWORD, uintptr_t * );
// typedef HRESULT (* ResizePseudoConsoleProcType)( uintptr_t, COORD );
// typedef HRESULT (* ClosePseudoConsoleProcType)( uintptr_t );
//
//
// CreatePseudoConsoleProcType pfnCreatePseudoConsole = NULL;
// ResizePseudoConsoleProcType pfnResizePseudoConsole = NULL;
// ClosePseudoConsoleProcType pfnClosePseudoConsole = NULL;
//
// HMODULE hLibKernel32_Kern = NULL;
//
// DWORD initPtyKernFuncs()
// {
// hLibKernel32_Kern = LoadLibrary( "kernel32.dll" );
// if( hLibKernel32_Kern == NULL )
// {
// return -1;
// }
//
// pfnCreatePseudoConsole = (CreatePseudoConsoleProcType) GetProcAddress(hLibKernel32_Kern, "CreatePseudoConsole" );
// if( pfnCreatePseudoConsole == NULL )
// {
// return -1;
// }
//
// pfnResizePseudoConsole = (ResizePseudoConsoleProcType) GetProcAddress(hLibKernel32_Kern, "ResizePseudoConsole" );
// if( pfnResizePseudoConsole == NULL )
// {
// return -1;
// }
//
// pfnClosePseudoConsole = (ClosePseudoConsoleProcType) GetProcAddress(hLibKernel32_Kern, "ClosePseudoConsole" );
// if( pfnClosePseudoConsole == NULL )
// {
// return -1;
// }
//
// return 0;
// }
//
// DWORD createPtyHelper( int xSize, int ySize, HANDLE input, HANDLE output, DWORD flags, uintptr_t * phPC )
// {
// COORD size;
// size.X = xSize;
// size.Y = ySize;
// return (DWORD) (*pfnCreatePseudoConsole)( size, input, output, flags, phPC );
// }
//
// DWORD resizePtyHelper( uintptr_t hpc, int xSize, int ySize )
// {
// COORD size;
// size.X = xSize;
// size.Y = ySize;
// return (DWORD) (*pfnResizePseudoConsole)( hpc, size );
// }
//
// DWORD closePtyHelper( uintptr_t hpc )
// {
// return (DWORD) (*pfnClosePseudoConsole)( hpc );
// }
//
// int hr_succeeded( DWORD hResult )
// {
// return SUCCEEDED( hResult );
// }
import "C"
var ptyInitSucceeded = false
func init() {
ret := int(C.initPtyKernFuncs())
ptyInitSucceeded = (ret == 0)
}
type winConPty struct {
inPipe syscall.Handle
outPipe syscall.Handle
innerInPipe syscall.Handle
innerOutPipe syscall.Handle
hcon uintptr
}
func (pty *winConPty) Read(p []byte) (n int, err error) {
return syscall.Read(pty.inPipe, p)
}
func (pty *winConPty) Write(p []byte) (n int, err error) {
return syscall.Write(pty.outPipe, p)
}
func (pty *winConPty) Close() error {
C.closePtyHelper(C.uintptr_t(pty.hcon))
err := syscall.CloseHandle(pty.inPipe)
if err != nil {
return err
}
err = syscall.CloseHandle(pty.outPipe)
if err != nil {
return err
}
err = syscall.CloseHandle(pty.innerInPipe)
if err != nil {
return err
}
err = syscall.CloseHandle(pty.innerOutPipe)
if err != nil {
return err
}
pty.hcon = 0
return nil
}
func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) {
return createPtyChildProcess(imagePath, pty.hcon)
}
func (pty *winConPty) Resize(x, y int) error {
cret := C.resizePtyHelper(C.uintptr_t(pty.hcon), C.int(x), C.int(y))
if int(C.hr_succeeded(cret)) == 0 {
return errors.New("Failed to resize ConPTY")
}
return nil
}
// NewPty creates a new instance of a Pty implementation for Windows on a newly allocated ConPTY
func NewPty(x, y int) (pty Pty, err error) {
if !ptyInitSucceeded {
w32.MessageBox(0, "Aminal requires APIs that are only available on Windows 10 1809 (October 2018 Update) or above. Please upgrade", "Aminal", 0)
return nil, errors.New("Windows PseudoConsole API unavailable on this version of Windows")
}
pty = nil
var inputReadSide, inputWriteSide syscall.Handle
var outputReadSide, outputWriteSide syscall.Handle
err = syscall.CreatePipe(&inputReadSide, &inputWriteSide, nil, 0)
if err != nil {
return
}
err = syscall.CreatePipe(&outputReadSide, &outputWriteSide, nil, 0)
if err != nil {
return
}
var hc C.uintptr_t
cret := C.createPtyHelper(C.int(x), C.int(y), C.HANDLE(inputReadSide), C.HANDLE(outputWriteSide), 0, &hc)
ret := int(cret)
if ret != 0 {
return nil, errors.New("Failed to allocate a ConPTY instance")
}
pty = &winConPty{
inPipe: outputReadSide,
outPipe: inputWriteSide,
innerInPipe: inputReadSide,
innerOutPipe: outputWriteSide,
hcon: uintptr(hc),
}
return pty, nil
}

View File

@ -4,13 +4,11 @@ import (
"bufio"
"fmt"
"io"
"os"
"sync"
"syscall"
"unsafe"
"github.com/liamg/aminal/buffer"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/platform"
"go.uber.org/zap"
)
@ -36,7 +34,7 @@ type Terminal struct {
buffers []*buffer.Buffer
activeBuffer *buffer.Buffer
lock sync.Mutex
pty *os.File
pty platform.Pty
logger *zap.SugaredLogger
title string
size Winsize
@ -64,7 +62,7 @@ type Winsize struct {
y uint16 //ignored, but necessary for ioctl calls
}
func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Terminal {
func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Terminal {
t := &Terminal{
buffers: []*buffer.Buffer{
buffer.NewBuffer(1, 1, buffer.CellAttributes{
@ -280,9 +278,8 @@ func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
terminal.size.Width = uint16(newCols)
terminal.size.Height = uint16(newLines)
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(terminal.pty.Fd()),
uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(&terminal.size)))
if err != 0 {
err := terminal.pty.Resize(int(newCols), int(newLines))
if err != nil {
return fmt.Errorf("Failed to set terminal size vai ioctl: Error no %d", err)
}

29
windows.md Normal file
View File

@ -0,0 +1,29 @@
### Windows dev env setup and build instructions:
1. Setup choco package manager https://chocolatey.org/docs/installation
2. Use `choco` to install golang and mingw
```choco install golang mingw```
### Setting aminal GoLang build env and directories structures for the project:
```
cd %YOUR_PROJECT_WORKING_DIR%
mkdir go\src\github.com\liamg
cd go\src\github.com\liamg
git clone git@github.com:jumptrading/aminal-mirror.git
move aminal-mirror aminal
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
set GOBIN=%GOPATH%/bin
set PATH=%GOBIN%;%PATH%
cd aminal
go get
go build
go install
```
Look for the aminal.exe built binary under your %GOBIN% path