Commit 2daed14a authored by Amos Wenger's avatar Amos Wenger

Original import

parents
Pipeline #10305 failed with stage
in 23 seconds
stages:
- test
test:linux:
stage: test
tags:
- linux
script:
- scripts/ci.sh
test:windows:
stage: test
tags:
- windows
script:
- scripts/ci.sh
test:darwin:
stage: test
tags:
- darwin
script:
- scripts/ci.sh
MIT LICENSE
Copyright (c) 2018 Leaf Corcoran and Amos Wenger, https://itch.io
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ox
[![build status](https://git.itch.ovh/itchio/ox/badges/master/build.svg)](https://git.itch.ovh/itchio/ox/commits/master)
[![codecov](https://codecov.io/gh/itchio/ox/branch/master/graph/badge.svg)](https://codecov.io/gh/itchio/ox)
[![Go Report Card](https://goreportcard.com/badge/github.com/itchio/ox)](https://goreportcard.com/report/github.com/itchio/ox)
[![GoDoc](https://godoc.org/github.com/itchio/ox?status.svg)](https://godoc.org/github.com/itchio/ox)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchio/ox/blob/master/LICENSE)
ox contains:
* Package `syscallex`: the missing parts of `syscall`
* Package `winox`: convenient wrappers for some Win32 APIs
* Package `macox`: convenient wrappers for some Cocoa APIs
## License
Licensed under MIT License, see `LICENSE` for details.
// +build darwin
package macox
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#import <Cocoa/Cocoa.h>
#include <stdlib.h>
char *GetExecutablePath(char *cBundlePath) {
NSString* bundlePath = [NSString stringWithUTF8String:cBundlePath];
if (!bundlePath) {
return 0;
}
NSBundle* bundle = [NSBundle bundleWithPath:bundlePath];
if (!bundle) {
return 0;
}
const char *tempString = [[bundle executablePath] UTF8String];
char *ret = malloc(strlen(tempString) + 1);
memcpy(ret, tempString, strlen(tempString) + 1);
return ret;
}
char *GetLibraryPath() {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
for (NSString* path in paths) {
const char *tempString = [path UTF8String];
char *ret = malloc(strlen(tempString) + 1);
memcpy(ret, tempString, strlen(tempString) + 1);
return ret;
}
return 0;
}
char *GetApplicationSupportPath() {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
for (NSString* path in paths) {
const char *tempString = [path UTF8String];
char *ret = malloc(strlen(tempString) + 1);
memcpy(ret, tempString, strlen(tempString) + 1);
return ret;
}
return 0;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func GetExecutablePath(bundlePath string) (string, error) {
cPath := C.GetExecutablePath(C.CString(bundlePath))
if uintptr(unsafe.Pointer(cPath)) == 0 {
return "", fmt.Errorf("Could not get executable path for app bundle (%s)", bundlePath)
}
defer C.free(unsafe.Pointer(cPath))
return C.GoString(cPath), nil
}
func GetLibraryPath() (string, error) {
cPath := C.GetLibraryPath()
if uintptr(unsafe.Pointer(cPath)) == 0 {
return "", fmt.Errorf("Could not get library path")
}
defer C.free(unsafe.Pointer(cPath))
return C.GoString(cPath), nil
}
func GetApplicationSupportPath() (string, error) {
cPath := C.GetApplicationSupportPath()
if uintptr(unsafe.Pointer(cPath)) == 0 {
return "", fmt.Errorf("Could not get application support path")
}
defer C.free(unsafe.Pointer(cPath))
return C.GoString(cPath), nil
}
package macox_test
import (
"testing"
"github.com/itchio/ox/macox"
"github.com/stretchr/testify/assert"
)
func Test_GetLibraryPath(t *testing.T) {
{
s, err := macox.GetExecutablePath("/Applications/TextEdit.app")
assert.NoError(t, err)
assert.NotEmpty(t, s)
}
{
s, err := macox.GetLibraryPath()
assert.NoError(t, err)
assert.NotEmpty(t, s)
}
{
s, err := macox.GetApplicationSupportPath()
assert.NoError(t, err)
assert.NotEmpty(t, s)
}
}
#!/bin/sh -xe
go version
export GOPATH=$PWD/gopath
rm -rf $GOPATH
export PKG=github.com/itchio/ox
export PATH=$PATH:$GOPATH/bin
mkdir -p $GOPATH/src/$PKG
rsync -a --exclude 'src' . $GOPATH/src/$PKG
go get -v -d -t $PKG/...
go test -v -cover -coverprofile=coverage.txt -race $PKG/...
curl -s https://codecov.io/bash | bash
This diff is collapsed.
package syscallex
import (
"syscall"
"unicode/utf16"
"unsafe"
)
// makeCmdLine builds a command line out of args by escaping "special"
// characters and joining the arguments with spaces.
func makeCmdLine(args []string) string {
var s string
for _, v := range args {
if s != "" {
s += " "
}
s += syscall.EscapeArg(v)
}
return s
}
// createEnvBlock converts an array of environment strings into
// the representation required by CreateProcess: a sequence of NUL
// terminated strings followed by a nil.
// Last bytes are two UCS-2 NULs, or four NUL bytes.
func createEnvBlock(envv []string) *uint16 {
if len(envv) == 0 {
return &utf16.Encode([]rune("\x00\x00"))[0]
}
length := 0
for _, s := range envv {
length += len(s) + 1
}
length += 1
b := make([]byte, length)
i := 0
for _, s := range envv {
l := len(s)
copy(b[i:i+l], []byte(s))
copy(b[i+l:i+l+1], []byte{0})
i = i + l + 1
}
copy(b[i:i+1], []byte{0})
return &utf16.Encode([]rune(string(b)))[0]
}
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
func normalizeDir(dir string) (name string, err error) {
ndir, err := syscall.FullPath(dir)
if err != nil {
return "", err
}
if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
// dir cannot have \\server\share\path form
return "", syscall.EINVAL
}
return ndir, nil
}
func volToUpper(ch int) int {
if 'a' <= ch && ch <= 'z' {
ch += 'A' - 'a'
}
return ch
}
func joinExeDirAndFName(dir, p string) (name string, err error) {
if len(p) == 0 {
return "", syscall.EINVAL
}
if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
// \\server\share\path form
return p, nil
}
if len(p) > 1 && p[1] == ':' {
// has drive letter
if len(p) == 2 {
return "", syscall.EINVAL
}
if isSlash(p[2]) {
return p, nil
} else {
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
return syscall.FullPath(d + "\\" + p[2:])
} else {
return syscall.FullPath(p)
}
}
} else {
// no drive letter
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if isSlash(p[0]) {
return syscall.FullPath(d[:2] + p)
} else {
return syscall.FullPath(d + "\\" + p)
}
}
}
type ProcAttr struct {
Dir string
Env []string
Files []uintptr
Sys *SysProcAttr
}
type SysProcAttr struct {
HideWindow bool
CmdLine string // used if non-empty, else the windows command line is built by escaping the arguments passed to StartProcess
CreationFlags uint32
LogonFlags uint32
ProcessHandle syscall.Handle
ThreadHandle syscall.Handle
}
var zeroProcAttr ProcAttr
var zeroSysProcAttr SysProcAttr
func StartProcessWithLogon(argv0 string, argv []string, username string, domain string, password string, attr *ProcAttr) (pid int, handle uintptr, err error) {
if len(argv0) == 0 {
return 0, 0, syscall.EWINDOWS
}
if attr == nil {
attr = &zeroProcAttr
}
sys := attr.Sys
if sys == nil {
sys = &zeroSysProcAttr
}
if len(attr.Files) > 3 {
return 0, 0, syscall.EWINDOWS
}
if len(attr.Files) < 3 {
return 0, 0, syscall.EINVAL
}
if len(attr.Dir) != 0 {
// StartProcess assumes that argv0 is relative to attr.Dir,
// because it implies Chdir(attr.Dir) before executing argv0.
// Windows CreateProcess assumes the opposite: it looks for
// argv0 relative to the current directory, and, only once the new
// process is started, it does Chdir(attr.Dir). We are adjusting
// for that difference here by making argv0 absolute.
var err error
argv0, err = joinExeDirAndFName(attr.Dir, argv0)
if err != nil {
return 0, 0, err
}
}
argv0p, err := syscall.UTF16PtrFromString(argv0)
if err != nil {
return 0, 0, err
}
var cmdline string
// Windows CreateProcess takes the command line as a single string:
// use attr.CmdLine if set, else build the command line by escaping
// and joining each argument with spaces
if sys.CmdLine != "" {
cmdline = sys.CmdLine
} else {
cmdline = makeCmdLine(argv)
}
var argvp *uint16
if len(cmdline) != 0 {
argvp, err = syscall.UTF16PtrFromString(cmdline)
if err != nil {
return 0, 0, err
}
}
var dirp *uint16
if len(attr.Dir) != 0 {
dirp, err = syscall.UTF16PtrFromString(attr.Dir)
if err != nil {
return 0, 0, err
}
}
// Acquire the fork lock so that no other threads
// create new fds that are not yet close-on-exec
// before we fork.
syscall.ForkLock.Lock()
defer syscall.ForkLock.Unlock()
p, _ := syscall.GetCurrentProcess()
fd := make([]syscall.Handle, len(attr.Files))
for i := range attr.Files {
if attr.Files[i] > 0 {
err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
if err != nil {
return 0, 0, err
}
defer syscall.CloseHandle(syscall.Handle(fd[i]))
}
}
si := new(syscall.StartupInfo)
si.Cb = uint32(unsafe.Sizeof(*si))
si.Flags = syscall.STARTF_USESTDHANDLES
if sys.HideWindow {
si.Flags |= syscall.STARTF_USESHOWWINDOW
si.ShowWindow = syscall.SW_HIDE
}
si.StdInput = fd[0]
si.StdOutput = fd[1]
si.StdErr = fd[2]
pi := new(syscall.ProcessInformation)
if username == "" {
creationFlags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
err = syscall.CreateProcess(
argv0p, argvp, nil, nil, true, creationFlags, createEnvBlock(attr.Env), dirp, si, pi,
)
if err != nil {
return 0, 0, err
}
} else {
usernamep := syscall.StringToUTF16Ptr(username)
domainp := syscall.StringToUTF16Ptr(domain)
passwordp := syscall.StringToUTF16Ptr(password)
creationFlags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
logonFlags := sys.LogonFlags
err = CreateProcessWithLogon(
usernamep, domainp, passwordp, logonFlags,
argv0p, argvp, creationFlags, createEnvBlock(attr.Env), dirp, si, pi,
)
if err != nil {
return 0, 0, err
}
}
attr.Sys.ThreadHandle = syscall.Handle(pi.Thread)
attr.Sys.ProcessHandle = syscall.Handle(pi.Process)
return int(pi.ProcessId), uintptr(pi.Process), nil
}
package syscallex
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// JobObjectInfoClass
// cf. https://msdn.microsoft.com/en-us/library/windows/desktop/ms686216%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
const (
JobObjectInfoClass_JobObjectBasicProcessIdList = 3
JobObjectInfoClass_JobObjectAssociateCompletionPortInformation = 7
JobObjectInfoClass_JobObjectExtendedLimitInformation = 9
)
// JobObjectBasicLimitInformation.LimitFlags
const (
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
)
// job object completion statuses, thanks wine!
// cf. https://www.winehq.org/pipermail/wine-cvs/2013-October/097834.html
const (
JOB_OBJECT_MSG_END_OF_JOB_TIME = 1
JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4
JOB_OBJECT_MSG_NEW_PROCESS = 6
JOB_OBJECT_MSG_EXIT_PROCESS = 7
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10
)
type JobObjectAssociateCompletionPort struct {
CompletionKey syscall.Handle
CompletionPort syscall.Handle
}
const (
CREATE_SUSPENDED = 0x00000004
PROCESS_ALL_ACCESS = syscall.STANDARD_RIGHTS_REQUIRED | syscall.SYNCHRONIZE | 0xfff
THREAD_SUSPEND_RESUME = 0x0002
TH32CS_SNAPPROCESS = 0x00000002
)
type ThreadEntry32 struct {
Size uint32
TUsage uint32
ThreadID uint32
OwnerProcessID uint32
BasePri int32
DeltaPri int32
Flags uint32
}
type ProcessEntry32 struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriorityClassBase int32
Flags uint32
ExeFile [MAX_PATH]uint16
}
var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procCreateJobObject = modkernel32.NewProc("CreateJobObjectW")
procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject")
procQueryInformationJobObject = modkernel32.NewProc("QueryInformationJobObject")
procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procOpenThreadToken = modkernel32.NewProc("OpenThreadToken")
procGetDiskFreeSpaceExW = modkernel32.NewProc("GetDiskFreeSpaceExW")
procOpenThread = modkernel32.NewProc("OpenThread")
procResumeThread = modkernel32.NewProc("ResumeThread")
procThread32First = modkernel32.NewProc("Thread32First")
procThread32Next = modkernel32.NewProc("Thread32Next")
procCreateToolhelp32Snapshot = modkernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32FirstW = modkernel32.NewProc("Process32FirstW")
procProcess32NextW = modkernel32.NewProc("Process32NextW")
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
)
func CreateJobObject(
jobAttributes *syscall.SecurityAttributes,
name *uint16,
) (handle syscall.Handle, err error) {
r1, _, e1 := syscall.Syscall(
procCreateJobObject.Addr(),
2,
uintptr(unsafe.Pointer(jobAttributes)),
uintptr(unsafe.Pointer(name)),
0,
)
handle = syscall.Handle(r1)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}
}
return
}
type IoCounters struct {
ReadOperationCount uint64
WriteOperationCount uint64
OtherOperationCount uint64
ReadTransferCount uint64
WriteTransferCount uint64
OtherTransferCount uint64
}
func SetInformationJobObject(
jobObject syscall.Handle,
jobObjectInfoClass uint32,
jobObjectInfo uintptr,
jobObjectInfoLength uintptr,
) (err error) {
r1, _, e1 := syscall.Syscall6(
procSetInformationJobObject.Addr(),
4,
uintptr(jobObject),
uintptr(jobObjectInfoClass),
jobObjectInfo,
jobObjectInfoLength,
0, 0,
)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}
}
return
}
func QueryInformationJobObject(
jobObject syscall.Handle,
jobObjectInfoClass uint32,
jobObjectInfo uintptr,
jobObjectInfoLength uintptr,
returnLength uintptr,
) (err error) {
r1, _, e1 := syscall.Syscall6(
procQueryInformationJobObject.Addr(),
5,
uintptr(jobObject),
uintptr(jobObjectInfoClass),
jobObjectInfo,
jobObjectInfoLength,
returnLength,
0,
)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}
}
return
}
func AssignProcessToJobObject(
jobObject syscall.Handle,
process syscall.Handle,
) (err error) {
r1, _, e1 := syscall.Syscall(
procAssignProcessToJobObject.Addr(),
2,
uintptr(jobObject),
uintptr(process),
0,
)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}
}
return
}
func GetCurrentThread() syscall.Handle {
r1, _, _ := syscall.Syscall(
procGetCurrentThread.Addr(),
0,
0, 0, 0,
)
return syscall.Handle(r1)
}
func OpenThreadToken(
threadHandle syscall.Handle,
desiredAccess uint32,
openAsSelf uint32,
tokenHandle *syscall.Token,
) (err error) {
r1, _, e1 := syscall.Syscall6(
procOpenThreadToken.Addr(),
4,
uintptr(threadHandle),
uintptr(desiredAccess),
uintptr(openAsSelf),
uintptr(unsafe.Pointer(tokenHandle)),
0, 0,
)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}
}
return
}
type DiskFreeSpace struct {
FreeBytesAvailable uint64
TotalNumberOfBytes uint64
TotalNumberOfFreeBytes uint64
}
func GetDiskFreeSpaceEx(path *uint16) (dfs *DiskFreeSpace, err error) {
var buf DiskFreeSpace
dfs = &buf
r1, _, e1 := syscall.Syscall6(
procGetDiskFreeSpaceExW.Addr(),
4,
uintptr(unsafe.Pointer(path)),
uintptr(unsafe.Pointer(&buf.FreeBytesAvailable)),
uintptr(unsafe.Pointer(&buf.TotalNumberOfBytes)),
uintptr(unsafe.Pointer(&buf.TotalNumberOfFreeBytes)),
0, 0,
)
if r1 == 0 {
if e1 != 0 {
err = e1
} else {
err = syscall.EINVAL
}