Commit 09a06448 authored by Abhi Shah's avatar Abhi Shah

Merge pull request #9654 from rjnagal/ux

Update docker dependencies.
parents c08f202f 256e03ff
...@@ -151,33 +151,33 @@ ...@@ -151,33 +151,33 @@
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/jsonmessage", "ImportPath": "github.com/docker/docker/pkg/jsonmessage",
"Comment": "v1.7.rc", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "0cd6c05d8112e9246b734107d54e2855e3d5fec5" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/mount", "ImportPath": "github.com/docker/docker/pkg/mount",
"Comment": "v1.4.1-1714-ged66853", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "ed6685369740035b0af9675bf9add52d0af7657b" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/parsers", "ImportPath": "github.com/docker/docker/pkg/parsers",
"Comment": "v1.4.1-1714-ged66853", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "ed6685369740035b0af9675bf9add52d0af7657b" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/term", "ImportPath": "github.com/docker/docker/pkg/term",
"Comment": "v1.4.1-1714-ged66853", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "ed6685369740035b0af9675bf9add52d0af7657b" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/timeutils", "ImportPath": "github.com/docker/docker/pkg/timeutils",
"Comment": "v1.7.rc", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "0cd6c05d8112e9246b734107d54e2855e3d5fec5" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/docker/pkg/units", "ImportPath": "github.com/docker/docker/pkg/units",
"Comment": "v1.4.1-1714-ged66853", "Comment": "v1.4.1-4045-g2b27fe1",
"Rev": "ed6685369740035b0af9675bf9add52d0af7657b" "Rev": "2b27fe17a1b3fb8472fde96d768fa70996adf201"
}, },
{ {
"ImportPath": "github.com/docker/libcontainer", "ImportPath": "github.com/docker/libcontainer",
......
package jsonmessage package jsonmessage
import ( import (
"bytes"
"fmt"
"testing" "testing"
"time"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/pkg/timeutils"
"strings"
) )
func TestError(t *testing.T) { func TestError(t *testing.T) {
...@@ -23,16 +30,181 @@ func TestProgress(t *testing.T) { ...@@ -23,16 +30,181 @@ func TestProgress(t *testing.T) {
t.Fatalf("Expected %q, got %q", expected, jp2.String()) t.Fatalf("Expected %q, got %q", expected, jp2.String())
} }
expected = "[=========================> ] 50 B/100 B" expectedStart := "[==========> ] 20 B/100 B"
jp3 := JSONProgress{Current: 50, Total: 100} jp3 := JSONProgress{Current: 20, Total: 100, Start: time.Now().Unix()}
if jp3.String() != expected { // Just look at the start of the string
t.Fatalf("Expected %q, got %q", expected, jp3.String()) // (the remaining time is really hard to test -_-)
if jp3.String()[:len(expectedStart)] != expectedStart {
t.Fatalf("Expected to start with %q, got %q", expectedStart, jp3.String())
} }
// this number can't be negetive gh#7136 expected = "[=========================> ] 50 B/100 B"
expected = "[==================================================>] 50 B/40 B" jp4 := JSONProgress{Current: 50, Total: 100}
jp4 := JSONProgress{Current: 50, Total: 40}
if jp4.String() != expected { if jp4.String() != expected {
t.Fatalf("Expected %q, got %q", expected, jp4.String()) t.Fatalf("Expected %q, got %q", expected, jp4.String())
} }
// this number can't be negative gh#7136
expected = "[==================================================>] 50 B/40 B"
jp5 := JSONProgress{Current: 50, Total: 40}
if jp5.String() != expected {
t.Fatalf("Expected %q, got %q", expected, jp5.String())
}
}
func TestJSONMessageDisplay(t *testing.T) {
now := time.Now().Unix()
messages := map[JSONMessage][]string{
// Empty
JSONMessage{}: {"\n", "\n"},
// Status
JSONMessage{
Status: "status",
}: {
"status\n",
"status\n",
},
// General
JSONMessage{
Time: now,
ID: "ID",
From: "From",
Status: "status",
}: {
fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now, 0).Format(timeutils.RFC3339NanoFixed)),
fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now, 0).Format(timeutils.RFC3339NanoFixed)),
},
// Stream over status
JSONMessage{
Status: "status",
Stream: "stream",
}: {
"stream",
"stream",
},
// With progress message
JSONMessage{
Status: "status",
ProgressMessage: "progressMessage",
}: {
"status progressMessage",
"status progressMessage",
},
// With progress, stream empty
JSONMessage{
Status: "status",
Stream: "",
Progress: &JSONProgress{Current: 1},
}: {
"",
fmt.Sprintf("%c[2K\rstatus 1 B\r", 27),
},
}
// The tests :)
for jsonMessage, expectedMessages := range messages {
// Without terminal
data := bytes.NewBuffer([]byte{})
if err := jsonMessage.Display(data, false); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[0] {
t.Fatalf("Expected [%v], got [%v]", expectedMessages[0], data.String())
}
// With terminal
data = bytes.NewBuffer([]byte{})
if err := jsonMessage.Display(data, true); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[1] {
t.Fatalf("Expected [%v], got [%v]", expectedMessages[1], data.String())
}
}
}
// Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code.
func TestJSONMessageDisplayWithJSONError(t *testing.T) {
data := bytes.NewBuffer([]byte{})
jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}}
err := jsonMessage.Display(data, true)
if err == nil || err.Error() != "Can't find it" {
t.Fatalf("Expected a JSONError 404, got [%v]", err)
}
jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
err = jsonMessage.Display(data, true)
if err == nil || err.Error() != "Authentication is required." {
t.Fatalf("Expected an error [Authentication is required.], got [%v]", err)
}
}
func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
var (
inFd uintptr
)
data := bytes.NewBuffer([]byte{})
reader := strings.NewReader("This is not a 'valid' JSON []")
inFd, _ = term.GetFdInfo(reader)
if err := DisplayJSONMessagesStream(reader, data, inFd, false); err == nil && err.Error()[:17] != "invalid character" {
t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err)
}
}
func TestDisplayJSONMessagesStream(t *testing.T) {
var (
inFd uintptr
)
messages := map[string][]string{
// empty string
"": {
"",
""},
// Without progress & ID
"{ \"status\": \"status\" }": {
"status\n",
"status\n",
},
// Without progress, with ID
"{ \"id\": \"ID\",\"status\": \"status\" }": {
"ID: status\n",
fmt.Sprintf("ID: status\n%c[%dB", 27, 0),
},
// With progress
"{ \"id\": \"ID\", \"status\": \"status\", \"progress\": \"ProgressMessage\" }": {
"ID: status ProgressMessage",
fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 0, 27, 0),
},
// With progressDetail
"{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": {
"", // progressbar is disabled in non-terminal
fmt.Sprintf("\n%c[%dA%c[2K\rID: status 1 B\r%c[%dB", 27, 0, 27, 27, 0),
},
}
for jsonMessage, expectedMessages := range messages {
data := bytes.NewBuffer([]byte{})
reader := strings.NewReader(jsonMessage)
inFd, _ = term.GetFdInfo(reader)
// Without terminal
if err := DisplayJSONMessagesStream(reader, data, inFd, false); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[0] {
t.Fatalf("Expected an [%v], got [%v]", expectedMessages[0], data.String())
}
// With terminal
data = bytes.NewBuffer([]byte{})
reader = strings.NewReader(jsonMessage)
if err := DisplayJSONMessagesStream(reader, data, inFd, true); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[1] {
t.Fatalf("Expected an [%v], got [%v]", expectedMessages[1], data.String())
}
}
} }
...@@ -8,12 +8,25 @@ package mount ...@@ -8,12 +8,25 @@ package mount
import "C" import "C"
const ( const (
RDONLY = C.MNT_RDONLY // RDONLY will mount the filesystem as read-only.
NOSUID = C.MNT_NOSUID RDONLY = C.MNT_RDONLY
NOEXEC = C.MNT_NOEXEC
// NOSUID will not allow set-user-identifier or set-group-identifier bits to
// take effect.
NOSUID = C.MNT_NOSUID
// NOEXEC will not allow execution of any binaries on the mounted file system.
NOEXEC = C.MNT_NOEXEC
// SYNCHRONOUS will allow any I/O to the file system to be done synchronously.
SYNCHRONOUS = C.MNT_SYNCHRONOUS SYNCHRONOUS = C.MNT_SYNCHRONOUS
NOATIME = C.MNT_NOATIME
// NOATIME will not update the file access time when reading from a file.
NOATIME = C.MNT_NOATIME
)
// These flags are unsupported.
const (
BIND = 0 BIND = 0
DIRSYNC = 0 DIRSYNC = 0
MANDLOCK = 0 MANDLOCK = 0
......
...@@ -5,26 +5,81 @@ import ( ...@@ -5,26 +5,81 @@ import (
) )
const ( const (
RDONLY = syscall.MS_RDONLY // RDONLY will mount the file system read-only.
NOSUID = syscall.MS_NOSUID RDONLY = syscall.MS_RDONLY
NODEV = syscall.MS_NODEV
NOEXEC = syscall.MS_NOEXEC // NOSUID will not allow set-user-identifier or set-group-identifier bits to
// take effect.
NOSUID = syscall.MS_NOSUID
// NODEV will not interpret character or block special devices on the file
// system.
NODEV = syscall.MS_NODEV
// NOEXEC will not allow execution of any binaries on the mounted file system.
NOEXEC = syscall.MS_NOEXEC
// SYNCHRONOUS will allow I/O to the file system to be done synchronously.
SYNCHRONOUS = syscall.MS_SYNCHRONOUS SYNCHRONOUS = syscall.MS_SYNCHRONOUS
DIRSYNC = syscall.MS_DIRSYNC
REMOUNT = syscall.MS_REMOUNT // DIRSYNC will force all directory updates within the file system to be done
MANDLOCK = syscall.MS_MANDLOCK // synchronously. This affects the following system calls: creat, link,
NOATIME = syscall.MS_NOATIME // unlink, symlink, mkdir, rmdir, mknod and rename.
NODIRATIME = syscall.MS_NODIRATIME DIRSYNC = syscall.MS_DIRSYNC
BIND = syscall.MS_BIND
RBIND = syscall.MS_BIND | syscall.MS_REC // REMOUNT will attempt to remount an already-mounted file system. This is
UNBINDABLE = syscall.MS_UNBINDABLE // commonly used to change the mount flags for a file system, especially to
// make a readonly file system writeable. It does not change device or mount
// point.
REMOUNT = syscall.MS_REMOUNT
// MANDLOCK will force mandatory locks on a filesystem.
MANDLOCK = syscall.MS_MANDLOCK
// NOATIME will not update the file access time when reading from a file.
NOATIME = syscall.MS_NOATIME
// NODIRATIME will not update the directory access time.
NODIRATIME = syscall.MS_NODIRATIME
// BIND remounts a subtree somewhere else.
BIND = syscall.MS_BIND
// RBIND remounts a subtree and all possible submounts somewhere else.
RBIND = syscall.MS_BIND | syscall.MS_REC
// UNBINDABLE creates a mount which cannot be cloned through a bind operation.
UNBINDABLE = syscall.MS_UNBINDABLE
// RUNBINDABLE marks the entire mount tree as UNBINDABLE.
RUNBINDABLE = syscall.MS_UNBINDABLE | syscall.MS_REC RUNBINDABLE = syscall.MS_UNBINDABLE | syscall.MS_REC
PRIVATE = syscall.MS_PRIVATE
RPRIVATE = syscall.MS_PRIVATE | syscall.MS_REC // PRIVATE creates a mount which carries no propagation abilities.
SLAVE = syscall.MS_SLAVE PRIVATE = syscall.MS_PRIVATE
RSLAVE = syscall.MS_SLAVE | syscall.MS_REC
SHARED = syscall.MS_SHARED // RPRIVATE marks the entire mount tree as PRIVATE.
RSHARED = syscall.MS_SHARED | syscall.MS_REC RPRIVATE = syscall.MS_PRIVATE | syscall.MS_REC
RELATIME = syscall.MS_RELATIME
// SLAVE creates a mount which receives propagation from its master, but not
// vice versa.
SLAVE = syscall.MS_SLAVE
// RSLAVE marks the entire mount tree as SLAVE.
RSLAVE = syscall.MS_SLAVE | syscall.MS_REC
// SHARED creates a mount which provides the ability to create mirrors of
// that mount such that mounts and unmounts within any of the mirrors
// propagate to the other mirrors.
SHARED = syscall.MS_SHARED
// RSHARED marks the entire mount tree as SHARED.
RSHARED = syscall.MS_SHARED | syscall.MS_REC
// RELATIME updates inode access times relative to modify or change time.
RELATIME = syscall.MS_RELATIME
// STRICTATIME allows to explicitly request full atime updates. This makes
// it possible for the kernel to default to relatime or noatime but still
// allow userspace to override it.
STRICTATIME = syscall.MS_STRICTATIME STRICTATIME = syscall.MS_STRICTATIME
) )
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
package mount package mount
// These flags are unsupported.
const ( const (
BIND = 0 BIND = 0
DIRSYNC = 0 DIRSYNC = 0
......
...@@ -4,11 +4,12 @@ import ( ...@@ -4,11 +4,12 @@ import (
"time" "time"
) )
// GetMounts retrieves a list of mounts for the current running process.
func GetMounts() ([]*MountInfo, error) { func GetMounts() ([]*MountInfo, error) {
return parseMountTable() return parseMountTable()
} }
// Looks at /proc/self/mountinfo to determine of the specified // Mounted looks at /proc/self/mountinfo to determine of the specified
// mountpoint has been mounted // mountpoint has been mounted
func Mounted(mountpoint string) (bool, error) { func Mounted(mountpoint string) (bool, error) {
entries, err := parseMountTable() entries, err := parseMountTable()
...@@ -25,9 +26,10 @@ func Mounted(mountpoint string) (bool, error) { ...@@ -25,9 +26,10 @@ func Mounted(mountpoint string) (bool, error) {
return false, nil return false, nil
} }
// Mount the specified options at the target path only if // Mount will mount filesystem according to the specified configuration, on the
// the target is not mounted // condition that the target path is *not* already mounted. Options must be
// Options must be specified as fstab style // specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See
// flags.go for supported option flags.
func Mount(device, target, mType, options string) error { func Mount(device, target, mType, options string) error {
flag, _ := parseOptions(options) flag, _ := parseOptions(options)
if flag&REMOUNT != REMOUNT { if flag&REMOUNT != REMOUNT {
...@@ -38,9 +40,10 @@ func Mount(device, target, mType, options string) error { ...@@ -38,9 +40,10 @@ func Mount(device, target, mType, options string) error {
return ForceMount(device, target, mType, options) return ForceMount(device, target, mType, options)
} }
// Mount the specified options at the target path // ForceMount will mount a filesystem according to the specified configuration,
// reguardless if the target is mounted or not // *regardless* if the target path is not already mounted. Options must be
// Options must be specified as fstab style // specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See
// flags.go for supported option flags.
func ForceMount(device, target, mType, options string) error { func ForceMount(device, target, mType, options string) error {
flag, data := parseOptions(options) flag, data := parseOptions(options)
if err := mount(device, target, mType, uintptr(flag), data); err != nil { if err := mount(device, target, mType, uintptr(flag), data); err != nil {
...@@ -49,7 +52,7 @@ func ForceMount(device, target, mType, options string) error { ...@@ -49,7 +52,7 @@ func ForceMount(device, target, mType, options string) error {
return nil return nil
} }
// Unmount the target only if it is mounted // Unmount will unmount the target filesystem, so long as it is mounted.
func Unmount(target string) error { func Unmount(target string) error {
if mounted, err := Mounted(target); err != nil || !mounted { if mounted, err := Mounted(target); err != nil || !mounted {
return err return err
...@@ -57,7 +60,8 @@ func Unmount(target string) error { ...@@ -57,7 +60,8 @@ func Unmount(target string) error {
return ForceUnmount(target) return ForceUnmount(target)
} }
// Unmount the target reguardless if it is mounted or not // ForceUnmount will force an unmount of the target filesystem, regardless if
// it is mounted or not.
func ForceUnmount(target string) (err error) { func ForceUnmount(target string) (err error) {
// Simple retry logic for unmount // Simple retry logic for unmount
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
......
package mount package mount
// MountInfo reveals information about a particular mounted filesystem. This
// struct is populated from the content in the /proc/<pid>/mountinfo file.
type MountInfo struct { type MountInfo struct {
Id, Parent, Major, Minor int // Id is a unique identifier of the mount (may be reused after umount).
Root, Mountpoint, Opts, Optional string Id int
Fstype, Source, VfsOpts string
// Parent indicates the ID of the mount parent (or of self for the top of the
// mount tree).
Parent int
// Major indicates one half of the device ID which identifies the device class.
Major int
// Minor indicates one half of the device ID which identifies a specific
// instance of device.
Minor int
// Root of the mount within the filesystem.
Root string
// Mountpoint indicates the mount point relative to the process's root.
Mountpoint string
// Opts represents mount-specific options.
Opts string
// Optional represents optional fields.
Optional string
// Fstype indicates the type of filesystem, such as EXT3.
Fstype string
// Source indicates filesystem specific information or "none".
Source string
// VfsOpts represents per super block options.
VfsOpts string
} }
...@@ -13,7 +13,8 @@ import ( ...@@ -13,7 +13,8 @@ import (
"unsafe" "unsafe"
) )
// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts // Parse /proc/self/mountinfo because comparing Dev and ino does not work from
// bind mounts.
func parseMountTable() ([]*MountInfo, error) { func parseMountTable() ([]*MountInfo, error) {
var rawEntries *C.struct_statfs var rawEntries *C.struct_statfs
......
...@@ -28,7 +28,8 @@ const ( ...@@ -28,7 +28,8 @@ const (
mountinfoFormat = "%d %d %d:%d %s %s %s %s" mountinfoFormat = "%d %d %d:%d %s %s %s %s"
) )
// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts // Parse /proc/self/mountinfo because comparing Dev and ino does not work from
// bind mounts
func parseMountTable() ([]*MountInfo, error) { func parseMountTable() ([]*MountInfo, error) {
f, err := os.Open("/proc/self/mountinfo") f, err := os.Open("/proc/self/mountinfo")
if err != nil { if err != nil {
...@@ -80,7 +81,9 @@ func parseInfoFile(r io.Reader) ([]*MountInfo, error) { ...@@ -80,7 +81,9 @@ func parseInfoFile(r io.Reader) ([]*MountInfo, error) {
return out, nil return out, nil
} }
// PidMountInfo collects the mounts for a specific Pid // PidMountInfo collects the mounts for a specific process ID. If the process
// ID is unknown, it is better to use `GetMounts` which will inspect
// "/proc/self/mountinfo" instead.
func PidMountInfo(pid int) ([]*MountInfo, error) { func PidMountInfo(pid int) ([]*MountInfo, error) {
f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid)) f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid))
if err != nil { if err != nil {
......
...@@ -2,34 +2,50 @@ ...@@ -2,34 +2,50 @@
package mount package mount
// MakeShared ensures a mounted filesystem has the SHARED mount option enabled.
// See the supported options in flags.go for further reference.
func MakeShared(mountPoint string) error { func MakeShared(mountPoint string) error {
return ensureMountedAs(mountPoint, "shared") return ensureMountedAs(mountPoint, "shared")
} }
// MakeRShared ensures a mounted filesystem has the RSHARED mount option enabled.
// See the supported options in flags.go for further reference.
func MakeRShared(mountPoint string) error { func MakeRShared(mountPoint string) error {
return ensureMountedAs(mountPoint, "rshared") return ensureMountedAs(mountPoint, "rshared")
} }
// MakePrivate ensures a mounted filesystem has the PRIVATE mount option enabled.
// See the supported options in flags.go for further reference.
func MakePrivate(mountPoint string) error { func MakePrivate(mountPoint string) error {
return ensureMountedAs(mountPoint, "private") return ensureMountedAs(mountPoint, "private")
} }
// MakeRPrivate ensures a mounted filesystem has the RPRIVATE mount option
// enabled. See the supported options in flags.go for further reference.
func MakeRPrivate(mountPoint string) error { func MakeRPrivate(mountPoint string) error {
return ensureMountedAs(mountPoint, "rprivate") return ensureMountedAs(mountPoint, "rprivate")
} }
// MakeSlave ensures a mounted filesystem has the SLAVE mount option enabled.
// See the supported options in flags.go for further reference.
func MakeSlave(mountPoint string) error { func MakeSlave(mountPoint string) error {
return ensureMountedAs(mountPoint, "slave") return ensureMountedAs(mountPoint, "slave")
} }
// MakeRSlave ensures a mounted filesystem has the RSLAVE mount option enabled.
// See the supported options in flags.go for further reference.
func MakeRSlave(mountPoint string) error { func MakeRSlave(mountPoint string) error {
return ensureMountedAs(mountPoint, "rslave") return ensureMountedAs(mountPoint, "rslave")
} }
// MakeUnbindable ensures a mounted filesystem has the UNBINDABLE mount option
// enabled. See the supported options in flags.go for further reference.
func MakeUnbindable(mountPoint string) error { func MakeUnbindable(mountPoint string) error {
return ensureMountedAs(mountPoint, "unbindable") return ensureMountedAs(mountPoint, "unbindable")
} }
// MakeRUnbindable ensures a mounted filesystem has the RUNBINDABLE mount
// option enabled. See the supported options in flags.go for further reference.
func MakeRUnbindable(mountPoint string) error { func MakeRUnbindable(mountPoint string) error {
return ensureMountedAs(mountPoint, "runbindable") return ensureMountedAs(mountPoint, "runbindable")
} }
......
package filters
import (
"encoding/json"
"errors"
"regexp"
"strings"
)
type Args map[string][]string
// Parse the argument to the filter flag. Like
//
// `docker ps -f 'created=today' -f 'image.name=ubuntu*'`
//
// If prev map is provided, then it is appended to, and returned. By default a new
// map is created.
func ParseFlag(arg string, prev Args) (Args, error) {
var filters Args = prev
if prev == nil {
filters = Args{}
}
if len(arg) == 0 {
return filters, nil
}
if !strings.Contains(arg, "=") {
return filters, ErrorBadFormat
}
f := strings.SplitN(arg, "=", 2)
name := strings.ToLower(strings.TrimSpace(f[0]))
value := strings.TrimSpace(f[1])
filters[name] = append(filters[name], value)
return filters, nil
}
var ErrorBadFormat = errors.New("bad format of filter (expected name=value)")
// packs the Args into an string for easy transport from client to server
func ToParam(a Args) (string, error) {
// this way we don't URL encode {}, just empty space
if len(a) == 0 {
return "", nil
}
buf, err := json.Marshal(a)
if err != nil {
return "", err
}
return string(buf), nil
}
// unpacks the filter Args
func FromParam(p string) (Args, error) {
args := Args{}
if len(p) == 0 {
return args, nil
}
if err := json.NewDecoder(strings.NewReader(p)).Decode(&args); err != nil {
return nil, err
}
return args, nil
}
func (filters Args) MatchKVList(field string, sources map[string]string) bool {
fieldValues := filters[field]
//do not filter if there is no filter set or cannot determine filter
if len(fieldValues) == 0 {
return true
}
if sources == nil || len(sources) == 0 {
return false
}
outer:
for _, name2match := range fieldValues {
testKV := strings.SplitN(name2match, "=", 2)
for k, v := range sources {
if len(testKV) == 1 {
if k == testKV[0] {
continue outer
}
} else if k == testKV[0] && v == testKV[1] {
continue outer
}
}
return false
}
return true
}
func (filters Args) Match(field, source string) bool {
fieldValues := filters[field]
//do not filter if there is no filter set or cannot determine filter
if len(fieldValues) == 0 {
return true
}
for _, name2match := range fieldValues {
match, err := regexp.MatchString(name2match, source)
if err != nil {
continue
}
if match {
return true
}
}
return false
}
package filters
import (
"sort"
"testing"
)
func TestParseArgs(t *testing.T) {
// equivalent of `docker ps -f 'created=today' -f 'image.name=ubuntu*' -f 'image.name=*untu'`
flagArgs := []string{
"created=today",
"image.name=ubuntu*",
"image.name=*untu",
}
var (
args = Args{}
err error
)
for i := range flagArgs {
args, err = ParseFlag(flagArgs[i], args)
if err != nil {
t.Errorf("failed to parse %s: %s", flagArgs[i], err)
}
}
if len(args["created"]) != 1 {
t.Errorf("failed to set this arg")
}
if len(args["image.name"]) != 2 {
t.Errorf("the args should have collapsed")
}
}
func TestParam(t *testing.T) {
a := Args{
"created": []string{"today"},
"image.name": []string{"ubuntu*", "*untu"},
}
v, err := ToParam(a)
if err != nil {
t.Errorf("failed to marshal the filters: %s", err)
}
v1, err := FromParam(v)
if err != nil {
t.Errorf("%s", err)
}
for key, vals := range v1 {
if _, ok := a[key]; !ok {
t.Errorf("could not find key %s in original set", key)
}
sort.Strings(vals)
sort.Strings(a[key])
if len(vals) != len(a[key]) {
t.Errorf("value lengths ought to match")
continue
}
for i := range vals {
if vals[i] != a[key][i] {
t.Errorf("expected %s, but got %s", a[key][i], vals[i])
}
}
}
}
func TestEmpty(t *testing.T) {
a := Args{}
v, err := ToParam(a)
if err != nil {
t.Errorf("failed to marshal the filters: %s", err)
}
v1, err := FromParam(v)
if err != nil {
t.Errorf("%s", err)
}
if len(a) != len(v1) {
t.Errorf("these should both be empty sets")
}
}
// +build !windows
package kernel
import (
"bytes"
"errors"
"fmt"
)
type KernelVersionInfo struct {
Kernel int
Major int
Minor int
Flavor string
}
func (k *KernelVersionInfo) String() string {
return fmt.Sprintf("%d.%d.%d%s", k.Kernel, k.Major, k.Minor, k.Flavor)
}
// Compare two KernelVersionInfo struct.
// Returns -1 if a < b, 0 if a == b, 1 it a > b
func CompareKernelVersion(a, b *KernelVersionInfo) int {
if a.Kernel < b.Kernel {
return -1
} else if a.Kernel > b.Kernel {
return 1
}
if a.Major < b.Major {
return -1
} else if a.Major > b.Major {
return 1
}
if a.Minor < b.Minor {
return -1
} else if a.Minor > b.Minor {
return 1
}
return 0
}
func GetKernelVersion() (*KernelVersionInfo, error) {
var (
err error
)
uts, err := uname()
if err != nil {
return nil, err
}
release := make([]byte, len(uts.Release))
i := 0
for _, c := range uts.Release {
release[i] = byte(c)
i++
}
// Remove the \x00 from the release for Atoi to parse correctly
release = release[:bytes.IndexByte(release, 0)]
return ParseRelease(string(release))
}
func ParseRelease(release string) (*KernelVersionInfo, error) {
var (
kernel, major, minor, parsed int
flavor, partial string
)
// Ignore error from Sscanf to allow an empty flavor. Instead, just
// make sure we got all the version numbers.
parsed, _ = fmt.Sscanf(release, "%d.%d%s", &kernel, &major, &partial)
if parsed < 2 {
return nil, errors.New("Can't parse kernel version " + release)
}
// sometimes we have 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64
parsed, _ = fmt.Sscanf(partial, ".%d%s", &minor, &flavor)
if parsed < 1 {
flavor = partial
}
return &KernelVersionInfo{
Kernel: kernel,
Major: major,
Minor: minor,
Flavor: flavor,
}, nil
}
package kernel
import (
"testing"
)
func assertParseRelease(t *testing.T, release string, b *KernelVersionInfo, result int) {
var (
a *KernelVersionInfo
)
a, _ = ParseRelease(release)
if r := CompareKernelVersion(a, b); r != result {
t.Fatalf("Unexpected kernel version comparison result. Found %d, expected %d", r, result)
}
if a.Flavor != b.Flavor {
t.Fatalf("Unexpected parsed kernel flavor. Found %s, expected %s", a.Flavor, b.Flavor)
}
}
func TestParseRelease(t *testing.T) {
assertParseRelease(t, "3.8.0", &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0}, 0)
assertParseRelease(t, "3.4.54.longterm-1", &KernelVersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: ".longterm-1"}, 0)
assertParseRelease(t, "3.4.54.longterm-1", &KernelVersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: ".longterm-1"}, 0)
assertParseRelease(t, "3.8.0-19-generic", &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "-19-generic"}, 0)
assertParseRelease(t, "3.12.8tag", &KernelVersionInfo{Kernel: 3, Major: 12, Minor: 8, Flavor: "tag"}, 0)
assertParseRelease(t, "3.12-1-amd64", &KernelVersionInfo{Kernel: 3, Major: 12, Minor: 0, Flavor: "-1-amd64"}, 0)
}
func assertKernelVersion(t *testing.T, a, b *KernelVersionInfo, result int) {
if r := CompareKernelVersion(a, b); r != result {
t.Fatalf("Unexpected kernel version comparison result. Found %d, expected %d", r, result)
}
}
func TestCompareKernelVersion(t *testing.T) {
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
0)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 2, Major: 6, Minor: 0},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
-1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
&KernelVersionInfo{Kernel: 2, Major: 6, Minor: 0},
1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
0)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 5},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 0, Minor: 20},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
-1)
}
package kernel
import (
"fmt"
"syscall"
"unsafe"
)
type KernelVersionInfo struct {
kvi string
major int
minor int
build int
}
func (k *KernelVersionInfo) String() string {
return fmt.Sprintf("%d.%d %d (%s)", k.major, k.minor, k.build, k.kvi)
}
func GetKernelVersion() (*KernelVersionInfo, error) {
var (
h syscall.Handle
dwVersion uint32
err error
)
KVI := &KernelVersionInfo{"Unknown", 0, 0, 0}
if err = syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE,
syscall.StringToUTF16Ptr(`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\`),
0,
syscall.KEY_READ,
&h); err != nil {
return KVI, err
}
defer syscall.RegCloseKey(h)
var buf [1 << 10]uint16
var typ uint32
n := uint32(len(buf) * 2) // api expects array of bytes, not uint16
if err = syscall.RegQueryValueEx(h,
syscall.StringToUTF16Ptr("BuildLabEx"),
nil,
&typ,
(*byte)(unsafe.Pointer(&buf[0])),
&n); err != nil {
return KVI, err
}
KVI.kvi = syscall.UTF16ToString(buf[:])
// Important - docker.exe MUST be manifested for this API to return
// the correct information.
if dwVersion, err = syscall.GetVersion(); err != nil {
return KVI, err
}
KVI.major = int(dwVersion & 0xFF)
KVI.minor = int((dwVersion & 0XFF00) >> 8)
KVI.build = int((dwVersion & 0xFFFF0000) >> 16)
return KVI, nil
}
package kernel
import (
"syscall"
)
type Utsname syscall.Utsname
func uname() (*syscall.Utsname, error) {
uts := &syscall.Utsname{}
if err := syscall.Uname(uts); err != nil {
return nil, err
}
return uts, nil
}
// +build !linux
package kernel
import (
"errors"
)
type Utsname struct {
Release [65]byte
}
func uname() (*Utsname, error) {
return nil, errors.New("Kernel version detection is available only on linux")
}
package operatingsystem
import (
"bytes"
"errors"
"io/ioutil"
)
var (
// file to use to detect if the daemon is running in a container
proc1Cgroup = "/proc/1/cgroup"
// file to check to determine Operating System
etcOsRelease = "/etc/os-release"
)
func GetOperatingSystem() (string, error) {
b, err := ioutil.ReadFile(etcOsRelease)
if err != nil {
return "", err
}
if i := bytes.Index(b, []byte("PRETTY_NAME")); i >= 0 {
b = b[i+13:]
return string(b[:bytes.IndexByte(b, '"')]), nil
}
return "", errors.New("PRETTY_NAME not found")
}
func IsContainerized() (bool, error) {
b, err := ioutil.ReadFile(proc1Cgroup)
if err != nil {
return false, err
}
for _, line := range bytes.Split(b, []byte{'\n'}) {
if len(line) > 0 && !bytes.HasSuffix(line, []byte{'/'}) {
return true, nil
}
}
return false, nil
}
package operatingsystem
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestGetOperatingSystem(t *testing.T) {
var (
backup = etcOsRelease
ubuntuTrusty = []byte(`NAME="Ubuntu"
VERSION="14.04, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`)
gentoo = []byte(`NAME=Gentoo
ID=gentoo
PRETTY_NAME="Gentoo/Linux"
ANSI_COLOR="1;32"
HOME_URL="http://www.gentoo.org/"
SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
BUG_REPORT_URL="https://bugs.gentoo.org/"
`)
noPrettyName = []byte(`NAME="Ubuntu"
VERSION="14.04, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`)
)
dir := os.TempDir()
etcOsRelease = filepath.Join(dir, "etcOsRelease")
defer func() {
os.Remove(etcOsRelease)
etcOsRelease = backup
}()
for expect, osRelease := range map[string][]byte{
"Ubuntu 14.04 LTS": ubuntuTrusty,
"Gentoo/Linux": gentoo,
"": noPrettyName,
} {
if err := ioutil.WriteFile(etcOsRelease, osRelease, 0600); err != nil {
t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
}
s, err := GetOperatingSystem()
if s != expect {
if expect == "" {
t.Fatalf("Expected error 'PRETTY_NAME not found', but got %v", err)
} else {
t.Fatalf("Expected '%s', but got '%s'. Err=%v", expect, s, err)
}
}
}
}
func TestIsContainerized(t *testing.T) {
var (
backup = proc1Cgroup
nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/
13:hugetlb:/
12:net_prio:/
11:perf_event:/
10:bfqio:/
9:blkio:/
8:net_cls:/
7:freezer:/
6:devices:/
5:memory:/
4:cpuacct:/
3:cpu:/
2:cpuset:/
`)
containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
7:net_cls:/
6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
1:cpuset:/`)
)
dir := os.TempDir()
proc1Cgroup = filepath.Join(dir, "proc1Cgroup")
defer func() {
os.Remove(proc1Cgroup)
proc1Cgroup = backup
}()
if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil {
t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
}
inContainer, err := IsContainerized()
if err != nil {
t.Fatal(err)
}
if inContainer {
t.Fatal("Wrongly assuming containerized")
}
if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil {
t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
}
inContainer, err = IsContainerized()
if err != nil {
t.Fatal(err)
}
if !inContainer {
t.Fatal("Wrongly assuming non-containerized")
}
}
package operatingsystem
import (
"syscall"
"unsafe"
)
// See https://code.google.com/p/go/source/browse/src/pkg/mime/type_windows.go?r=d14520ac25bf6940785aabb71f5be453a286f58c
// for a similar sample
func GetOperatingSystem() (string, error) {
var h syscall.Handle
// Default return value
ret := "Unknown Operating System"
if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE,
syscall.StringToUTF16Ptr(`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\`),
0,
syscall.KEY_READ,
&h); err != nil {
return ret, err
}
defer syscall.RegCloseKey(h)
var buf [1 << 10]uint16
var typ uint32
n := uint32(len(buf) * 2) // api expects array of bytes, not uint16
if err := syscall.RegQueryValueEx(h,
syscall.StringToUTF16Ptr("ProductName"),
nil,
&typ,
(*byte)(unsafe.Pointer(&buf[0])),
&n); err != nil {
return ret, err
}
ret = syscall.UTF16ToString(buf[:])
return ret, nil
}
// No-op on Windows
func IsContainerized() (bool, error) {
return false, nil
}
...@@ -2,6 +2,7 @@ package parsers ...@@ -2,6 +2,7 @@ package parsers
import ( import (
"fmt" "fmt"
"runtime"
"strconv" "strconv"
"strings" "strings"
) )
...@@ -10,7 +11,12 @@ import ( ...@@ -10,7 +11,12 @@ import (
func ParseHost(defaultTCPAddr, defaultUnixAddr, addr string) (string, error) { func ParseHost(defaultTCPAddr, defaultUnixAddr, addr string) (string, error) {
addr = strings.TrimSpace(addr) addr = strings.TrimSpace(addr)
if addr == "" { if addr == "" {
addr = fmt.Sprintf("unix://%s", defaultUnixAddr) if runtime.GOOS != "windows" {
addr = fmt.Sprintf("unix://%s", defaultUnixAddr)
} else {
// Note - defaultTCPAddr already includes tcp:// prefix
addr = fmt.Sprintf("%s", defaultTCPAddr)
}
} }
addrParts := strings.Split(addr, "://") addrParts := strings.Split(addr, "://")
if len(addrParts) == 1 { if len(addrParts) == 1 {
...@@ -135,3 +141,17 @@ func ParsePortRange(ports string) (uint64, uint64, error) { ...@@ -135,3 +141,17 @@ func ParsePortRange(ports string) (uint64, uint64, error) {
} }
return start, end, nil return start, end, nil
} }
func ParseLink(val string) (string, string, error) {
if val == "" {
return "", "", fmt.Errorf("empty string specified for links")
}
arr := strings.Split(val, ":")
if len(arr) > 2 {
return "", "", fmt.Errorf("bad format for links: %s", val)
}
if len(arr) == 1 {
return val, val, nil
}
return arr[0], arr[1], nil
}
package parsers
import (
"strings"
"testing"
)
func TestParseHost(t *testing.T) {
var (
defaultHttpHost = "127.0.0.1"
defaultUnix = "/var/run/docker.sock"
)
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "0.0.0.0"); err == nil {
t.Errorf("tcp 0.0.0.0 address expected error return, but err == nil, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "tcp://"); err == nil {
t.Errorf("default tcp:// address expected error return, but err == nil, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "0.0.0.1:5555"); err != nil || addr != "tcp://0.0.0.1:5555" {
t.Errorf("0.0.0.1:5555 -> expected tcp://0.0.0.1:5555, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, ":6666"); err != nil || addr != "tcp://127.0.0.1:6666" {
t.Errorf(":6666 -> expected tcp://127.0.0.1:6666, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "tcp://:7777"); err != nil || addr != "tcp://127.0.0.1:7777" {
t.Errorf("tcp://:7777 -> expected tcp://127.0.0.1:7777, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, ""); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("empty argument -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "unix:///var/run/docker.sock"); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "unix://"); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "udp://127.0.0.1"); err == nil {
t.Errorf("udp protocol address expected error return, but err == nil. Got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultUnix, "udp://127.0.0.1:2375"); err == nil {
t.Errorf("udp protocol address expected error return, but err == nil. Got %s", addr)
}
}
func TestParseRepositoryTag(t *testing.T) {
if repo, tag := ParseRepositoryTag("root"); repo != "root" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("root:tag"); repo != "root" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "tag", repo, tag)
}
if repo, digest := ParseRepositoryTag("root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "root" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "root", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
}
if repo, tag := ParseRepositoryTag("user/repo"); repo != "user/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("user/repo:tag"); repo != "user/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "tag", repo, tag)
}
if repo, digest := ParseRepositoryTag("user/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "user/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "user/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
}
if repo, tag := ParseRepositoryTag("url:5000/repo"); repo != "url:5000/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("url:5000/repo:tag"); repo != "url:5000/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "tag", repo, tag)
}
if repo, digest := ParseRepositoryTag("url:5000/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "url:5000/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "url:5000/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
}
}
func TestParsePortMapping(t *testing.T) {
data, err := PartParser("ip:public:private", "192.168.1.1:80:8080")
if err != nil {
t.Fatal(err)
}
if len(data) != 3 {
t.FailNow()
}
if data["ip"] != "192.168.1.1" {
t.Fail()
}
if data["public"] != "80" {
t.Fail()
}
if data["private"] != "8080" {
t.Fail()
}
}
func TestParsePortRange(t *testing.T) {
if start, end, err := ParsePortRange("8000-8080"); err != nil || start != 8000 || end != 8080 {
t.Fatalf("Error: %s or Expecting {start,end} values {8000,8080} but found {%d,%d}.", err, start, end)
}
}
func TestParsePortRangeIncorrectRange(t *testing.T) {
if _, _, err := ParsePortRange("9000-8080"); err == nil || !strings.Contains(err.Error(), "Invalid range specified for the Port") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}
func TestParsePortRangeIncorrectEndRange(t *testing.T) {
if _, _, err := ParsePortRange("8000-a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
if _, _, err := ParsePortRange("8000-30a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}
func TestParsePortRangeIncorrectStartRange(t *testing.T) {
if _, _, err := ParsePortRange("a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
if _, _, err := ParsePortRange("30a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}
func TestParseLink(t *testing.T) {
name, alias, err := ParseLink("name:alias")
if err != nil {
t.Fatalf("Expected not to error out on a valid name:alias format but got: %v", err)
}
if name != "name" {
t.Fatalf("Link name should have been name, got %s instead", name)
}
if alias != "alias" {
t.Fatalf("Link alias should have been alias, got %s instead", alias)
}
// short format definition
name, alias, err = ParseLink("name")
if err != nil {
t.Fatalf("Expected not to error out on a valid name only format but got: %v", err)
}
if name != "name" {
t.Fatalf("Link name should have been name, got %s instead", name)
}
if alias != "name" {
t.Fatalf("Link alias should have been name, got %s instead", alias)
}
// empty string link definition is not allowed
if _, _, err := ParseLink(""); err == nil || !strings.Contains(err.Error(), "empty string specified for links") {
t.Fatalf("Expected error 'empty string specified for links' but got: %v", err)
}
// more than two colons are not allowed
if _, _, err := ParseLink("link:alias:wrong"); err == nil || !strings.Contains(err.Error(), "bad format for links: link:alias:wrong") {
t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err)
}
}
// +build windows
package term
import (
"syscall"
"unsafe"
)
const (
// Consts for Get/SetConsoleMode function
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
ENABLE_ECHO_INPUT = 0x0004
ENABLE_INSERT_MODE = 0x0020
ENABLE_LINE_INPUT = 0x0002
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_WINDOW_INPUT = 0x0008
// If parameter is a screen buffer handle, additional values
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
)
var kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
var (
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
)
func GetConsoleMode(fileDesc uintptr) (uint32, error) {
var mode uint32
err := syscall.GetConsoleMode(syscall.Handle(fileDesc), &mode)
return mode, err
}
func SetConsoleMode(fileDesc uintptr, mode uint32) error {
r, _, err := setConsoleModeProc.Call(fileDesc, uintptr(mode), 0)
if r == 0 {
if err != nil {
return err
}
return syscall.EINVAL
}
return nil
}
// types for calling GetConsoleScreenBufferInfo
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx
type (
SHORT int16
SMALL_RECT struct {
Left SHORT
Top SHORT
Right SHORT
Bottom SHORT
}
COORD struct {
X SHORT
Y SHORT
}
WORD uint16
CONSOLE_SCREEN_BUFFER_INFO struct {
dwSize COORD
dwCursorPosition COORD
wAttributes WORD
srWindow SMALL_RECT
dwMaximumWindowSize COORD
}
)
func GetConsoleScreenBufferInfo(fileDesc uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
var info CONSOLE_SCREEN_BUFFER_INFO
r, _, err := getConsoleScreenBufferInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&info)), 0)
if r == 0 {
if err != nil {
return nil, err
}
return nil, syscall.EINVAL
}
return &info, nil
}
...@@ -24,6 +24,7 @@ func MakeRaw(fd uintptr) (*State, error) { ...@@ -24,6 +24,7 @@ func MakeRaw(fd uintptr) (*State, error) {
newState := oldState.termios newState := oldState.termios
C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&newState))) C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&newState)))
newState.Oflag = newState.Oflag | C.OPOST
if err := tcset(fd, &newState); err != 0 { if err := tcset(fd, &newState); err != 0 {
return nil, err return nil, err
} }
......
...@@ -4,6 +4,7 @@ package term ...@@ -4,6 +4,7 @@ package term
import ( import (
"errors" "errors"
"io"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
...@@ -25,6 +26,20 @@ type Winsize struct { ...@@ -25,6 +26,20 @@ type Winsize struct {
y uint16 y uint16
} }
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
return os.Stdin, os.Stdout, os.Stderr
}
func GetFdInfo(in interface{}) (uintptr, bool) {
var inFd uintptr
var isTerminalIn bool
if file, ok := in.(*os.File); ok {
inFd = file.Fd()
isTerminalIn = IsTerminal(inFd)
}
return inFd, isTerminalIn
}
func GetWinsize(fd uintptr) (*Winsize, error) { func GetWinsize(fd uintptr) (*Winsize, error) {
ws := &Winsize{} ws := &Winsize{}
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws))) _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
......
// +build windows // +build windows
package term package term
import (
"io"
"os"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/term/winconsole"
)
// State holds the console mode for the terminal.
type State struct { type State struct {
mode uint32 mode uint32
} }
// Winsize is used for window size.
type Winsize struct { type Winsize struct {
Height uint16 Height uint16
Width uint16 Width uint16
...@@ -13,75 +22,115 @@ type Winsize struct { ...@@ -13,75 +22,115 @@ type Winsize struct {
y uint16 y uint16
} }
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
switch {
case os.Getenv("ConEmuANSI") == "ON":
// The ConEmu shell emulates ANSI well by default.
return os.Stdin, os.Stdout, os.Stderr
case os.Getenv("MSYSTEM") != "":
// MSYS (mingw) does not emulate ANSI well.
return winconsole.WinConsoleStreams()
default:
return winconsole.WinConsoleStreams()
}
}
// GetFdInfo returns file descriptor and bool indicating whether the file is a terminal.
func GetFdInfo(in interface{}) (uintptr, bool) {
return winconsole.GetHandleInfo(in)
}
// GetWinsize retrieves the window size of the terminal connected to the passed file descriptor.
func GetWinsize(fd uintptr) (*Winsize, error) { func GetWinsize(fd uintptr) (*Winsize, error) {
ws := &Winsize{} info, err := winconsole.GetConsoleScreenBufferInfo(fd)
var info *CONSOLE_SCREEN_BUFFER_INFO
info, err := GetConsoleScreenBufferInfo(fd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ws.Height = uint16(info.srWindow.Right - info.srWindow.Left + 1)
ws.Width = uint16(info.srWindow.Bottom - info.srWindow.Top + 1)
ws.x = 0 // todo azlinux -- this is the pixel size of the Window, and not currently used by any caller
ws.y = 0
return ws, nil // TODO(azlinux): Set the pixel width / height of the console (currently unused by any caller)
return &Winsize{
Width: uint16(info.Window.Right - info.Window.Left + 1),
Height: uint16(info.Window.Bottom - info.Window.Top + 1),
x: 0,
y: 0}, nil
} }
// SetWinsize sets the size of the given terminal connected to the passed file descriptor.
func SetWinsize(fd uintptr, ws *Winsize) error { func SetWinsize(fd uintptr, ws *Winsize) error {
// TODO(azlinux): Implement SetWinsize
logrus.Debugf("[windows] SetWinsize: WARNING -- Unsupported method invoked")
return nil return nil
} }
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd uintptr) bool { func IsTerminal(fd uintptr) bool {
_, e := GetConsoleMode(fd) return winconsole.IsConsole(fd)
return e == nil
} }
// Restore restores the terminal connected to the given file descriptor to a // RestoreTerminal restores the terminal connected to the given file descriptor to a
// previous state. // previous state.
func RestoreTerminal(fd uintptr, state *State) error { func RestoreTerminal(fd uintptr, state *State) error {
return SetConsoleMode(fd, state.mode) return winconsole.SetConsoleMode(fd, state.mode)
} }
// SaveState saves the state of the terminal connected to the given file descriptor.
func SaveState(fd uintptr) (*State, error) { func SaveState(fd uintptr) (*State, error) {
mode, e := GetConsoleMode(fd) mode, e := winconsole.GetConsoleMode(fd)
if e != nil { if e != nil {
return nil, e return nil, e
} }
return &State{mode}, nil return &State{mode}, nil
} }
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings // DisableEcho disables echo for the terminal connected to the given file descriptor.
// -- See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
func DisableEcho(fd uintptr, state *State) error { func DisableEcho(fd uintptr, state *State) error {
state.mode &^= (ENABLE_ECHO_INPUT) mode := state.mode
state.mode |= (ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT) mode &^= winconsole.ENABLE_ECHO_INPUT
return SetConsoleMode(fd, state.mode) mode |= winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT
// TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state.
return winconsole.SetConsoleMode(fd, mode)
} }
// SetRawTerminal puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func SetRawTerminal(fd uintptr) (*State, error) { func SetRawTerminal(fd uintptr) (*State, error) {
oldState, err := MakeRaw(fd) state, err := MakeRaw(fd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO (azlinux): implement handling interrupt and restore state of terminal // TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state.
return oldState, err return state, err
} }
// MakeRaw puts the terminal connected to the given file descriptor into raw // MakeRaw puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be // mode and returns the previous state of the terminal so that it can be
// restored. // restored.
func MakeRaw(fd uintptr) (*State, error) { func MakeRaw(fd uintptr) (*State, error) {
var state *State
state, err := SaveState(fd) state, err := SaveState(fd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings // See
state.mode &^= (ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT) // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
err = SetConsoleMode(fd, state.mode) // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
mode := state.mode
// Disable these modes
mode &^= winconsole.ENABLE_ECHO_INPUT
mode &^= winconsole.ENABLE_LINE_INPUT
mode &^= winconsole.ENABLE_MOUSE_INPUT
mode &^= winconsole.ENABLE_WINDOW_INPUT
mode &^= winconsole.ENABLE_PROCESSED_INPUT
// Enable these modes
mode |= winconsole.ENABLE_EXTENDED_FLAGS
mode |= winconsole.ENABLE_INSERT_MODE
mode |= winconsole.ENABLE_QUICK_EDIT_MODE
err = winconsole.SetConsoleMode(fd, mode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
// +build windows
package winconsole
import (
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Sirupsen/logrus"
)
const (
// Consts for Get/SetConsoleMode function
// -- 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
// If parameter is a screen buffer handle, additional values
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
//http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes
FOREGROUND_BLUE = 1
FOREGROUND_GREEN = 2
FOREGROUND_RED = 4
FOREGROUND_INTENSITY = 8
FOREGROUND_MASK_SET = 0x000F
FOREGROUND_MASK_UNSET = 0xFFF0
BACKGROUND_BLUE = 16
BACKGROUND_GREEN = 32
BACKGROUND_RED = 64
BACKGROUND_INTENSITY = 128
BACKGROUND_MASK_SET = 0x00F0
BACKGROUND_MASK_UNSET = 0xFF0F
COMMON_LVB_REVERSE_VIDEO = 0x4000
COMMON_LVB_UNDERSCORE = 0x8000
// http://man7.org/linux/man-pages/man4/console_codes.4.html
// ECMA-48 Set Graphics Rendition
ANSI_ATTR_RESET = 0
ANSI_ATTR_BOLD = 1
ANSI_ATTR_DIM = 2
ANSI_ATTR_UNDERLINE = 4
ANSI_ATTR_BLINK = 5
ANSI_ATTR_REVERSE = 7
ANSI_ATTR_INVISIBLE = 8
ANSI_ATTR_UNDERLINE_OFF = 24
ANSI_ATTR_BLINK_OFF = 25
ANSI_ATTR_REVERSE_OFF = 27
ANSI_ATTR_INVISIBLE_OFF = 8
ANSI_FOREGROUND_BLACK = 30
ANSI_FOREGROUND_RED = 31
ANSI_FOREGROUND_GREEN = 32
ANSI_FOREGROUND_YELLOW = 33
ANSI_FOREGROUND_BLUE = 34
ANSI_FOREGROUND_MAGENTA = 35
ANSI_FOREGROUND_CYAN = 36
ANSI_FOREGROUND_WHITE = 37
ANSI_FOREGROUND_DEFAULT = 39
ANSI_BACKGROUND_BLACK = 40
ANSI_BACKGROUND_RED = 41
ANSI_BACKGROUND_GREEN = 42
ANSI_BACKGROUND_YELLOW = 43
ANSI_BACKGROUND_BLUE = 44
ANSI_BACKGROUND_MAGENTA = 45
ANSI_BACKGROUND_CYAN = 46
ANSI_BACKGROUND_WHITE = 47
ANSI_BACKGROUND_DEFAULT = 49
ANSI_MAX_CMD_LENGTH = 256
MAX_INPUT_EVENTS = 128
MAX_INPUT_BUFFER = 1024
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 24
)
// http://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
)
var kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
var (
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
fillConsoleOutputCharacterProc = kernel32DLL.NewProc("FillConsoleOutputCharacterW")
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
getNumberOfConsoleInputEventsProc = kernel32DLL.NewProc("GetNumberOfConsoleInputEvents")
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
)
// types for calling various windows API
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx
type (
SHORT int16
BOOL int32
WORD uint16
WCHAR uint16
DWORD uint32
SMALL_RECT struct {
Left SHORT
Top SHORT
Right SHORT
Bottom SHORT
}
COORD struct {
X SHORT
Y SHORT
}
CONSOLE_SCREEN_BUFFER_INFO struct {
Size COORD
CursorPosition COORD
Attributes WORD
Window SMALL_RECT
MaximumWindowSize COORD
}
CONSOLE_CURSOR_INFO struct {
Size DWORD
Visible BOOL
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684166(v=vs.85).aspx
KEY_EVENT_RECORD struct {
KeyDown BOOL
RepeatCount WORD
VirtualKeyCode WORD
VirtualScanCode WORD
UnicodeChar WCHAR
ControlKeyState DWORD
}
INPUT_RECORD struct {
EventType WORD
KeyEvent KEY_EVENT_RECORD
}
CHAR_INFO struct {
UnicodeChar WCHAR
Attributes WORD
}
)
// TODO(azlinux): Basic type clean-up
// -- Convert all uses of uintptr to syscall.Handle to be consistent with Windows syscall
// -- Convert, as appropriate, types to use defined Windows types (e.g., DWORD instead of uint32)
// Implements the TerminalEmulator interface
type WindowsTerminal struct {
outMutex sync.Mutex
inMutex sync.Mutex
inputBuffer []byte
inputSize int
inputEvents []INPUT_RECORD
screenBufferInfo *CONSOLE_SCREEN_BUFFER_INFO
inputEscapeSequence []byte
}
func getStdHandle(stdhandle int) uintptr {
handle, err := syscall.GetStdHandle(stdhandle)
if err != nil {
panic(fmt.Errorf("could not get standard io handle %d", stdhandle))
}
return uintptr(handle)
}
func WinConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
handler := &WindowsTerminal{
inputBuffer: make([]byte, MAX_INPUT_BUFFER),
inputEscapeSequence: []byte(KEY_ESC_CSI),
inputEvents: make([]INPUT_RECORD, MAX_INPUT_EVENTS),
}
if IsConsole(os.Stdin.Fd()) {
stdIn = &terminalReader{
wrappedReader: os.Stdin,
emulator: handler,
command: make([]byte, 0, ANSI_MAX_CMD_LENGTH),
fd: getStdHandle(syscall.STD_INPUT_HANDLE),
}
} else {
stdIn = os.Stdin
}
if IsConsole(os.Stdout.Fd()) {
stdoutHandle := getStdHandle(syscall.STD_OUTPUT_HANDLE)
// Save current screen buffer info
screenBufferInfo, err := GetConsoleScreenBufferInfo(stdoutHandle)
if err != nil {
// If GetConsoleScreenBufferInfo returns a nil error, it usually means that stdout is not a TTY.
// However, this is in the branch where stdout is a TTY, hence the panic.
panic("could not get console screen buffer info")
}
handler.screenBufferInfo = screenBufferInfo
buffer = make([]CHAR_INFO, screenBufferInfo.MaximumWindowSize.X*screenBufferInfo.MaximumWindowSize.Y)
stdOut = &terminalWriter{
wrappedWriter: os.Stdout,
emulator: handler,
command: make([]byte, 0, ANSI_MAX_CMD_LENGTH),
fd: stdoutHandle,
}
} else {
stdOut = os.Stdout
}
if IsConsole(os.Stderr.Fd()) {
stdErr = &terminalWriter{
wrappedWriter: os.Stderr,
emulator: handler,
command: make([]byte, 0, ANSI_MAX_CMD_LENGTH),
fd: getStdHandle(syscall.STD_ERROR_HANDLE),
}
} else {
stdErr = os.Stderr
}
return stdIn, stdOut, stdErr
}
// GetHandleInfo returns file descriptor and bool indicating whether the file is a console.
func GetHandleInfo(in interface{}) (uintptr, bool) {
var inFd uintptr
var isTerminalIn bool
switch t := in.(type) {
case *terminalReader:
in = t.wrappedReader
case *terminalWriter:
in = t.wrappedWriter
}
if file, ok := in.(*os.File); ok {
inFd = file.Fd()
isTerminalIn = IsConsole(inFd)
}
return inFd, isTerminalIn
}
func getError(r1, r2 uintptr, lastErr error) error {
// If the function fails, the return value is zero.
if r1 == 0 {
if lastErr != nil {
return lastErr
}
return syscall.EINVAL
}
return nil
}
// GetConsoleMode gets the console mode for given file descriptor
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
func GetConsoleMode(handle uintptr) (uint32, error) {
var mode uint32
err := syscall.GetConsoleMode(syscall.Handle(handle), &mode)
return mode, err
}
// SetConsoleMode sets the console mode for given file descriptor
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
func SetConsoleMode(handle uintptr, mode uint32) error {
return getError(setConsoleModeProc.Call(handle, uintptr(mode), 0))
}
// SetCursorVisible sets the cursor visbility
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx
func SetCursorVisible(handle uintptr, isVisible BOOL) (bool, error) {
var cursorInfo *CONSOLE_CURSOR_INFO = &CONSOLE_CURSOR_INFO{}
if err := getError(getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)); err != nil {
return false, err
}
cursorInfo.Visible = isVisible
if err := getError(setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)); err != nil {
return false, err
}
return true, nil
}
// SetWindowSize sets the size of the console window.
func SetWindowSize(handle uintptr, width, height, max SHORT) (bool, error) {
window := SMALL_RECT{Left: 0, Top: 0, Right: width - 1, Bottom: height - 1}
coord := COORD{X: width - 1, Y: max}
if err := getError(setConsoleWindowInfoProc.Call(handle, uintptr(1), uintptr(unsafe.Pointer(&window)))); err != nil {
return false, err
}
if err := getError(setConsoleScreenBufferSizeProc.Call(handle, marshal(coord))); err != nil {
return false, err
}
return true, nil
}
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
var info CONSOLE_SCREEN_BUFFER_INFO
if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil {
return nil, err
}
return &info, nil
}
// setConsoleTextAttribute sets the attributes of characters written to the
// console screen buffer by the WriteFile or WriteConsole function,
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx
func setConsoleTextAttribute(handle uintptr, attribute WORD) error {
return getError(setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0))
}
func writeConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) (bool, error) {
if err := getError(writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), marshal(bufferSize), marshal(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))); err != nil {
return false, err
}
return true, nil
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682663(v=vs.85).aspx
func fillConsoleOutputCharacter(handle uintptr, fillChar byte, length uint32, writeCord COORD) (bool, error) {
out := int64(0)
if err := getError(fillConsoleOutputCharacterProc.Call(handle, uintptr(fillChar), uintptr(length), marshal(writeCord), uintptr(unsafe.Pointer(&out)))); err != nil {
return false, err
}
return true, nil
}
// Gets the number of space characters to write for "clearing" the section of terminal
func getNumberOfChars(fromCoord COORD, toCoord COORD, screenSize COORD) uint32 {
// must be valid cursor position
if fromCoord.X < 0 || fromCoord.Y < 0 || toCoord.X < 0 || toCoord.Y < 0 {
return 0
}
if fromCoord.X >= screenSize.X || fromCoord.Y >= screenSize.Y || toCoord.X >= screenSize.X || toCoord.Y >= screenSize.Y {
return 0
}
// can't be backwards
if fromCoord.Y > toCoord.Y {
return 0
}
// same line
if fromCoord.Y == toCoord.Y {
return uint32(toCoord.X-fromCoord.X) + 1
}
// spans more than one line
if fromCoord.Y < toCoord.Y {
// from start till end of line for first line + from start of line till end
retValue := uint32(screenSize.X-fromCoord.X) + uint32(toCoord.X) + 1
// don't count first and last line
linesBetween := toCoord.Y - fromCoord.Y - 1
if linesBetween > 0 {
retValue = retValue + uint32(linesBetween*screenSize.X)
}
return retValue
}
return 0
}
var buffer []CHAR_INFO
func clearDisplayRect(handle uintptr, attributes WORD, fromCoord COORD, toCoord COORD) (uint32, error) {
var writeRegion SMALL_RECT
writeRegion.Left = fromCoord.X
writeRegion.Top = fromCoord.Y
writeRegion.Right = toCoord.X
writeRegion.Bottom = toCoord.Y
// allocate and initialize buffer
width := toCoord.X - fromCoord.X + 1
height := toCoord.Y - fromCoord.Y + 1
size := uint32(width) * uint32(height)
if size > 0 {
buffer := make([]CHAR_INFO, size)
for i := range buffer {
buffer[i] = CHAR_INFO{WCHAR(' '), attributes}
}
// Write to buffer
r, err := writeConsoleOutput(handle, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, &writeRegion)
if !r {
if err != nil {
return 0, err
}
return 0, syscall.EINVAL
}
}
return uint32(size), nil
}
func clearDisplayRange(handle uintptr, attributes WORD, fromCoord COORD, toCoord COORD) (uint32, error) {
nw := uint32(0)
// start and end on same line
if fromCoord.Y == toCoord.Y {
return clearDisplayRect(handle, attributes, fromCoord, toCoord)
}
// TODO(azlinux): if full screen, optimize
// spans more than one line
if fromCoord.Y < toCoord.Y {
// from start position till end of line for first line
n, err := clearDisplayRect(handle, attributes, fromCoord, COORD{X: toCoord.X, Y: fromCoord.Y})
if err != nil {
return nw, err
}
nw += n
// lines between
linesBetween := toCoord.Y - fromCoord.Y - 1
if linesBetween > 0 {
n, err = clearDisplayRect(handle, attributes, COORD{X: 0, Y: fromCoord.Y + 1}, COORD{X: toCoord.X, Y: toCoord.Y - 1})
if err != nil {
return nw, err
}
nw += n
}
// lines at end
n, err = clearDisplayRect(handle, attributes, COORD{X: 0, Y: toCoord.Y}, toCoord)
if err != nil {
return nw, err
}
nw += n
}
return nw, nil
}
// setConsoleCursorPosition sets the console cursor position
// Note The X and Y are zero based
// If relative is true then the new position is relative to current one
func setConsoleCursorPosition(handle uintptr, isRelative bool, column int16, line int16) error {
screenBufferInfo, err := GetConsoleScreenBufferInfo(handle)
if err != nil {
return err
}
var position COORD
if isRelative {
position.X = screenBufferInfo.CursorPosition.X + SHORT(column)
position.Y = screenBufferInfo.CursorPosition.Y + SHORT(line)
} else {
position.X = SHORT(column)
position.Y = SHORT(line)
}
return getError(setConsoleCursorPositionProc.Call(handle, marshal(position), 0))
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683207(v=vs.85).aspx
func getNumberOfConsoleInputEvents(handle uintptr) (uint16, error) {
var n DWORD
if err := getError(getNumberOfConsoleInputEventsProc.Call(handle, uintptr(unsafe.Pointer(&n)))); err != nil {
return 0, err
}
return uint16(n), nil
}
//http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
func readConsoleInputKey(handle uintptr, inputBuffer []INPUT_RECORD) (int, error) {
var nr DWORD
if err := getError(readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&inputBuffer[0])), uintptr(len(inputBuffer)), uintptr(unsafe.Pointer(&nr)))); err != nil {
return 0, err
}
return int(nr), nil
}
func getWindowsTextAttributeForAnsiValue(originalFlag WORD, defaultValue WORD, ansiValue int16) (WORD, error) {
flag := WORD(originalFlag)
if flag == 0 {
flag = defaultValue
}
switch ansiValue {
case ANSI_ATTR_RESET:
flag &^= COMMON_LVB_UNDERSCORE
flag &^= BACKGROUND_INTENSITY
flag = flag | FOREGROUND_INTENSITY
case ANSI_ATTR_INVISIBLE:
// TODO: how do you reset reverse?
case ANSI_ATTR_UNDERLINE:
flag = flag | COMMON_LVB_UNDERSCORE
case ANSI_ATTR_BLINK:
// seems like background intenisty is blink
flag = flag | BACKGROUND_INTENSITY
case ANSI_ATTR_UNDERLINE_OFF:
flag &^= COMMON_LVB_UNDERSCORE
case ANSI_ATTR_BLINK_OFF:
// seems like background intenisty is blink
flag &^= BACKGROUND_INTENSITY
case ANSI_ATTR_BOLD:
flag = flag | FOREGROUND_INTENSITY
case ANSI_ATTR_DIM:
flag &^= FOREGROUND_INTENSITY
case ANSI_ATTR_REVERSE, ANSI_ATTR_REVERSE_OFF:
// swap forground and background bits
foreground := flag & FOREGROUND_MASK_SET
background := flag & BACKGROUND_MASK_SET
flag = (flag & BACKGROUND_MASK_UNSET & FOREGROUND_MASK_UNSET) | (foreground << 4) | (background >> 4)
// FOREGROUND
case ANSI_FOREGROUND_DEFAULT:
flag = (flag & FOREGROUND_MASK_UNSET) | (defaultValue & FOREGROUND_MASK_SET)
case ANSI_FOREGROUND_BLACK:
flag = flag ^ (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
case ANSI_FOREGROUND_RED:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED
case ANSI_FOREGROUND_GREEN:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_GREEN
case ANSI_FOREGROUND_YELLOW:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_GREEN
case ANSI_FOREGROUND_BLUE:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_BLUE
case ANSI_FOREGROUND_MAGENTA:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_BLUE
case ANSI_FOREGROUND_CYAN:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_GREEN | FOREGROUND_BLUE
case ANSI_FOREGROUND_WHITE:
flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
// Background
case ANSI_BACKGROUND_DEFAULT:
// Black with no intensity
flag = (flag & BACKGROUND_MASK_UNSET) | (defaultValue & BACKGROUND_MASK_SET)
case ANSI_BACKGROUND_BLACK:
flag = (flag & BACKGROUND_MASK_UNSET)
case ANSI_BACKGROUND_RED:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED
case ANSI_BACKGROUND_GREEN:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_GREEN
case ANSI_BACKGROUND_YELLOW:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_GREEN
case ANSI_BACKGROUND_BLUE:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_BLUE
case ANSI_BACKGROUND_MAGENTA:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_BLUE
case ANSI_BACKGROUND_CYAN:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_GREEN | BACKGROUND_BLUE
case ANSI_BACKGROUND_WHITE:
flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
}
return flag, nil
}
// HandleOutputCommand interpretes the Ansi commands and then makes appropriate Win32 calls
func (term *WindowsTerminal) HandleOutputCommand(handle uintptr, command []byte) (n int, err error) {
// always consider all the bytes in command, processed
n = len(command)
parsedCommand := parseAnsiCommand(command)
logrus.Debugf("[windows] HandleOutputCommand: %v", parsedCommand)
// console settings changes need to happen in atomic way
term.outMutex.Lock()
defer term.outMutex.Unlock()
switch parsedCommand.Command {
case "m":
// [Value;...;Valuem
// Set Graphics Mode:
// Calls the graphics functions specified by the following values.
// These specified functions remain active until the next occurrence of this escape sequence.
// Graphics mode changes the colors and attributes of text (such as bold and underline) displayed on the screen.
screenBufferInfo, err := GetConsoleScreenBufferInfo(handle)
if err != nil {
return n, err
}
flag := screenBufferInfo.Attributes
for _, e := range parsedCommand.Parameters {
value, _ := strconv.ParseInt(e, 10, 16) // base 10, 16 bit
if value == ANSI_ATTR_RESET {
flag = term.screenBufferInfo.Attributes // reset
} else {
flag, err = getWindowsTextAttributeForAnsiValue(flag, term.screenBufferInfo.Attributes, int16(value))
if err != nil {
return n, err
}
}
}
if err := setConsoleTextAttribute(handle, flag); err != nil {
return n, err
}
case "H", "f":
// [line;columnH
// [line;columnf
// Moves the cursor to the specified position (coordinates).
// If you do not specify a position, the cursor moves to the home position at the upper-left corner of the screen (line 0, column 0).
screenBufferInfo, err := GetConsoleScreenBufferInfo(handle)
if err != nil {
return n, err
}
line, err := parseInt16OrDefault(parsedCommand.getParam(0), 1)
if err != nil {
return n, err
}
if line > int16(screenBufferInfo.Window.Bottom) {
line = int16(screenBufferInfo.Window.Bottom) + 1
}
column, err := parseInt16OrDefault(parsedCommand.getParam(1), 1)
if err != nil {
return n, err
}
if column > int16(screenBufferInfo.Window.Right) {
column = int16(screenBufferInfo.Window.Right) + 1
}
// The numbers are not 0 based, but 1 based
logrus.Debugf("[windows] HandleOutputCommmand: Moving cursor to (%v,%v)", column-1, line-1)
if err := setConsoleCursorPosition(handle, false, column-1, line-1); err != nil {
return n, err
}
case "A":
// [valueA
// Moves the cursor up by the specified number of lines without changing columns.
// If the cursor is already on the top line, ignores this sequence.
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1)
if err != nil {
return len(command), err
}
if err := setConsoleCursorPosition(handle, true, 0, -value); err != nil {
return n, err
}
case "B":
// [valueB
// Moves the cursor down by the specified number of lines without changing columns.
// If the cursor is already on the bottom line, ignores this sequence.
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1)
if err != nil {
return n, err
}
if err := setConsoleCursorPosition(handle, true, 0, value); err != nil {
return n, err
}
case "C":
// [valueC
// Moves the cursor forward by the specified number of columns without changing lines.
// If the cursor is already in the rightmost column, ignores this sequence.
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1)
if err != nil {
return n, err
}
if err := setConsoleCursorPosition(handle, true, value, 0); err != nil {
return n, err
}
case "D":
// [valueD
// Moves the cursor back by the specified number of columns without changing lines.
// If the cursor is already in the leftmost column, ignores this sequence.
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1)
if err != nil {
return n, err
}
if err := setConsoleCursorPosition(handle, true, -value, 0); err != nil {
return n, err
}
case "J":
// [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.
// Clears the screen and moves the cursor to the home position (line 0, column 0).
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 0)
if err != nil {
return n, err
}
var start COORD
var cursor COORD
var end COORD
screenBufferInfo, err := GetConsoleScreenBufferInfo(handle)
if err != nil {
return n, err
}
switch value {
case 0:
start = screenBufferInfo.CursorPosition
// end of the buffer
end.X = screenBufferInfo.Size.X - 1
end.Y = screenBufferInfo.Size.Y - 1
// cursor
cursor = screenBufferInfo.CursorPosition
case 1:
// start of the screen
start.X = 0
start.Y = 0
// end of the screen
end = screenBufferInfo.CursorPosition
// cursor
cursor = screenBufferInfo.CursorPosition
case 2:
// start of the screen
start.X = 0
start.Y = 0
// end of the buffer
end.X = screenBufferInfo.Size.X - 1
end.Y = screenBufferInfo.Size.Y - 1
// cursor
cursor.X = 0
cursor.Y = 0
}
if _, err := clearDisplayRange(uintptr(handle), term.screenBufferInfo.Attributes, start, end); err != nil {
return n, err
}
// remember the the cursor position is 1 based
if err := setConsoleCursorPosition(handle, false, int16(cursor.X), int16(cursor.Y)); err != nil {
return n, err
}
case "K":
// [K
// Clears all characters from the cursor position to the end of the line (including the character at the cursor position).
// [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.
value, err := parseInt16OrDefault(parsedCommand.getParam(0), 0)
var start COORD
var cursor COORD
var end COORD
screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle))
if err != nil {
return n, err
}
switch value {
case 0:
// start is where cursor is
start = screenBufferInfo.CursorPosition
// end of line
end.X = screenBufferInfo.Size.X - 1
end.Y = screenBufferInfo.CursorPosition.Y
// cursor remains the same
cursor = screenBufferInfo.CursorPosition
case 1:
// beginning of line
start.X = 0
start.Y = screenBufferInfo.CursorPosition.Y
// until cursor
end = screenBufferInfo.CursorPosition
// cursor remains the same
cursor = screenBufferInfo.CursorPosition
case 2:
// start of the line
start.X = 0
start.Y = screenBufferInfo.CursorPosition.Y - 1
// end of the line
end.X = screenBufferInfo.Size.X - 1
end.Y = screenBufferInfo.CursorPosition.Y - 1
// cursor
cursor.X = 0
cursor.Y = screenBufferInfo.CursorPosition.Y - 1
}
if _, err := clearDisplayRange(uintptr(handle), term.screenBufferInfo.Attributes, start, end); err != nil {
return n, err
}
// remember the the cursor position is 1 based
if err := setConsoleCursorPosition(uintptr(handle), false, int16(cursor.X), int16(cursor.Y)); err != nil {
return n, err
}
case "l":
for _, value := range parsedCommand.Parameters {
switch value {
case "?25", "25":
SetCursorVisible(uintptr(handle), BOOL(0))
case "?1049", "1049":
// TODO (azlinux): Restore terminal
case "?1", "1":
// If the DECCKM function is reset, then the arrow keys send ANSI cursor sequences to the host.
term.inputEscapeSequence = []byte(KEY_ESC_CSI)
}
}
case "h":
for _, value := range parsedCommand.Parameters {
switch value {
case "?25", "25":
SetCursorVisible(uintptr(handle), BOOL(1))
case "?1049", "1049":
// TODO (azlinux): Save terminal
case "?1", "1":
// If the DECCKM function is set, then the arrow keys send application sequences to the host.
// DECCKM (default off): When set, the cursor keys send an ESC O prefix, rather than ESC [.
term.inputEscapeSequence = []byte(KEY_ESC_O)
}
}
case "]":
/*
TODO (azlinux):
Linux Console Private CSI Sequences
The following sequences are neither ECMA-48 nor native VT102. They are
native to the Linux console driver. Colors are in SGR parameters: 0 =
black, 1 = red, 2 = green, 3 = brown, 4 = blue, 5 = magenta, 6 = cyan,
7 = white.
ESC [ 1 ; n ] Set color n as the underline color
ESC [ 2 ; n ] Set color n as the dim color
ESC [ 8 ] Make the current color pair the default attributes.
ESC [ 9 ; n ] Set screen blank timeout to n minutes.
ESC [ 10 ; n ] Set bell frequency in Hz.
ESC [ 11 ; n ] Set bell duration in msec.
ESC [ 12 ; n ] Bring specified console to the front.
ESC [ 13 ] Unblank the screen.
ESC [ 14 ; n ] Set the VESA powerdown interval in minutes.
*/
}
return n, nil
}
// WriteChars writes the bytes to given writer.
func (term *WindowsTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
return w.Write(p)
}
const (
CAPSLOCK_ON = 0x0080 //The CAPS LOCK light is on.
ENHANCED_KEY = 0x0100 //The key is enhanced.
LEFT_ALT_PRESSED = 0x0002 //The left ALT key is pressed.
LEFT_CTRL_PRESSED = 0x0008 //The left CTRL key is pressed.
NUMLOCK_ON = 0x0020 //The NUM LOCK light is on.
RIGHT_ALT_PRESSED = 0x0001 //The right ALT key is pressed.
RIGHT_CTRL_PRESSED = 0x0004 //The right CTRL key is pressed.
SCROLLLOCK_ON = 0x0040 //The SCROLL LOCK light is on.
SHIFT_PRESSED = 0x0010 // The SHIFT key is pressed.
)
const (
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"
)
var keyMapPrefix = map[WORD]string{
VK_UP: "\x1B[%sA",
VK_DOWN: "\x1B[%sB",
VK_RIGHT: "\x1B[%sC",
VK_LEFT: "\x1B[%sD",
VK_HOME: "\x1B[1%s~", // showkey shows ^[[1
VK_END: "\x1B[4%s~", // showkey shows ^[[4
VK_INSERT: "\x1B[2%s~",
VK_DELETE: "\x1B[3%s~",
VK_PRIOR: "\x1B[5%s~",
VK_NEXT: "\x1B[6%s~",
VK_F1: "",
VK_F2: "",
VK_F3: "\x1B[13%s~",
VK_F4: "\x1B[14%s~",
VK_F5: "\x1B[15%s~",
VK_F6: "\x1B[17%s~",
VK_F7: "\x1B[18%s~",
VK_F8: "\x1B[19%s~",
VK_F9: "\x1B[20%s~",
VK_F10: "\x1B[21%s~",
VK_F11: "\x1B[23%s~",
VK_F12: "\x1B[24%s~",
}
var arrowKeyMapPrefix = map[WORD]string{
VK_UP: "%s%sA",
VK_DOWN: "%s%sB",
VK_RIGHT: "%s%sC",
VK_LEFT: "%s%sD",
}
func getControlStateParameter(shift, alt, control, meta bool) string {
if shift && alt && control {
return KEY_CONTROL_PARAM_8
}
if alt && control {
return KEY_CONTROL_PARAM_7
}
if shift && control {
return KEY_CONTROL_PARAM_6
}
if control {
return KEY_CONTROL_PARAM_5
}
if shift && alt {
return KEY_CONTROL_PARAM_4
}
if alt {
return KEY_CONTROL_PARAM_3
}
if shift {
return KEY_CONTROL_PARAM_2
}
return ""
}
func getControlKeys(controlState DWORD) (shift, alt, control bool) {
shift = 0 != (controlState & SHIFT_PRESSED)
alt = 0 != (controlState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED))
control = 0 != (controlState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))
return shift, alt, control
}
func charSequenceForKeys(key WORD, controlState DWORD, escapeSequence []byte) string {
i, ok := arrowKeyMapPrefix[key]
if ok {
shift, alt, control := getControlKeys(controlState)
modifier := getControlStateParameter(shift, alt, control, false)
return fmt.Sprintf(i, escapeSequence, modifier)
}
i, ok = keyMapPrefix[key]
if ok {
shift, alt, control := getControlKeys(controlState)
modifier := getControlStateParameter(shift, alt, control, false)
return fmt.Sprintf(i, modifier)
}
return ""
}
// mapKeystokeToTerminalString maps the given input event record to string
func mapKeystokeToTerminalString(keyEvent *KEY_EVENT_RECORD, escapeSequence []byte) string {
_, alt, control := getControlKeys(keyEvent.ControlKeyState)
if keyEvent.UnicodeChar == 0 {
return charSequenceForKeys(keyEvent.VirtualKeyCode, keyEvent.ControlKeyState, escapeSequence)
}
if control {
// TODO(azlinux): Implement following control sequences
// <Ctrl>-D Signals the end of input from the keyboard; also exits current shell.
// <Ctrl>-H Deletes the first character to the left of the cursor. Also called the ERASE key.
// <Ctrl>-Q Restarts printing after it has been stopped with <Ctrl>-s.
// <Ctrl>-S Suspends printing on the screen (does not stop the program).
// <Ctrl>-U Deletes all characters on the current line. Also called the KILL key.
// <Ctrl>-E Quits current command and creates a core
}
// <Alt>+Key generates ESC N Key
if !control && alt {
return KEY_ESC_N + strings.ToLower(string(keyEvent.UnicodeChar))
}
return string(keyEvent.UnicodeChar)
}
// getAvailableInputEvents polls the console for availble events
// The function does not return until at least one input record has been read.
func getAvailableInputEvents(handle uintptr, inputEvents []INPUT_RECORD) (n int, err error) {
// TODO(azlinux): Why is there a for loop? Seems to me, that `n` cannot be negative. - tibor
for {
// Read number of console events available
n, err = readConsoleInputKey(handle, inputEvents)
if err != nil || n >= 0 {
return n, err
}
}
}
// getTranslatedKeyCodes converts the input events into the string of characters
// The ansi escape sequence are used to map key strokes to the strings
func getTranslatedKeyCodes(inputEvents []INPUT_RECORD, escapeSequence []byte) string {
var buf bytes.Buffer
for i := 0; i < len(inputEvents); i++ {
input := inputEvents[i]
if input.EventType == KEY_EVENT && input.KeyEvent.KeyDown != 0 {
keyString := mapKeystokeToTerminalString(&input.KeyEvent, escapeSequence)
buf.WriteString(keyString)
}
}
return buf.String()
}
// ReadChars reads the characters from the given reader
func (term *WindowsTerminal) ReadChars(fd uintptr, r io.Reader, p []byte) (n int, err error) {
for term.inputSize == 0 {
nr, err := getAvailableInputEvents(fd, term.inputEvents)
if nr == 0 && nil != err {
return n, err
}
if nr > 0 {
keyCodes := getTranslatedKeyCodes(term.inputEvents[:nr], term.inputEscapeSequence)
term.inputSize = copy(term.inputBuffer, keyCodes)
}
}
n = copy(p, term.inputBuffer[:term.inputSize])
term.inputSize -= n
return n, nil
}
// HandleInputSequence interprets the input sequence command
func (term *WindowsTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) {
return 0, nil
}
func marshal(c COORD) uintptr {
return uintptr(*((*DWORD)(unsafe.Pointer(&c))))
}
// IsConsole returns true if the given file descriptor is a terminal.
// -- The code assumes that GetConsoleMode will return an error for file descriptors that are not a console.
func IsConsole(fd uintptr) bool {
_, e := GetConsoleMode(fd)
return e == nil
}
// +build windows
package winconsole
import (
"fmt"
"testing"
)
func helpsTestParseInt16OrDefault(t *testing.T, expectedValue int16, shouldFail bool, input string, defaultValue int16, format string, args ...string) {
value, err := parseInt16OrDefault(input, defaultValue)
if nil != err && !shouldFail {
t.Errorf("Unexpected error returned %v", err)
t.Errorf(format, args)
}
if nil == err && shouldFail {
t.Errorf("Should have failed as expected\n\tReturned value = %d", value)
t.Errorf(format, args)
}
if expectedValue != value {
t.Errorf("The value returned does not match expected\n\tExpected:%v\n\t:Actual%v", expectedValue, value)
t.Errorf(format, args)
}
}
func TestParseInt16OrDefault(t *testing.T) {
// empty string
helpsTestParseInt16OrDefault(t, 0, false, "", 0, "Empty string returns default")
helpsTestParseInt16OrDefault(t, 2, false, "", 2, "Empty string returns default")
// normal case
helpsTestParseInt16OrDefault(t, 0, false, "0", 0, "0 handled correctly")
helpsTestParseInt16OrDefault(t, 111, false, "111", 2, "Normal")
helpsTestParseInt16OrDefault(t, 111, false, "+111", 2, "+N")
helpsTestParseInt16OrDefault(t, -111, false, "-111", 2, "-N")
helpsTestParseInt16OrDefault(t, 0, false, "+0", 11, "+0")
helpsTestParseInt16OrDefault(t, 0, false, "-0", 12, "-0")
// ill formed strings
helpsTestParseInt16OrDefault(t, 0, true, "abc", 0, "Invalid string")
helpsTestParseInt16OrDefault(t, 42, true, "+= 23", 42, "Invalid string")
helpsTestParseInt16OrDefault(t, 42, true, "123.45", 42, "float like")
}
func helpsTestGetNumberOfChars(t *testing.T, expected uint32, fromCoord COORD, toCoord COORD, screenSize COORD, format string, args ...interface{}) {
actual := getNumberOfChars(fromCoord, toCoord, screenSize)
mesg := fmt.Sprintf(format, args)
assertTrue(t, expected == actual, fmt.Sprintf("%s Expected=%d, Actual=%d, Parameters = { fromCoord=%+v, toCoord=%+v, screenSize=%+v", mesg, expected, actual, fromCoord, toCoord, screenSize))
}
func TestGetNumberOfChars(t *testing.T) {
// Note: The columns and lines are 0 based
// Also that interval is "inclusive" means will have both start and end chars
// This test only tests the number opf characters being written
// all four corners
maxWindow := COORD{X: 80, Y: 50}
leftTop := COORD{X: 0, Y: 0}
rightTop := COORD{X: 79, Y: 0}
leftBottom := COORD{X: 0, Y: 49}
rightBottom := COORD{X: 79, Y: 49}
// same position
helpsTestGetNumberOfChars(t, 1, COORD{X: 1, Y: 14}, COORD{X: 1, Y: 14}, COORD{X: 80, Y: 50}, "Same position random line")
// four corners
helpsTestGetNumberOfChars(t, 1, leftTop, leftTop, maxWindow, "Same position- leftTop")
helpsTestGetNumberOfChars(t, 1, rightTop, rightTop, maxWindow, "Same position- rightTop")
helpsTestGetNumberOfChars(t, 1, leftBottom, leftBottom, maxWindow, "Same position- leftBottom")
helpsTestGetNumberOfChars(t, 1, rightBottom, rightBottom, maxWindow, "Same position- rightBottom")
// from this char to next char on same line
helpsTestGetNumberOfChars(t, 2, COORD{X: 0, Y: 0}, COORD{X: 1, Y: 0}, maxWindow, "Next position on same line")
helpsTestGetNumberOfChars(t, 2, COORD{X: 1, Y: 14}, COORD{X: 2, Y: 14}, maxWindow, "Next position on same line")
// from this char to next 10 chars on same line
helpsTestGetNumberOfChars(t, 11, COORD{X: 0, Y: 0}, COORD{X: 10, Y: 0}, maxWindow, "Next position on same line")
helpsTestGetNumberOfChars(t, 11, COORD{X: 1, Y: 14}, COORD{X: 11, Y: 14}, maxWindow, "Next position on same line")
helpsTestGetNumberOfChars(t, 5, COORD{X: 3, Y: 11}, COORD{X: 7, Y: 11}, maxWindow, "To and from on same line")
helpsTestGetNumberOfChars(t, 8, COORD{X: 0, Y: 34}, COORD{X: 7, Y: 34}, maxWindow, "Start of line to middle")
helpsTestGetNumberOfChars(t, 4, COORD{X: 76, Y: 34}, COORD{X: 79, Y: 34}, maxWindow, "Middle to end of line")
// multiple lines - 1
helpsTestGetNumberOfChars(t, 81, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 1}, maxWindow, "one line below same X")
helpsTestGetNumberOfChars(t, 81, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 11}, maxWindow, "one line below same X")
// multiple lines - 2
helpsTestGetNumberOfChars(t, 161, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 2}, maxWindow, "one line below same X")
helpsTestGetNumberOfChars(t, 161, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 12}, maxWindow, "one line below same X")
// multiple lines - 3
helpsTestGetNumberOfChars(t, 241, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 3}, maxWindow, "one line below same X")
helpsTestGetNumberOfChars(t, 241, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 13}, maxWindow, "one line below same X")
// full line
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 0}, COORD{X: 79, Y: 0}, maxWindow, "Full line - first")
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 23}, COORD{X: 79, Y: 23}, maxWindow, "Full line - random")
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 49}, COORD{X: 79, Y: 49}, maxWindow, "Full line - last")
// full screen
helpsTestGetNumberOfChars(t, 80*50, leftTop, rightBottom, maxWindow, "full screen")
helpsTestGetNumberOfChars(t, 80*50-1, COORD{X: 1, Y: 0}, rightBottom, maxWindow, "dropping first char to, end of screen")
helpsTestGetNumberOfChars(t, 80*50-2, COORD{X: 2, Y: 0}, rightBottom, maxWindow, "dropping first two char to, end of screen")
helpsTestGetNumberOfChars(t, 80*50-1, leftTop, COORD{X: 78, Y: 49}, maxWindow, "from start of screen, till last char-1")
helpsTestGetNumberOfChars(t, 80*50-2, leftTop, COORD{X: 77, Y: 49}, maxWindow, "from start of screen, till last char-2")
helpsTestGetNumberOfChars(t, 80*50-5, COORD{X: 4, Y: 0}, COORD{X: 78, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-1")
helpsTestGetNumberOfChars(t, 80*50-6, COORD{X: 4, Y: 0}, COORD{X: 77, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-2")
}
var allForeground = []int16{
ANSI_FOREGROUND_BLACK,
ANSI_FOREGROUND_RED,
ANSI_FOREGROUND_GREEN,
ANSI_FOREGROUND_YELLOW,
ANSI_FOREGROUND_BLUE,
ANSI_FOREGROUND_MAGENTA,
ANSI_FOREGROUND_CYAN,
ANSI_FOREGROUND_WHITE,
ANSI_FOREGROUND_DEFAULT,
}
var allBackground = []int16{
ANSI_BACKGROUND_BLACK,
ANSI_BACKGROUND_RED,
ANSI_BACKGROUND_GREEN,
ANSI_BACKGROUND_YELLOW,
ANSI_BACKGROUND_BLUE,
ANSI_BACKGROUND_MAGENTA,
ANSI_BACKGROUND_CYAN,
ANSI_BACKGROUND_WHITE,
ANSI_BACKGROUND_DEFAULT,
}
func maskForeground(flag WORD) WORD {
return flag & FOREGROUND_MASK_UNSET
}
func onlyForeground(flag WORD) WORD {
return flag & FOREGROUND_MASK_SET
}
func maskBackground(flag WORD) WORD {
return flag & BACKGROUND_MASK_UNSET
}
func onlyBackground(flag WORD) WORD {
return flag & BACKGROUND_MASK_SET
}
func helpsTestGetWindowsTextAttributeForAnsiValue(t *testing.T, oldValue WORD /*, expected WORD*/, ansi int16, onlyMask WORD, restMask WORD) WORD {
actual, err := getWindowsTextAttributeForAnsiValue(oldValue, FOREGROUND_MASK_SET, ansi)
assertTrue(t, nil == err, "Should be no error")
// assert that other bits are not affected
if 0 != oldValue {
assertTrue(t, (actual&restMask) == (oldValue&restMask), "The operation should not have affected other bits actual=%X oldValue=%X ansi=%d", actual, oldValue, ansi)
}
return actual
}
func TestBackgroundForAnsiValue(t *testing.T) {
// Check that nothing else changes
// background changes
for _, state1 := range allBackground {
for _, state2 := range allBackground {
flag := WORD(0)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
}
}
// cummulative bcakground changes
for _, state1 := range allBackground {
flag := WORD(0)
for _, state2 := range allBackground {
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
}
}
// change background after foreground
for _, state1 := range allForeground {
for _, state2 := range allBackground {
flag := WORD(0)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
}
}
// change background after change cumulative
for _, state1 := range allForeground {
flag := WORD(0)
for _, state2 := range allBackground {
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
}
}
}
func TestForegroundForAnsiValue(t *testing.T) {
// Check that nothing else changes
for _, state1 := range allForeground {
for _, state2 := range allForeground {
flag := WORD(0)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
}
}
for _, state1 := range allForeground {
flag := WORD(0)
for _, state2 := range allForeground {
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
}
}
for _, state1 := range allBackground {
for _, state2 := range allForeground {
flag := WORD(0)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
}
}
for _, state1 := range allBackground {
flag := WORD(0)
for _, state2 := range allForeground {
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
}
}
}
package winconsole
import (
"fmt"
"io"
"strconv"
"strings"
)
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
const (
ANSI_ESCAPE_PRIMARY = 0x1B
ANSI_ESCAPE_SECONDARY = 0x5B
ANSI_COMMAND_FIRST = 0x40
ANSI_COMMAND_LAST = 0x7E
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 = '\\'
ANSI_BEL = 0x07
KEY_EVENT = 1
)
// Interface that implements terminal handling
type terminalEmulator interface {
HandleOutputCommand(fd uintptr, command []byte) (n int, err error)
HandleInputSequence(fd uintptr, command []byte) (n int, err error)
WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error)
ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error)
}
type terminalWriter struct {
wrappedWriter io.Writer
emulator terminalEmulator
command []byte
inSequence bool
fd uintptr
}
type terminalReader struct {
wrappedReader io.ReadCloser
emulator terminalEmulator
command []byte
inSequence bool
fd uintptr
}
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
func isAnsiCommandChar(b byte) bool {
switch {
case ANSI_COMMAND_FIRST <= b && b <= ANSI_COMMAND_LAST && b != ANSI_ESCAPE_SECONDARY:
return true
case b == ANSI_CMD_G1 || b == ANSI_CMD_OSC || b == ANSI_CMD_DECPAM || b == ANSI_CMD_DECPNM:
// non-CSI escape sequence terminator
return true
case b == ANSI_CMD_STR_TERM || b == ANSI_BEL:
// String escape sequence terminator
return true
}
return false
}
func isCharacterSelectionCmdChar(b byte) bool {
return (b == ANSI_CMD_G0 || b == ANSI_CMD_G1 || b == ANSI_CMD_G2 || b == ANSI_CMD_G3)
}
func isXtermOscSequence(command []byte, current byte) bool {
return (len(command) >= 2 && command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_CMD_OSC && current != ANSI_BEL)
}
// Write writes len(p) bytes from p to the underlying data stream.
// http://golang.org/pkg/io/#Writer
func (tw *terminalWriter) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
if tw.emulator == nil {
return tw.wrappedWriter.Write(p)
}
// Emulate terminal by extracting commands and executing them
totalWritten := 0
start := 0 // indicates start of the next chunk
end := len(p)
for current := 0; current < end; current++ {
if tw.inSequence {
// inside escape sequence
tw.command = append(tw.command, p[current])
if isAnsiCommandChar(p[current]) {
if !isXtermOscSequence(tw.command, p[current]) {
// found the last command character.
// Now we have a complete command.
nchar, err := tw.emulator.HandleOutputCommand(tw.fd, tw.command)
totalWritten += nchar
if err != nil {
return totalWritten, err
}
// clear the command
// don't include current character again
tw.command = tw.command[:0]
start = current + 1
tw.inSequence = false
}
}
} else {
if p[current] == ANSI_ESCAPE_PRIMARY {
// entering escape sequnce
tw.inSequence = true
// indicates end of "normal sequence", write whatever you have so far
if len(p[start:current]) > 0 {
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:current])
totalWritten += nw
if err != nil {
return totalWritten, err
}
}
// include the current character as part of the next sequence
tw.command = append(tw.command, p[current])
}
}
}
// note that so far, start of the escape sequence triggers writing out of bytes to console.
// For the part _after_ the end of last escape sequence, it is not written out yet. So write it out
if !tw.inSequence {
// assumption is that we can't be inside sequence and therefore command should be empty
if len(p[start:]) > 0 {
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:])
totalWritten += nw
if err != nil {
return totalWritten, err
}
}
}
return totalWritten, nil
}
// Read reads up to len(p) bytes into p.
// http://golang.org/pkg/io/#Reader
func (tr *terminalReader) Read(p []byte) (n int, err error) {
//Implementations of Read are discouraged from returning a zero byte count
// with a nil error, except when len(p) == 0.
if len(p) == 0 {
return 0, nil
}
if nil == tr.emulator {
return tr.readFromWrappedReader(p)
}
return tr.emulator.ReadChars(tr.fd, tr.wrappedReader, p)
}
// Close the underlying stream
func (tr *terminalReader) Close() (err error) {
return tr.wrappedReader.Close()
}
func (tr *terminalReader) readFromWrappedReader(p []byte) (n int, err error) {
return tr.wrappedReader.Read(p)
}
type ansiCommand struct {
CommandBytes []byte
Command string
Parameters []string
IsSpecial bool
}
func parseAnsiCommand(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
retValue := &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] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_ESCAPE_SECONDARY {
start++
}
// convert this to GetNextParam method
retValue.Parameters = strings.Split(string(command[start:lastCharIndex]), ANSI_PARAMETER_SEP)
}
return retValue
}
func (c *ansiCommand) getParam(index int) string {
if len(c.Parameters) > index {
return c.Parameters[index]
}
return ""
}
func (ac *ansiCommand) String() string {
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
bytesToHex(ac.CommandBytes),
ac.Command,
strings.Join(ac.Parameters, "\",\""))
}
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, "")
}
func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) {
if s == "" {
return defaultValue, nil
}
parsedValue, err := strconv.ParseInt(s, 10, 16)
if err != nil {
return defaultValue, err
}
return int16(parsedValue), nil
}
package winconsole
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"testing"
)
const (
WRITE_OPERATION = iota
COMMAND_OPERATION = iota
)
var languages = []string{
"Български",
"Català",
"Čeština",
"Ελληνικά",
"Español",
"Esperanto",
"Euskara",
"Français",
"Galego",
"한국어",
"ქართული",
"Latviešu",
"Lietuvių",
"Magyar",
"Nederlands",
"日本語",
"Norsk bokmål",
"Norsk nynorsk",
"Polski",
"Português",
"Română",
"Русский",
"Slovenčina",
"Slovenščina",
"Српски",
"српскохрватски",
"Suomi",
"Svenska",
"ไทย",
"Tiếng Việt",
"Türkçe",
"Українська",
"中文",
}
// Mock terminal handler object
type mockTerminal struct {
OutputCommandSequence []terminalOperation
}
// Used for recording the callback data
type terminalOperation struct {
Operation int
Data []byte
Str string
}
func (mt *mockTerminal) record(operation int, data []byte) {
op := terminalOperation{
Operation: operation,
Data: make([]byte, len(data)),
}
copy(op.Data, data)
op.Str = string(op.Data)
mt.OutputCommandSequence = append(mt.OutputCommandSequence, op)
}
func (mt *mockTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) {
mt.record(COMMAND_OPERATION, command)
return len(command), nil
}
func (mt *mockTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) {
return 0, nil
}
func (mt *mockTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) {
mt.record(WRITE_OPERATION, p)
return len(p), nil
}
func (mt *mockTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) {
return len(p), nil
}
func assertTrue(t *testing.T, cond bool, format string, args ...interface{}) {
if !cond {
t.Errorf(format, args...)
}
}
// reflect.DeepEqual does not provide detailed information as to what excatly failed.
func assertBytesEqual(t *testing.T, expected, actual []byte, format string, args ...interface{}) {
match := true
mismatchIndex := 0
if len(expected) == len(actual) {
for i := 0; i < len(expected); i++ {
if expected[i] != actual[i] {
match = false
mismatchIndex = i
break
}
}
} else {
match = false
t.Errorf("Lengths don't match Expected=%d Actual=%d", len(expected), len(actual))
}
if !match {
t.Errorf("Mismatch at index %d ", mismatchIndex)
t.Errorf("\tActual String = %s", string(actual))
t.Errorf("\tExpected String = %s", string(expected))
t.Errorf("\tActual = %v", actual)
t.Errorf("\tExpected = %v", expected)
t.Errorf(format, args)
}
}
// Just to make sure :)
func TestAssertEqualBytes(t *testing.T) {
data := []byte{9, 9, 1, 1, 1, 9, 9}
assertBytesEqual(t, data, data, "Self")
assertBytesEqual(t, data[1:4], data[1:4], "Self")
assertBytesEqual(t, []byte{1, 1}, []byte{1, 1}, "Simple match")
assertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 2, 3}, "content mismatch")
assertBytesEqual(t, []byte{1, 1, 1}, data[2:5], "slice match")
}
/*
func TestAssertEqualBytesNegative(t *testing.T) {
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
AssertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 1, 1}, "content mismatch")
}*/
// Checks that the calls received
func assertHandlerOutput(t *testing.T, mock *mockTerminal, plainText string, commands ...string) {
text := make([]byte, 0, 3*len(plainText))
cmdIndex := 0
for opIndex := 0; opIndex < len(mock.OutputCommandSequence); opIndex++ {
op := mock.OutputCommandSequence[opIndex]
if op.Operation == WRITE_OPERATION {
t.Logf("\nThe data is[%d] == %s", opIndex, string(op.Data))
text = append(text[:], op.Data...)
} else {
assertTrue(t, mock.OutputCommandSequence[opIndex].Operation == COMMAND_OPERATION, "Operation should be command : %s", fmt.Sprintf("%+v", mock))
assertBytesEqual(t, StringToBytes(commands[cmdIndex]), mock.OutputCommandSequence[opIndex].Data, "Command data should match")
cmdIndex++
}
}
assertBytesEqual(t, StringToBytes(plainText), text, "Command data should match %#v", mock)
}
func StringToBytes(str string) []byte {
bytes := make([]byte, len(str))
copy(bytes[:], str)
return bytes
}
func TestParseAnsiCommand(t *testing.T) {
// Note: if the parameter does not exist then the empty value is returned
c := parseAnsiCommand(StringToBytes("\x1Bm"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "" == c.getParam(0), "should return empty string")
assertTrue(t, "" == c.getParam(1), "should return empty string")
// Escape sequence - ESC[
c = parseAnsiCommand(StringToBytes("\x1B[m"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "" == c.getParam(0), "should return empty string")
assertTrue(t, "" == c.getParam(1), "should return empty string")
// Escape sequence With empty parameters- ESC[
c = parseAnsiCommand(StringToBytes("\x1B[;m"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "" == c.getParam(0), "should return empty string")
assertTrue(t, "" == c.getParam(1), "should return empty string")
assertTrue(t, "" == c.getParam(2), "should return empty string")
// Escape sequence With empty muliple parameters- ESC[
c = parseAnsiCommand(StringToBytes("\x1B[;;m"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "" == c.getParam(0), "")
assertTrue(t, "" == c.getParam(1), "")
assertTrue(t, "" == c.getParam(2), "")
// Escape sequence With muliple parameters- ESC[
c = parseAnsiCommand(StringToBytes("\x1B[1;2;3m"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "1" == c.getParam(0), "")
assertTrue(t, "2" == c.getParam(1), "")
assertTrue(t, "3" == c.getParam(2), "")
// Escape sequence With muliple parameters- some missing
c = parseAnsiCommand(StringToBytes("\x1B[1;;3;;;6m"))
assertTrue(t, c.Command == "m", "Command should be m")
assertTrue(t, "1" == c.getParam(0), "")
assertTrue(t, "" == c.getParam(1), "")
assertTrue(t, "3" == c.getParam(2), "")
assertTrue(t, "" == c.getParam(3), "")
assertTrue(t, "" == c.getParam(4), "")
assertTrue(t, "6" == c.getParam(5), "")
}
func newBufferedMockTerm() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser, mock *mockTerminal) {
var input bytes.Buffer
var output bytes.Buffer
var err bytes.Buffer
mock = &mockTerminal{
OutputCommandSequence: make([]terminalOperation, 0, 256),
}
stdOut = &terminalWriter{
wrappedWriter: &output,
emulator: mock,
command: make([]byte, 0, 256),
}
stdErr = &terminalWriter{
wrappedWriter: &err,
emulator: mock,
command: make([]byte, 0, 256),
}
stdIn = &terminalReader{
wrappedReader: ioutil.NopCloser(&input),
emulator: mock,
command: make([]byte, 0, 256),
}
return
}
func TestOutputSimple(t *testing.T) {
stdOut, _, _, mock := newBufferedMockTerm()
stdOut.Write(StringToBytes("Hello world"))
stdOut.Write(StringToBytes("\x1BmHello again"))
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
assertBytesEqual(t, StringToBytes("\x1Bm"), mock.OutputCommandSequence[1].Data, "Command data should match")
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
}
func TestOutputSplitCommand(t *testing.T) {
stdOut, _, _, mock := newBufferedMockTerm()
stdOut.Write(StringToBytes("Hello world\x1B[1;2;3"))
stdOut.Write(StringToBytes("mHello again"))
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
}
func TestOutputMultipleCommands(t *testing.T) {
stdOut, _, _, mock := newBufferedMockTerm()
stdOut.Write(StringToBytes("Hello world"))
stdOut.Write(StringToBytes("\x1B[1;2;3m"))
stdOut.Write(StringToBytes("\x1B[J"))
stdOut.Write(StringToBytes("Hello again"))
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
assertTrue(t, mock.OutputCommandSequence[2].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
assertBytesEqual(t, StringToBytes("\x1B[J"), mock.OutputCommandSequence[2].Data, "Command data should match")
assertTrue(t, mock.OutputCommandSequence[3].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[3].Data, "Write data should match")
}
// Splits the given data in two chunks , makes two writes and checks the split data is parsed correctly
// checks output write/command is passed to handler correctly
func helpsTestOutputSplitChunksAtIndex(t *testing.T, i int, data []byte) {
t.Logf("\ni=%d", i)
stdOut, _, _, mock := newBufferedMockTerm()
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
t.Logf("\nWriting chunk[1] == %s", string(data[i:]))
stdOut.Write(data[:i])
stdOut.Write(data[i:])
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, data[i:], mock.OutputCommandSequence[1].Data, "Write data should match")
}
// Splits the given data in three chunks , makes three writes and checks the split data is parsed correctly
// checks output write/command is passed to handler correctly
func helpsTestOutputSplitThreeChunksAtIndex(t *testing.T, data []byte, i int, j int) {
stdOut, _, _, mock := newBufferedMockTerm()
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
t.Logf("\nWriting chunk[1] == %s", string(data[i:j]))
t.Logf("\nWriting chunk[2] == %s", string(data[j:]))
stdOut.Write(data[:i])
stdOut.Write(data[i:j])
stdOut.Write(data[j:])
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, data[i:j], mock.OutputCommandSequence[1].Data, "Write data should match")
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
assertBytesEqual(t, data[j:], mock.OutputCommandSequence[2].Data, "Write data should match")
}
// Splits the output into two parts and tests all such possible pairs
func helpsTestOutputSplitChunks(t *testing.T, data []byte) {
for i := 1; i < len(data)-1; i++ {
helpsTestOutputSplitChunksAtIndex(t, i, data)
}
}
// Splits the output in three parts and tests all such possible triples
func helpsTestOutputSplitThreeChunks(t *testing.T, data []byte) {
for i := 1; i < len(data)-2; i++ {
for j := i + 1; j < len(data)-1; j++ {
helpsTestOutputSplitThreeChunksAtIndex(t, data, i, j)
}
}
}
func helpsTestOutputSplitCommandsAtIndex(t *testing.T, data []byte, i int, plainText string, commands ...string) {
t.Logf("\ni=%d", i)
stdOut, _, _, mock := newBufferedMockTerm()
stdOut.Write(data[:i])
stdOut.Write(data[i:])
assertHandlerOutput(t, mock, plainText, commands...)
}
func helpsTestOutputSplitCommands(t *testing.T, data []byte, plainText string, commands ...string) {
for i := 1; i < len(data)-1; i++ {
helpsTestOutputSplitCommandsAtIndex(t, data, i, plainText, commands...)
}
}
func injectCommandAt(data string, i int, command string) string {
retValue := make([]byte, len(data)+len(command)+4)
retValue = append(retValue, data[:i]...)
retValue = append(retValue, data[i:]...)
return string(retValue)
}
func TestOutputSplitChunks(t *testing.T) {
data := StringToBytes("qwertyuiopasdfghjklzxcvbnm")
helpsTestOutputSplitChunks(t, data)
helpsTestOutputSplitChunks(t, StringToBytes("BBBBB"))
helpsTestOutputSplitThreeChunks(t, StringToBytes("ABCDE"))
}
func TestOutputSplitChunksIncludingCommands(t *testing.T) {
helpsTestOutputSplitCommands(t, StringToBytes("Hello world.\x1B[mHello again."), "Hello world.Hello again.", "\x1B[m")
helpsTestOutputSplitCommandsAtIndex(t, StringToBytes("Hello world.\x1B[mHello again."), 2, "Hello world.Hello again.", "\x1B[m")
}
func TestSplitChunkUnicode(t *testing.T) {
for _, l := range languages {
data := StringToBytes(l)
helpsTestOutputSplitChunks(t, data)
helpsTestOutputSplitThreeChunks(t, data)
}
}
package timeutils
import (
"testing"
"time"
)
// Testing to ensure 'year' fields is between 0 and 9999
func TestFastMarshalJSONWithInvalidDate(t *testing.T) {
aTime := time.Date(-1, 1, 1, 0, 0, 0, 0, time.Local)
json, err := FastMarshalJSON(aTime)
if err == nil {
t.Fatalf("FastMarshalJSON should throw an error, but was '%v'", json)
}
anotherTime := time.Date(10000, 1, 1, 0, 0, 0, 0, time.Local)
json, err = FastMarshalJSON(anotherTime)
if err == nil {
t.Fatalf("FastMarshalJSON should throw an error, but was '%v'", json)
}
}
func TestFastMarshalJSON(t *testing.T) {
aTime := time.Date(2015, 5, 29, 11, 1, 2, 3, time.UTC)
json, err := FastMarshalJSON(aTime)
if err != nil {
t.Fatal(err)
}
expected := "\"2015-05-29T11:01:02.000000003Z\""
if json != expected {
t.Fatalf("Expected %v, got %v", expected, json)
}
location, err := time.LoadLocation("Europe/Paris")
if err != nil {
t.Fatal(err)
}
aTime = time.Date(2015, 5, 29, 11, 1, 2, 3, location)
json, err = FastMarshalJSON(aTime)
if err != nil {
t.Fatal(err)
}
expected = "\"2015-05-29T11:01:02.000000003+02:00\""
if json != expected {
t.Fatalf("Expected %v, got %v", expected, json)
}
}
...@@ -6,10 +6,17 @@ import ( ...@@ -6,10 +6,17 @@ import (
"time" "time"
) )
// GetTimestamp tries to parse given string as RFC3339 time // GetTimestamp tries to parse given string as golang duration,
// or Unix timestamp (with seconds precision), if successful // then RFC3339 time and finally as a Unix timestamp. If
//returns a Unix timestamp as string otherwise returns value back. // any of these were successful, it returns a Unix timestamp
func GetTimestamp(value string) string { // as string otherwise returns the given value back.
// In case of duration input, the returned timestamp is computed
// as the given reference time minus the amount of the duration.
func GetTimestamp(value string, reference time.Time) string {
if d, err := time.ParseDuration(value); value != "0" && err == nil {
return strconv.FormatInt(reference.Add(-d).Unix(), 10)
}
var format string var format string
if strings.Contains(value, ".") { if strings.Contains(value, ".") {
format = time.RFC3339Nano format = time.RFC3339Nano
......
package timeutils package timeutils
import ( import (
"fmt"
"testing" "testing"
"time"
) )
func TestGetTimestamp(t *testing.T) { func TestGetTimestamp(t *testing.T) {
now := time.Now()
cases := []struct{ in, expected string }{ cases := []struct{ in, expected string }{
{"0", "-62167305600"}, // 0 gets parsed year 0 {"0", "-62167305600"}, // 0 gets parsed year 0
...@@ -23,12 +26,17 @@ func TestGetTimestamp(t *testing.T) { ...@@ -23,12 +26,17 @@ func TestGetTimestamp(t *testing.T) {
// unix timestamps returned as is // unix timestamps returned as is
{"1136073600", "1136073600"}, {"1136073600", "1136073600"},
// Durations
{"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix())},
{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix())},
{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix())},
// String fallback // String fallback
{"invalid", "invalid"}, {"invalid", "invalid"},
} }
for _, c := range cases { for _, c := range cases {
o := GetTimestamp(c.in) o := GetTimestamp(c.in, now)
if o != c.expected { if o != c.expected {
t.Fatalf("wrong value for '%s'. expected:'%s' got:'%s'", c.in, c.expected, o) t.Fatalf("wrong value for '%s'. expected:'%s' got:'%s'", c.in, c.expected, o)
} }
......
...@@ -27,5 +27,5 @@ func HumanDuration(d time.Duration) string { ...@@ -27,5 +27,5 @@ func HumanDuration(d time.Duration) string {
} else if hours < 24*365*2 { } else if hours < 24*365*2 {
return fmt.Sprintf("%d months", hours/24/30) return fmt.Sprintf("%d months", hours/24/30)
} }
return fmt.Sprintf("%f years", d.Hours()/24/365) return fmt.Sprintf("%d years", int(d.Hours())/24/365)
} }
...@@ -41,6 +41,6 @@ func TestHumanDuration(t *testing.T) { ...@@ -41,6 +41,6 @@ func TestHumanDuration(t *testing.T) {
assertEquals(t, "13 months", HumanDuration(13*month)) assertEquals(t, "13 months", HumanDuration(13*month))
assertEquals(t, "23 months", HumanDuration(23*month)) assertEquals(t, "23 months", HumanDuration(23*month))
assertEquals(t, "24 months", HumanDuration(24*month)) assertEquals(t, "24 months", HumanDuration(24*month))
assertEquals(t, "2.010959 years", HumanDuration(24*month+2*week)) assertEquals(t, "2 years", HumanDuration(24*month+2*week))
assertEquals(t, "3.164384 years", HumanDuration(3*year+2*month)) assertEquals(t, "3 years", HumanDuration(3*year+2*month))
} }
...@@ -37,23 +37,25 @@ var ( ...@@ -37,23 +37,25 @@ var (
var decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} var decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
var binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} var binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
// CustomSize returns a human-readable approximation of a size
// using custom format
func CustomSize(format string, size float64, base float64, _map []string) string {
i := 0
for size >= base {
size = size / base
i++
}
return fmt.Sprintf(format, size, _map[i])
}
// HumanSize returns a human-readable approximation of a size // HumanSize returns a human-readable approximation of a size
// using SI standard (eg. "44kB", "17MB") // using SI standard (eg. "44kB", "17MB")
func HumanSize(size float64) string { func HumanSize(size float64) string {
return intToString(float64(size), 1000.0, decimapAbbrs) return CustomSize("%.4g %s", float64(size), 1000.0, decimapAbbrs)
} }
func BytesSize(size float64) string { func BytesSize(size float64) string {
return intToString(size, 1024.0, binaryAbbrs) return CustomSize("%.4g %s", size, 1024.0, binaryAbbrs)
}
func intToString(size, unit float64, _map []string) string {
i := 0
for size >= unit {
size = size / unit
i++
}
return fmt.Sprintf("%.4g %s", size, _map[i])
} }
// FromHumanSize returns an integer from a human-readable specification of a // FromHumanSize returns an integer from a human-readable specification of a
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment