parent
6f18d35abf
commit
184295c57e
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"remotePath": "/usr/local/gopath/src/github.com/soopsio/gosuv/",
|
||||||
|
"host": "10.10.99.177",
|
||||||
|
"username": "root",
|
||||||
|
"password": "111111",
|
||||||
|
"port": 22,
|
||||||
|
"secure": false,
|
||||||
|
"protocol": "sftp",
|
||||||
|
"uploadOnSave": true,
|
||||||
|
"passive": false,
|
||||||
|
"debug": true,
|
||||||
|
"privateKeyPath": null,
|
||||||
|
"passphrase": null,
|
||||||
|
"ignore": [
|
||||||
|
"\\.vscode",
|
||||||
|
"\\.git",
|
||||||
|
"\\.DS_Store"
|
||||||
|
],
|
||||||
|
"generatedFiles": {
|
||||||
|
"uploadOnSave": false,
|
||||||
|
"extensionsToInclude": [],
|
||||||
|
"path": ""
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
// +build !vfs
|
// +build !vfs
|
||||||
//go:generate go run assets_generate.go
|
//go:generate go run assets_generate.go
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
// Assets contains project assets.
|
// Assets contains project assets.
|
||||||
var Assets http.FileSystem = http.Dir("res")
|
var Assets http.FileSystem = http.Dir("res")
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
// +build ignore
|
// +build ignore
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/shurcooL/vfsgen"
|
"github.com/shurcooL/vfsgen"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var fs http.FileSystem = http.Dir("res")
|
var fs http.FileSystem = http.Dir("res")
|
||||||
|
|
||||||
err := vfsgen.Generate(fs, vfsgen.Options{
|
err := vfsgen.Generate(fs, vfsgen.Options{
|
||||||
PackageName: "main",
|
PackageName: "main",
|
||||||
BuildTags: "vfs",
|
BuildTags: "vfs",
|
||||||
VariableName: "Assets",
|
VariableName: "Assets",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,193 +1,193 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glycerine/rbuf"
|
"github.com/glycerine/rbuf"
|
||||||
"github.com/qiniu/log"
|
"github.com/qiniu/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The new broadcast
|
// The new broadcast
|
||||||
type StreamWriter struct {
|
type StreamWriter struct {
|
||||||
wc io.WriteCloser
|
wc io.WriteCloser
|
||||||
stream string
|
stream string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WriteBroadcaster struct {
|
type WriteBroadcaster struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
buf *rbuf.FixedSizeRingBuf
|
buf *rbuf.FixedSizeRingBuf
|
||||||
writers map[StreamWriter]bool
|
writers map[StreamWriter]bool
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWriteBroadcaster(size int) *WriteBroadcaster {
|
func NewWriteBroadcaster(size int) *WriteBroadcaster {
|
||||||
if size <= 0 {
|
if size <= 0 {
|
||||||
size = 4 * 1024
|
size = 4 * 1024
|
||||||
}
|
}
|
||||||
bc := &WriteBroadcaster{
|
bc := &WriteBroadcaster{
|
||||||
writers: make(map[StreamWriter]bool),
|
writers: make(map[StreamWriter]bool),
|
||||||
buf: rbuf.NewFixedSizeRingBuf(size),
|
buf: rbuf.NewFixedSizeRingBuf(size),
|
||||||
closed: false,
|
closed: false,
|
||||||
}
|
}
|
||||||
return bc
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wb *WriteBroadcaster) Closed() bool {
|
func (wb *WriteBroadcaster) Closed() bool {
|
||||||
return wb.closed
|
return wb.closed
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is main func
|
// this is main func
|
||||||
func (wb *WriteBroadcaster) NewChanString(name string) chan string {
|
func (wb *WriteBroadcaster) NewChanString(name string) chan string {
|
||||||
wb.Lock()
|
wb.Lock()
|
||||||
defer wb.Unlock()
|
defer wb.Unlock()
|
||||||
|
|
||||||
wr := NewChanStrWriter()
|
wr := NewChanStrWriter()
|
||||||
if wb.closed {
|
if wb.closed {
|
||||||
wr.Close()
|
wr.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sw := StreamWriter{wc: wr, stream: name}
|
sw := StreamWriter{wc: wr, stream: name}
|
||||||
wb.writers[sw] = true
|
wb.writers[sw] = true
|
||||||
wr.Write(wb.buf.Bytes())
|
wr.Write(wb.buf.Bytes())
|
||||||
return wr.C
|
return wr.C
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wb *WriteBroadcaster) Bytes() []byte {
|
func (wb *WriteBroadcaster) Bytes() []byte {
|
||||||
return wb.buf.Bytes()
|
return wb.buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
||||||
w.Lock()
|
w.Lock()
|
||||||
defer w.Unlock()
|
defer w.Unlock()
|
||||||
|
|
||||||
// write with advance
|
// write with advance
|
||||||
w.buf.WriteAndMaybeOverwriteOldestData(p)
|
w.buf.WriteAndMaybeOverwriteOldestData(p)
|
||||||
|
|
||||||
for sw := range w.writers {
|
for sw := range w.writers {
|
||||||
// set write timeout
|
// set write timeout
|
||||||
err = GoTimeout(func() error {
|
err = GoTimeout(func() error {
|
||||||
if _, err := sw.wc.Write(p); err != nil { //|| n != len(p) {
|
if _, err := sw.wc.Write(p); err != nil { //|| n != len(p) {
|
||||||
return errors.New("broadcast to " + sw.stream + " error")
|
return errors.New("broadcast to " + sw.stream + " error")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, time.Second*1)
|
}, time.Second*1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// On error, evict the writer
|
// On error, evict the writer
|
||||||
log.Warnf("broadcase write error: %s, %s", sw.stream, err)
|
log.Warnf("broadcase write error: %s, %s", sw.stream, err)
|
||||||
sw.wc.Close()
|
sw.wc.Close()
|
||||||
delete(w.writers, sw)
|
delete(w.writers, sw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WriteBroadcaster) CloseWriter(name string) {
|
func (w *WriteBroadcaster) CloseWriter(name string) {
|
||||||
for sw := range w.writers {
|
for sw := range w.writers {
|
||||||
if sw.stream == name {
|
if sw.stream == name {
|
||||||
sw.wc.Close()
|
sw.wc.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WriteBroadcaster) CloseWriters() error {
|
func (w *WriteBroadcaster) CloseWriters() error {
|
||||||
w.Lock()
|
w.Lock()
|
||||||
defer w.Unlock()
|
defer w.Unlock()
|
||||||
for sw := range w.writers {
|
for sw := range w.writers {
|
||||||
sw.wc.Close()
|
sw.wc.Close()
|
||||||
}
|
}
|
||||||
w.writers = make(map[StreamWriter]bool)
|
w.writers = make(map[StreamWriter]bool)
|
||||||
w.closed = true
|
w.closed = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// // nop writer
|
// // nop writer
|
||||||
// type NopWriter struct{}
|
// type NopWriter struct{}
|
||||||
|
|
||||||
// func (*NopWriter) Write(buf []byte) (int, error) {
|
// func (*NopWriter) Write(buf []byte) (int, error) {
|
||||||
// return len(buf), nil
|
// return len(buf), nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// type nopWriteCloser struct {
|
// type nopWriteCloser struct {
|
||||||
// io.Writer
|
// io.Writer
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// func (w *nopWriteCloser) Close() error { return nil }
|
// func (w *nopWriteCloser) Close() error { return nil }
|
||||||
|
|
||||||
// func NopWriteCloser(w io.Writer) io.WriteCloser {
|
// func NopWriteCloser(w io.Writer) io.WriteCloser {
|
||||||
// return &nopWriteCloser{w}
|
// return &nopWriteCloser{w}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// chan string writer
|
// chan string writer
|
||||||
type chanStrWriter struct {
|
type chanStrWriter struct {
|
||||||
C chan string
|
C chan string
|
||||||
closed bool
|
closed bool
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chanStrWriter) Write(data []byte) (n int, err error) {
|
func (c *chanStrWriter) Write(data []byte) (n int, err error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if c.closed {
|
if c.closed {
|
||||||
return 0, errors.New("chan writer closed")
|
return 0, errors.New("chan writer closed")
|
||||||
}
|
}
|
||||||
c.C <- string(data) // write timeout
|
c.C <- string(data) // write timeout
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chanStrWriter) Close() error {
|
func (c *chanStrWriter) Close() error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if !c.closed {
|
if !c.closed {
|
||||||
c.closed = true
|
c.closed = true
|
||||||
close(c.C)
|
close(c.C)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChanStrWriter() *chanStrWriter {
|
func NewChanStrWriter() *chanStrWriter {
|
||||||
return &chanStrWriter{
|
return &chanStrWriter{
|
||||||
C: make(chan string, 10),
|
C: make(chan string, 10),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// quick loss writer
|
// quick loss writer
|
||||||
type QuickLossBroadcastWriter struct {
|
type QuickLossBroadcastWriter struct {
|
||||||
*WriteBroadcaster
|
*WriteBroadcaster
|
||||||
bufC chan string
|
bufC chan string
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *QuickLossBroadcastWriter) Write(buf []byte) (int, error) {
|
func (w *QuickLossBroadcastWriter) Write(buf []byte) (int, error) {
|
||||||
select {
|
select {
|
||||||
case w.bufC <- string(buf):
|
case w.bufC <- string(buf):
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return len(buf), nil
|
return len(buf), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *QuickLossBroadcastWriter) Close() error {
|
func (w *QuickLossBroadcastWriter) Close() error {
|
||||||
if !w.closed {
|
if !w.closed {
|
||||||
w.closed = true
|
w.closed = true
|
||||||
close(w.bufC)
|
close(w.bufC)
|
||||||
w.WriteBroadcaster.CloseWriters()
|
w.WriteBroadcaster.CloseWriters()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *QuickLossBroadcastWriter) drain() {
|
func (w *QuickLossBroadcastWriter) drain() {
|
||||||
for data := range w.bufC {
|
for data := range w.bufC {
|
||||||
w.WriteBroadcaster.Write([]byte(data))
|
w.WriteBroadcaster.Write([]byte(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuickLossBroadcastWriter(size int) *QuickLossBroadcastWriter {
|
func NewQuickLossBroadcastWriter(size int) *QuickLossBroadcastWriter {
|
||||||
qlw := &QuickLossBroadcastWriter{
|
qlw := &QuickLossBroadcastWriter{
|
||||||
WriteBroadcaster: NewWriteBroadcaster(size),
|
WriteBroadcaster: NewWriteBroadcaster(size),
|
||||||
bufC: make(chan string, 20),
|
bufC: make(chan string, 20),
|
||||||
}
|
}
|
||||||
go qlw.drain()
|
go qlw.drain()
|
||||||
return qlw
|
return qlw
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,56 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// func TestBroadcast(t *testing.T) {
|
// func TestBroadcast(t *testing.T) {
|
||||||
// bs := NewBroadcastString()
|
// bs := NewBroadcastString()
|
||||||
// bs.WriteMessage("hello")
|
// bs.WriteMessage("hello")
|
||||||
// time.Sleep(10 * time.Millisecond)
|
// time.Sleep(10 * time.Millisecond)
|
||||||
// c1 := bs.AddListener(nil)
|
// c1 := bs.AddListener(nil)
|
||||||
// go func() {
|
// go func() {
|
||||||
// bs.WriteMessage("world")
|
// bs.WriteMessage("world")
|
||||||
// }()
|
// }()
|
||||||
// message := <-c1
|
// message := <-c1
|
||||||
// if message != "world" {
|
// if message != "world" {
|
||||||
// t.Fatalf("expect message world, but got %s", message)
|
// t.Fatalf("expect message world, but got %s", message)
|
||||||
// }
|
// }
|
||||||
// c2 := bs.AddListener(nil)
|
// c2 := bs.AddListener(nil)
|
||||||
// go func() {
|
// go func() {
|
||||||
// bs.WriteMessage("tab")
|
// bs.WriteMessage("tab")
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
// // test write multi
|
// // test write multi
|
||||||
// wg := sync.WaitGroup{}
|
// wg := sync.WaitGroup{}
|
||||||
// wg.Add(2)
|
// wg.Add(2)
|
||||||
// go func() {
|
// go func() {
|
||||||
// message = <-c2
|
// message = <-c2
|
||||||
// if message != "tab" {
|
// if message != "tab" {
|
||||||
// t.Errorf("expect tab, but got %s", message)
|
// t.Errorf("expect tab, but got %s", message)
|
||||||
// }
|
// }
|
||||||
// wg.Done()
|
// wg.Done()
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
// message = <-c1
|
// message = <-c1
|
||||||
// if message != "tab" {
|
// if message != "tab" {
|
||||||
// t.Errorf("expect tab, but got %s", message)
|
// t.Errorf("expect tab, but got %s", message)
|
||||||
// }
|
// }
|
||||||
// wg.Done()
|
// wg.Done()
|
||||||
// }()
|
// }()
|
||||||
// wg.Wait()
|
// wg.Wait()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func TestRingBuffer(t *testing.T) {
|
func TestRingBuffer(t *testing.T) {
|
||||||
Convey("Write some string to ring buffer", t, func() {
|
Convey("Write some string to ring buffer", t, func() {
|
||||||
// buf := rbuf.NewFixedSizeRingBuf(5)
|
// buf := rbuf.NewFixedSizeRingBuf(5)
|
||||||
// buf.Write([]byte("abcde"))
|
// buf.Write([]byte("abcde"))
|
||||||
// So(string(buf.Bytes()), ShouldEqual, "abcde")
|
// So(string(buf.Bytes()), ShouldEqual, "abcde")
|
||||||
// buf.Advance(2)
|
// buf.Advance(2)
|
||||||
// buf.Write([]byte("fg"))
|
// buf.Write([]byte("fg"))
|
||||||
// So(string(buf.Bytes()), ShouldEqual, "cdefg")
|
// So(string(buf.Bytes()), ShouldEqual, "cdefg")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,48 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-yaml/yaml"
|
"github.com/go-yaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Server struct {
|
Server struct {
|
||||||
HttpAuth struct {
|
HttpAuth struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
User string `yaml:"username"`
|
User string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
} `yaml:"httpauth"`
|
} `yaml:"httpauth"`
|
||||||
Addr string `yaml:"addr"`
|
Addr string `yaml:"addr"`
|
||||||
} `yaml:"server"`
|
Name string `yaml:"name"`
|
||||||
|
} `yaml:"server,omitempty"`
|
||||||
Client struct {
|
Notifications Notifications `yaml:"notifications,omitempty" json:"-"`
|
||||||
ServerURL string `yaml:"server_url"`
|
|
||||||
}
|
Client struct {
|
||||||
}
|
ServerURL string `yaml:"server_url"`
|
||||||
|
}
|
||||||
func readConf(filename string) (c Configuration, err error) {
|
}
|
||||||
// initial default value
|
|
||||||
c.Server.Addr = ":11313" // in memory of 08-31 13:13
|
func readConf(filename string) (c Configuration, err error) {
|
||||||
c.Client.ServerURL = "http://localhost:11313"
|
// initial default value
|
||||||
|
c.Server.Addr = ":11313" // in memory of 08-31 13:13
|
||||||
data, err := ioutil.ReadFile(filename)
|
c.Client.ServerURL = "http://localhost:11313"
|
||||||
if err != nil {
|
|
||||||
data = []byte("")
|
data, err := ioutil.ReadFile(filename)
|
||||||
}
|
if err != nil {
|
||||||
err = yaml.Unmarshal(data, &c)
|
data = []byte("")
|
||||||
if err != nil {
|
}
|
||||||
return
|
err = yaml.Unmarshal(data, &c)
|
||||||
}
|
if err != nil {
|
||||||
cfgDir := filepath.Dir(filename)
|
return
|
||||||
if !IsDir(cfgDir) {
|
}
|
||||||
os.MkdirAll(cfgDir, 0755)
|
cfgDir := filepath.Dir(filename)
|
||||||
}
|
if !IsDir(cfgDir) {
|
||||||
data, _ = yaml.Marshal(c)
|
os.MkdirAll(cfgDir, 0755)
|
||||||
err = ioutil.WriteFile(filename, data, 0644)
|
}
|
||||||
return
|
data, _ = yaml.Marshal(c)
|
||||||
}
|
err = ioutil.WriteFile(filename, data, 0644)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findProcess(name string) bool {
|
func findProcess(name string) bool {
|
||||||
name = fmt.Sprintf("[%c]%s", name[0], name[1:])
|
name = fmt.Sprintf("[%c]%s", name[0], name[1:])
|
||||||
c := exec.Command("bash", "-c", fmt.Sprintf("ps -eo command | grep %s", strconv.Quote(name)))
|
c := exec.Command("bash", "-c", fmt.Sprintf("ps -eo command | grep %s", strconv.Quote(name)))
|
||||||
output, err := c.CombinedOutput()
|
output, err := c.CombinedOutput()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
So(string(output), ShouldNotEqual, "")
|
So(string(output), ShouldNotEqual, "")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStopCommand(t *testing.T) {
|
func TestStopCommand(t *testing.T) {
|
||||||
Convey("Stop command should clean up all program", t, func() {
|
Convey("Stop command should clean up all program", t, func() {
|
||||||
p := NewProcess(Program{
|
p := NewProcess(Program{
|
||||||
Name: "sleep",
|
Name: "sleep",
|
||||||
Command: "(echo hello; sleep 17&); exit 1",
|
Command: "(echo hello; sleep 17&); exit 1",
|
||||||
StopTimeout: 1,
|
StopTimeout: 1,
|
||||||
})
|
})
|
||||||
p.startCommand()
|
p.startCommand()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
p.stopCommand()
|
p.stopCommand()
|
||||||
So(p.cmd, ShouldBeNil)
|
So(p.cmd, ShouldBeNil)
|
||||||
exists := findProcess("sleep 17")
|
exists := findProcess("sleep 17")
|
||||||
So(exists, ShouldBeFalse)
|
So(exists, ShouldBeFalse)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
hash: ad7063d34838040bf683183342f3814298179801e2e055f29bca796456c88300
|
|
||||||
updated: 2017-08-11T19:08:31.193345249+08:00
|
|
||||||
imports:
|
|
||||||
- name: github.com/codeskyblue/kexec
|
|
||||||
version: 863094f94c7fb7c235764bf8f0f79cccea78c8eb
|
|
||||||
- name: github.com/equinox-io/equinox
|
|
||||||
version: 6f97d0d3970881d3e53dd6f547a41109eb055e54
|
|
||||||
subpackages:
|
|
||||||
- internal/go-update
|
|
||||||
- internal/go-update/internal/binarydist
|
|
||||||
- internal/go-update/internal/osext
|
|
||||||
- internal/osext
|
|
||||||
- proto
|
|
||||||
- name: github.com/franela/goreq
|
|
||||||
version: b5b0f5eb2d16f20345cce0a544a75163579c0b00
|
|
||||||
- name: github.com/glycerine/rbuf
|
|
||||||
version: 96ad00d7fa74f7dd9857f2b6068451062b4ebc5d
|
|
||||||
- name: github.com/go-yaml/yaml
|
|
||||||
version: 25c4ec802a7d637f88d584ab26798e94ad14c13b
|
|
||||||
- name: github.com/goji/httpauth
|
|
||||||
version: 2da839ab0f4df05a6db5eb277995589dadbd4fb9
|
|
||||||
- name: github.com/gorilla/context
|
|
||||||
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
|
||||||
- name: github.com/gorilla/mux
|
|
||||||
version: ac112f7d75a0714af1bd86ab17749b31f7809640
|
|
||||||
- name: github.com/gorilla/websocket
|
|
||||||
version: a69d9f6de432e2c6b296a947d8a5ee88f68522cf
|
|
||||||
- name: github.com/kennygrant/sanitize
|
|
||||||
version: 6a0bfdde8629a3a3a7418a7eae45c54154692514
|
|
||||||
- name: github.com/mitchellh/go-ps
|
|
||||||
version: 4fdf99ab29366514c69ccccddab5dc58b8d84062
|
|
||||||
- name: github.com/qiniu/log
|
|
||||||
version: a304a74568d6982c5b89de1c68ac8fca3add196a
|
|
||||||
- name: github.com/shurcooL/httpfs
|
|
||||||
version: bc35257962c2dea93e81c976b72c7c6eac45fd8a
|
|
||||||
subpackages:
|
|
||||||
- vfsutil
|
|
||||||
- name: github.com/shurcooL/vfsgen
|
|
||||||
version: 385e5833a54aaba5860ca26036b8e8b72135ab96
|
|
||||||
- name: github.com/urfave/cli
|
|
||||||
version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1
|
|
||||||
- name: golang.org/x/net
|
|
||||||
version: 1c05540f6879653db88113bc4a2b70aec4bd491f
|
|
||||||
subpackages:
|
|
||||||
- html
|
|
||||||
- html/atom
|
|
||||||
- name: golang.org/x/tools
|
|
||||||
version: 5831d16d18029819d39f99bdc2060b8eff410b6b
|
|
||||||
subpackages:
|
|
||||||
- godoc/vfs
|
|
||||||
testImports:
|
|
||||||
- name: github.com/gopherjs/gopherjs
|
|
||||||
version: 2b1d432c8a82c9bff0b0baffaeb3ec6e92974112
|
|
||||||
subpackages:
|
|
||||||
- js
|
|
||||||
- name: github.com/jtolds/gls
|
|
||||||
version: 77f18212c9c7edc9bd6a33d383a7b545ce62f064
|
|
||||||
- name: github.com/smartystreets/assertions
|
|
||||||
version: 1540c14c9f1bd1abeba90f29762a4c6e50582303
|
|
||||||
subpackages:
|
|
||||||
- internal/go-render/render
|
|
||||||
- internal/oglematchers
|
|
||||||
- name: github.com/smartystreets/goconvey
|
|
||||||
version: 9e8dc3f972df6c8fcc0375ef492c24d0bb204857
|
|
||||||
subpackages:
|
|
||||||
- convey
|
|
||||||
- convey/gotest
|
|
||||||
- convey/reporting
|
|
||||||
- name: github.com/smartystreets/logging
|
|
||||||
version: ac3a674540761aa0b4382094ba4795f917e85c7f
|
|
@ -1,101 +1,101 @@
|
|||||||
package gops
|
package gops
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
mps "github.com/mitchellh/go-ps"
|
mps "github.com/mitchellh/go-ps"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Process struct {
|
type Process struct {
|
||||||
mps.Process
|
mps.Process
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProcess(pid int) (p Process, err error) {
|
func NewProcess(pid int) (p Process, err error) {
|
||||||
mp, err := mps.FindProcess(pid)
|
mp, err := mps.FindProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return Process{
|
return Process{
|
||||||
Process: mp,
|
Process: mp,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (p *Process) Mem() (m sigar.ProcMem, err error) {
|
// func (p *Process) Mem() (m sigar.ProcMem, err error) {
|
||||||
// err = m.Get(p.Pid())
|
// err = m.Get(p.Pid())
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
type ProcInfo struct {
|
type ProcInfo struct {
|
||||||
Pid int `json:"pid"`
|
Pid int `json:"pid"`
|
||||||
Pids []int `json:"pids"`
|
Pids []int `json:"pids"`
|
||||||
Rss int `json:"rss"`
|
Rss int `json:"rss"`
|
||||||
PCpu float64 `json:"pcpu"`
|
PCpu float64 `json:"pcpu"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pi *ProcInfo) Add(add ProcInfo) {
|
func (pi *ProcInfo) Add(add ProcInfo) {
|
||||||
pi.Rss += add.Rss
|
pi.Rss += add.Rss
|
||||||
pi.PCpu += add.PCpu
|
pi.PCpu += add.PCpu
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU Percent * 100
|
// CPU Percent * 100
|
||||||
// only linux and darwin works
|
// only linux and darwin works
|
||||||
func (p *Process) ProcInfo() (pi ProcInfo, err error) {
|
func (p *Process) ProcInfo() (pi ProcInfo, err error) {
|
||||||
pi.Pid = p.Pid()
|
pi.Pid = p.Pid()
|
||||||
cmd := exec.Command("ps", "-o", "pcpu,rss", "-p", strconv.Itoa(p.Pid()))
|
cmd := exec.Command("ps", "-o", "pcpu,rss", "-p", strconv.Itoa(p.Pid()))
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.New("ps err: " + err.Error())
|
err = errors.New("ps err: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.SplitN(string(output), "\n", 2)
|
fields := strings.SplitN(string(output), "\n", 2)
|
||||||
if len(fields) != 2 {
|
if len(fields) != 2 {
|
||||||
err = errors.New("parse ps command out format error")
|
err = errors.New("parse ps command out format error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = fmt.Sscanf(fields[1], "%f %d", &pi.PCpu, &pi.Rss)
|
_, err = fmt.Sscanf(fields[1], "%f %d", &pi.PCpu, &pi.Rss)
|
||||||
pi.Rss *= 1024
|
pi.Rss *= 1024
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all child process
|
// Get all child process
|
||||||
func (p *Process) Children(recursive bool) (cps []Process) {
|
func (p *Process) Children(recursive bool) (cps []Process) {
|
||||||
pses, err := mps.Processes()
|
pses, err := mps.Processes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pidMap := make(map[int][]mps.Process, 0)
|
pidMap := make(map[int][]mps.Process, 0)
|
||||||
for _, p := range pses {
|
for _, p := range pses {
|
||||||
pidMap[p.PPid()] = append(pidMap[p.PPid()], p)
|
pidMap[p.PPid()] = append(pidMap[p.PPid()], p)
|
||||||
}
|
}
|
||||||
var travel func(int)
|
var travel func(int)
|
||||||
travel = func(pid int) {
|
travel = func(pid int) {
|
||||||
for _, p := range pidMap[pid] {
|
for _, p := range pidMap[pid] {
|
||||||
cps = append(cps, Process{p})
|
cps = append(cps, Process{p})
|
||||||
if recursive {
|
if recursive {
|
||||||
travel(p.Pid())
|
travel(p.Pid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
travel(p.Pid())
|
travel(p.Pid())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Sum everything
|
//Sum everything
|
||||||
func (p *Process) ChildrenProcInfo(recursive bool) (pi ProcInfo) {
|
func (p *Process) ChildrenProcInfo(recursive bool) (pi ProcInfo) {
|
||||||
cps := p.Children(recursive)
|
cps := p.Children(recursive)
|
||||||
for _, cp := range cps {
|
for _, cp := range cps {
|
||||||
info, er := cp.ProcInfo()
|
info, er := cp.ProcInfo()
|
||||||
if er != nil {
|
if er != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pi.Add(info)
|
pi.Add(info)
|
||||||
pi.Pids = append(pi.Pids, cp.Pid())
|
pi.Pids = append(pi.Pids, cp.Pid())
|
||||||
}
|
}
|
||||||
pi.Pid = p.Pid()
|
pi.Pid = p.Pid()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package gops
|
package gops
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestProcInfo(t *testing.T) {
|
func TestProcInfo(t *testing.T) {
|
||||||
p, err := NewProcess(6464)
|
p, err := NewProcess(6464)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pi, err := p.ProcInfo()
|
pi, err := p.ProcInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Log(pi)
|
t.Log(pi)
|
||||||
}
|
}
|
||||||
|
@ -1,587 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Bootstrap v3.3.5 (http://getbootstrap.com)
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
*/
|
|
||||||
.btn-default,
|
|
||||||
.btn-primary,
|
|
||||||
.btn-success,
|
|
||||||
.btn-info,
|
|
||||||
.btn-warning,
|
|
||||||
.btn-danger {
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
|
|
||||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
|
||||||
}
|
|
||||||
.btn-default:active,
|
|
||||||
.btn-primary:active,
|
|
||||||
.btn-success:active,
|
|
||||||
.btn-info:active,
|
|
||||||
.btn-warning:active,
|
|
||||||
.btn-danger:active,
|
|
||||||
.btn-default.active,
|
|
||||||
.btn-primary.active,
|
|
||||||
.btn-success.active,
|
|
||||||
.btn-info.active,
|
|
||||||
.btn-warning.active,
|
|
||||||
.btn-danger.active {
|
|
||||||
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
|
||||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
|
||||||
}
|
|
||||||
.btn-default.disabled,
|
|
||||||
.btn-primary.disabled,
|
|
||||||
.btn-success.disabled,
|
|
||||||
.btn-info.disabled,
|
|
||||||
.btn-warning.disabled,
|
|
||||||
.btn-danger.disabled,
|
|
||||||
.btn-default[disabled],
|
|
||||||
.btn-primary[disabled],
|
|
||||||
.btn-success[disabled],
|
|
||||||
.btn-info[disabled],
|
|
||||||
.btn-warning[disabled],
|
|
||||||
.btn-danger[disabled],
|
|
||||||
fieldset[disabled] .btn-default,
|
|
||||||
fieldset[disabled] .btn-primary,
|
|
||||||
fieldset[disabled] .btn-success,
|
|
||||||
fieldset[disabled] .btn-info,
|
|
||||||
fieldset[disabled] .btn-warning,
|
|
||||||
fieldset[disabled] .btn-danger {
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.btn-default .badge,
|
|
||||||
.btn-primary .badge,
|
|
||||||
.btn-success .badge,
|
|
||||||
.btn-info .badge,
|
|
||||||
.btn-warning .badge,
|
|
||||||
.btn-danger .badge {
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
.btn:active,
|
|
||||||
.btn.active {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-default {
|
|
||||||
text-shadow: 0 1px 0 #fff;
|
|
||||||
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
|
|
||||||
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #dbdbdb;
|
|
||||||
border-color: #ccc;
|
|
||||||
}
|
|
||||||
.btn-default:hover,
|
|
||||||
.btn-default:focus {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-default:active,
|
|
||||||
.btn-default.active {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border-color: #dbdbdb;
|
|
||||||
}
|
|
||||||
.btn-default.disabled,
|
|
||||||
.btn-default[disabled],
|
|
||||||
fieldset[disabled] .btn-default,
|
|
||||||
.btn-default.disabled:hover,
|
|
||||||
.btn-default[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-default:hover,
|
|
||||||
.btn-default.disabled:focus,
|
|
||||||
.btn-default[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-default:focus,
|
|
||||||
.btn-default.disabled.focus,
|
|
||||||
.btn-default[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-default.focus,
|
|
||||||
.btn-default.disabled:active,
|
|
||||||
.btn-default[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-default:active,
|
|
||||||
.btn-default.disabled.active,
|
|
||||||
.btn-default[disabled].active,
|
|
||||||
fieldset[disabled] .btn-default.active {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #245580;
|
|
||||||
}
|
|
||||||
.btn-primary:hover,
|
|
||||||
.btn-primary:focus {
|
|
||||||
background-color: #265a88;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-primary:active,
|
|
||||||
.btn-primary.active {
|
|
||||||
background-color: #265a88;
|
|
||||||
border-color: #245580;
|
|
||||||
}
|
|
||||||
.btn-primary.disabled,
|
|
||||||
.btn-primary[disabled],
|
|
||||||
fieldset[disabled] .btn-primary,
|
|
||||||
.btn-primary.disabled:hover,
|
|
||||||
.btn-primary[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-primary:hover,
|
|
||||||
.btn-primary.disabled:focus,
|
|
||||||
.btn-primary[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-primary:focus,
|
|
||||||
.btn-primary.disabled.focus,
|
|
||||||
.btn-primary[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-primary.focus,
|
|
||||||
.btn-primary.disabled:active,
|
|
||||||
.btn-primary[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-primary:active,
|
|
||||||
.btn-primary.disabled.active,
|
|
||||||
.btn-primary[disabled].active,
|
|
||||||
fieldset[disabled] .btn-primary.active {
|
|
||||||
background-color: #265a88;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-success {
|
|
||||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
|
|
||||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #3e8f3e;
|
|
||||||
}
|
|
||||||
.btn-success:hover,
|
|
||||||
.btn-success:focus {
|
|
||||||
background-color: #419641;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-success:active,
|
|
||||||
.btn-success.active {
|
|
||||||
background-color: #419641;
|
|
||||||
border-color: #3e8f3e;
|
|
||||||
}
|
|
||||||
.btn-success.disabled,
|
|
||||||
.btn-success[disabled],
|
|
||||||
fieldset[disabled] .btn-success,
|
|
||||||
.btn-success.disabled:hover,
|
|
||||||
.btn-success[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-success:hover,
|
|
||||||
.btn-success.disabled:focus,
|
|
||||||
.btn-success[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-success:focus,
|
|
||||||
.btn-success.disabled.focus,
|
|
||||||
.btn-success[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-success.focus,
|
|
||||||
.btn-success.disabled:active,
|
|
||||||
.btn-success[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-success:active,
|
|
||||||
.btn-success.disabled.active,
|
|
||||||
.btn-success[disabled].active,
|
|
||||||
fieldset[disabled] .btn-success.active {
|
|
||||||
background-color: #419641;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-info {
|
|
||||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
|
|
||||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #28a4c9;
|
|
||||||
}
|
|
||||||
.btn-info:hover,
|
|
||||||
.btn-info:focus {
|
|
||||||
background-color: #2aabd2;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-info:active,
|
|
||||||
.btn-info.active {
|
|
||||||
background-color: #2aabd2;
|
|
||||||
border-color: #28a4c9;
|
|
||||||
}
|
|
||||||
.btn-info.disabled,
|
|
||||||
.btn-info[disabled],
|
|
||||||
fieldset[disabled] .btn-info,
|
|
||||||
.btn-info.disabled:hover,
|
|
||||||
.btn-info[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-info:hover,
|
|
||||||
.btn-info.disabled:focus,
|
|
||||||
.btn-info[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-info:focus,
|
|
||||||
.btn-info.disabled.focus,
|
|
||||||
.btn-info[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-info.focus,
|
|
||||||
.btn-info.disabled:active,
|
|
||||||
.btn-info[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-info:active,
|
|
||||||
.btn-info.disabled.active,
|
|
||||||
.btn-info[disabled].active,
|
|
||||||
fieldset[disabled] .btn-info.active {
|
|
||||||
background-color: #2aabd2;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-warning {
|
|
||||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
|
|
||||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #e38d13;
|
|
||||||
}
|
|
||||||
.btn-warning:hover,
|
|
||||||
.btn-warning:focus {
|
|
||||||
background-color: #eb9316;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-warning:active,
|
|
||||||
.btn-warning.active {
|
|
||||||
background-color: #eb9316;
|
|
||||||
border-color: #e38d13;
|
|
||||||
}
|
|
||||||
.btn-warning.disabled,
|
|
||||||
.btn-warning[disabled],
|
|
||||||
fieldset[disabled] .btn-warning,
|
|
||||||
.btn-warning.disabled:hover,
|
|
||||||
.btn-warning[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-warning:hover,
|
|
||||||
.btn-warning.disabled:focus,
|
|
||||||
.btn-warning[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-warning:focus,
|
|
||||||
.btn-warning.disabled.focus,
|
|
||||||
.btn-warning[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-warning.focus,
|
|
||||||
.btn-warning.disabled:active,
|
|
||||||
.btn-warning[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-warning:active,
|
|
||||||
.btn-warning.disabled.active,
|
|
||||||
.btn-warning[disabled].active,
|
|
||||||
fieldset[disabled] .btn-warning.active {
|
|
||||||
background-color: #eb9316;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.btn-danger {
|
|
||||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
|
|
||||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #b92c28;
|
|
||||||
}
|
|
||||||
.btn-danger:hover,
|
|
||||||
.btn-danger:focus {
|
|
||||||
background-color: #c12e2a;
|
|
||||||
background-position: 0 -15px;
|
|
||||||
}
|
|
||||||
.btn-danger:active,
|
|
||||||
.btn-danger.active {
|
|
||||||
background-color: #c12e2a;
|
|
||||||
border-color: #b92c28;
|
|
||||||
}
|
|
||||||
.btn-danger.disabled,
|
|
||||||
.btn-danger[disabled],
|
|
||||||
fieldset[disabled] .btn-danger,
|
|
||||||
.btn-danger.disabled:hover,
|
|
||||||
.btn-danger[disabled]:hover,
|
|
||||||
fieldset[disabled] .btn-danger:hover,
|
|
||||||
.btn-danger.disabled:focus,
|
|
||||||
.btn-danger[disabled]:focus,
|
|
||||||
fieldset[disabled] .btn-danger:focus,
|
|
||||||
.btn-danger.disabled.focus,
|
|
||||||
.btn-danger[disabled].focus,
|
|
||||||
fieldset[disabled] .btn-danger.focus,
|
|
||||||
.btn-danger.disabled:active,
|
|
||||||
.btn-danger[disabled]:active,
|
|
||||||
fieldset[disabled] .btn-danger:active,
|
|
||||||
.btn-danger.disabled.active,
|
|
||||||
.btn-danger[disabled].active,
|
|
||||||
fieldset[disabled] .btn-danger.active {
|
|
||||||
background-color: #c12e2a;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.thumbnail,
|
|
||||||
.img-thumbnail {
|
|
||||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
|
||||||
}
|
|
||||||
.dropdown-menu > li > a:hover,
|
|
||||||
.dropdown-menu > li > a:focus {
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
|
|
||||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.dropdown-menu > .active > a,
|
|
||||||
.dropdown-menu > .active > a:hover,
|
|
||||||
.dropdown-menu > .active > a:focus {
|
|
||||||
background-color: #2e6da4;
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.navbar-default {
|
|
||||||
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
|
|
||||||
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-radius: 4px;
|
|
||||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
|
||||||
}
|
|
||||||
.navbar-default .navbar-nav > .open > a,
|
|
||||||
.navbar-default .navbar-nav > .active > a {
|
|
||||||
background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
|
|
||||||
background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
|
||||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
|
||||||
}
|
|
||||||
.navbar-brand,
|
|
||||||
.navbar-nav > li > a {
|
|
||||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
|
|
||||||
}
|
|
||||||
.navbar-inverse {
|
|
||||||
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
|
|
||||||
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.navbar-inverse .navbar-nav > .open > a,
|
|
||||||
.navbar-inverse .navbar-nav > .active > a {
|
|
||||||
background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
|
|
||||||
background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
|
||||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
|
||||||
}
|
|
||||||
.navbar-inverse .navbar-brand,
|
|
||||||
.navbar-inverse .navbar-nav > li > a {
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
|
|
||||||
}
|
|
||||||
.navbar-static-top,
|
|
||||||
.navbar-fixed-top,
|
|
||||||
.navbar-fixed-bottom {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.navbar .navbar-nav .open .dropdown-menu > .active > a,
|
|
||||||
.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
|
|
||||||
.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
|
|
||||||
color: #fff;
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert {
|
|
||||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
|
|
||||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
|
||||||
}
|
|
||||||
.alert-success {
|
|
||||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
|
|
||||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #b2dba1;
|
|
||||||
}
|
|
||||||
.alert-info {
|
|
||||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
|
|
||||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #9acfea;
|
|
||||||
}
|
|
||||||
.alert-warning {
|
|
||||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
|
|
||||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #f5e79e;
|
|
||||||
}
|
|
||||||
.alert-danger {
|
|
||||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
|
|
||||||
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #dca7a7;
|
|
||||||
}
|
|
||||||
.progress {
|
|
||||||
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
|
|
||||||
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar {
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar-success {
|
|
||||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
|
|
||||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar-info {
|
|
||||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
|
|
||||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar-warning {
|
|
||||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
|
|
||||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar-danger {
|
|
||||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
|
|
||||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.progress-bar-striped {
|
|
||||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
|
||||||
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
|
||||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
|
||||||
}
|
|
||||||
.list-group {
|
|
||||||
border-radius: 4px;
|
|
||||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
|
||||||
}
|
|
||||||
.list-group-item.active,
|
|
||||||
.list-group-item.active:hover,
|
|
||||||
.list-group-item.active:focus {
|
|
||||||
text-shadow: 0 -1px 0 #286090;
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #2b669a;
|
|
||||||
}
|
|
||||||
.list-group-item.active .badge,
|
|
||||||
.list-group-item.active:hover .badge,
|
|
||||||
.list-group-item.active:focus .badge {
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
|
||||||
}
|
|
||||||
.panel-default > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
|
|
||||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.panel-primary > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
|
|
||||||
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.panel-success > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
|
|
||||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.panel-info > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
|
|
||||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.panel-warning > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
|
|
||||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.panel-danger > .panel-heading {
|
|
||||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
|
|
||||||
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
.well {
|
|
||||||
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
|
|
||||||
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
border-color: #dcdcdc;
|
|
||||||
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
/*# sourceMappingURL=bootstrap-theme.css.map */
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,13 +0,0 @@
|
|||||||
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
|
|
||||||
require('../../js/transition.js')
|
|
||||||
require('../../js/alert.js')
|
|
||||||
require('../../js/button.js')
|
|
||||||
require('../../js/carousel.js')
|
|
||||||
require('../../js/collapse.js')
|
|
||||||
require('../../js/dropdown.js')
|
|
||||||
require('../../js/modal.js')
|
|
||||||
require('../../js/tooltip.js')
|
|
||||||
require('../../js/popover.js')
|
|
||||||
require('../../js/scrollspy.js')
|
|
||||||
require('../../js/tab.js')
|
|
||||||
require('../../js/affix.js')
|
|
@ -1,39 +1,39 @@
|
|||||||
/* */
|
/* */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
background-color: #1b926c;
|
background-color: #1b926c;
|
||||||
border-color: #1fa67a;
|
border-color: #1fa67a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-brand {
|
.navbar .navbar-brand {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar ul.navbar-nav>li>a {
|
.navbar ul.navbar-nav>li>a {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.realtime-log {
|
.realtime-log {
|
||||||
height: 40em;
|
height: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
border-radius: 0.3em;
|
border-radius: 0.3em;
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
small.user {
|
small.user {
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-red {
|
.color-red {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
@ -1,252 +1,252 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<title>GoSUV</title>
|
<title>GoSUV</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico" />
|
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico" />
|
||||||
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
|
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
|
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
|
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<nav class="navbar navbar-inverse">
|
<nav class="navbar navbar-inverse">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
||||||
<span class="sr-only">Toggle navigation</span>
|
<span class="sr-only">Toggle navigation</span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="/">Go Supervisor 2.0 <small class="user">[[.User]]</small></a>
|
<a class="navbar-brand" href="/">Go Supervisor 2.0 <small class="user">[[.User]]</small></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<!-- <li class="hidden-xs">
|
<!-- <li class="hidden-xs">
|
||||||
<a>
|
<a>
|
||||||
<span class="glyphicon glyphicon-qrcode"></span>
|
<span class="glyphicon glyphicon-qrcode"></span>
|
||||||
</a>
|
</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="alert alert-danger" role="alert" v-if="!isConnectionAlive">
|
<div class="alert alert-danger" role="alert" v-if="!isConnectionAlive">
|
||||||
<strong>Connection lost</strong> try to reconnect after 3s, or <a href="/" class="alert-link">connect immediately</a>
|
<strong>Connection lost</strong> try to reconnect after 3s, or <a href="/" class="alert-link">connect immediately</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button class="btn btn-default btn-sm" id="launchNewProgram">
|
<button class="btn btn-default btn-sm" id="launchNewProgram">
|
||||||
<span class="glyphicon glyphicon-plus"></span> New Program
|
<span class="glyphicon glyphicon-plus"></span> New Program
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-sm" v-on:click="refresh">
|
<button class="btn btn-default btn-sm" v-on:click="refresh">
|
||||||
<span class="glyphicon glyphicon-refresh"></span> Refresh
|
<span class="glyphicon glyphicon-refresh"></span> Refresh
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-sm" v-on:click="reload">
|
<button class="btn btn-default btn-sm" v-on:click="reload">
|
||||||
<span class="glyphicon glyphicon-repeat"></span> Reload
|
<span class="glyphicon glyphicon-repeat"></span> Reload
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-sm" v-on:click="test" data-toggle="tooltip" data-placement="bottom" title="Tooltip on bottom">
|
<button class="btn btn-default btn-sm" v-on:click="test" data-toggle="tooltip" data-placement="bottom" title="Tooltip on bottom">
|
||||||
<span class="glyphicon glyphicon-glass"></span> Test
|
<span class="glyphicon glyphicon-glass"></span> Test
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>Status</td>
|
<td>Status</td>
|
||||||
<td>View</td>
|
<td>View</td>
|
||||||
<td>Command</td>
|
<td>Command</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="p in programs">
|
<tr v-for="p in programs">
|
||||||
<td v-text="p.program.name"></td>
|
<td v-text="p.program.name"></td>
|
||||||
<td v-html="p.status | colorStatus"></td>
|
<td v-html="p.status | colorStatus"></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
|
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
|
||||||
<span class="fa fa-file-text-o"></span> Log
|
<span class="fa fa-file-text-o"></span> Log
|
||||||
</button>
|
</button>
|
||||||
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
|
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
|
||||||
<span class="fa fa-bar-chart"></span> Profiles
|
<span class="fa fa-bar-chart"></span> Profiles
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
|
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
|
||||||
<span class="glyphicon glyphicon-info-sign"></span> Info
|
<span class="glyphicon glyphicon-info-sign"></span> Info
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button v-on:click="cmdStart(p.program.name)" class="btn btn-default btn-xs" :disabled='["running", "stopping"].indexOf(p.status) != -1'>
|
<button v-on:click="cmdStart(p.program.name)" class="btn btn-default btn-xs" :disabled='["running", "stopping"].indexOf(p.status) != -1'>
|
||||||
<span class="glyphicon glyphicon-play"></span> Start
|
<span class="glyphicon glyphicon-play"></span> Start
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name)" :disabled="!canStop(p.status)">
|
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name)" :disabled="!canStop(p.status)">
|
||||||
<span class="glyphicon glyphicon-stop"></span> Stop
|
<span class="glyphicon glyphicon-stop"></span> Stop
|
||||||
</button>
|
</button>
|
||||||
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
|
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
|
||||||
<span class="glyphicon glyphicon-edit"></span> Edit
|
<span class="glyphicon glyphicon-edit"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
|
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
|
||||||
<span class="color-red glyphicon glyphicon-trash"></span> Delete
|
<span class="color-red glyphicon glyphicon-trash"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
||||||
<a href="https://github.com/codeskyblue/gosuv">gosuv ([[.Version]])</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2017. go1.7
|
<a href="https://github.com/codeskyblue/gosuv">gosuv ([[.Version]])</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2017. go1.7
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- panels -->
|
<!-- panels -->
|
||||||
<!-- modals -->
|
<!-- modals -->
|
||||||
<div class="modal" id="newProgram">
|
<div class="modal" id="newProgram">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="formNewProgram" action="/api/programs" method="post">
|
<form id="formNewProgram" action="/api/programs" method="post">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title">New program</h4>
|
<h4 class="modal-title">New program</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input type="text" name="name" v-model="program.name" class="form-control" placeholder="name (must be unique)" required>
|
<input type="text" name="name" v-model="program.name" class="form-control" placeholder="name (must be unique)" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Command</label>
|
<label>Command</label>
|
||||||
<input type="text" name="command" class="form-control" placeholder="shell command, ex: redis-server --port 6379">
|
<input type="text" name="command" class="form-control" placeholder="shell command, ex: redis-server --port 6379">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Directory</label>
|
<label>Directory</label>
|
||||||
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
|
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>User</label>
|
<label>User</label>
|
||||||
<input type="text" name="user" class="form-control" placeholder="user, optional">
|
<input type="text" name="user" class="form-control" placeholder="user, optional">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Fail Retries</label>
|
<label>Fail Retries</label>
|
||||||
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" value="3">
|
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" value="3">
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input name="autostart" type="checkbox"> Auto start
|
<input name="autostart" type="checkbox"> Auto start
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
|
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
<button type="submit" class="btn btn-primary">Add program</button>
|
<button type="submit" class="btn btn-primary">Add program</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- model edit -->
|
<!-- model edit -->
|
||||||
<div class="modal" id="programEdit">
|
<div class="modal" id="programEdit">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form v-on:submit.prevent="editProgram">
|
<form v-on:submit.prevent="editProgram">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span v-text="edit.program.name"></span></h4>
|
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span v-text="edit.program.name"></span></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Command</label>
|
<label>Command</label>
|
||||||
<input type="text" name="command" class="form-control" v-model="edit.program.command">
|
<input type="text" name="command" class="form-control" v-model="edit.program.command">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Directory</label>
|
<label>Directory</label>
|
||||||
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
|
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>User</label>
|
<label>User</label>
|
||||||
<input type="text" name="user" class="form-control" v-model="edit.program.user">
|
<input type="text" name="user" class="form-control" v-model="edit.program.user">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Fail Retries</label>
|
<label>Fail Retries</label>
|
||||||
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" v-model="edit.program.startRetries">
|
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" v-model="edit.program.startRetries">
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
|
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
|
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
<button type="submit" class="btn btn-primary">Update</button>
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.modal -->
|
<!-- /.modal -->
|
||||||
<div class="modal" id="modalTailf">
|
<div class="modal" id="modalTailf">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="formNewProgram" action="/api/programs" method="post">
|
<form id="formNewProgram" action="/api/programs" method="post">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title">Tail</h4>
|
<h4 class="modal-title">Tail</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>Line: {{log.line_count}}</p>
|
<p>Line: {{log.line_count}}</p>
|
||||||
<pre v-html="log.content" class="realtime-log"></pre>
|
<pre v-html="log.content" class="realtime-log"></pre>
|
||||||
<div class="checkbox text-right">
|
<div class="checkbox text-right">
|
||||||
<label>
|
<label>
|
||||||
<input v-model="log.follow" type="checkbox"> Follow
|
<input v-model="log.follow" type="checkbox"> Follow
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- polyfill make browser support ECMAScript 6th edition -->
|
<!-- polyfill make browser support ECMAScript 6th edition -->
|
||||||
<script src="/res/js/promise-polyfill.min.js"></script>
|
<script src="/res/js/promise-polyfill.min.js"></script>
|
||||||
<script src="/res/js/jquery-3.1.0.min.js"></script>
|
<script src="/res/js/jquery-3.1.0.min.js"></script>
|
||||||
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
||||||
<script src="/res/js/moment.min.js"></script>
|
<script src="/res/js/moment.min.js"></script>
|
||||||
<script src="/res/js/underscore-min.js"></script>
|
<script src="/res/js/underscore-min.js"></script>
|
||||||
<script src="/res/js/vue-1.0.min.js"></script>
|
<script src="/res/js/vue-1.0.min.js"></script>
|
||||||
<script src="/res/js/common.js"></script>
|
<script src="/res/js/common.js"></script>
|
||||||
<script src="/res/js/index.js"></script>
|
<script src="/res/js/index.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
$("#launchNewProgram").click(function() {
|
$("#launchNewProgram").click(function() {
|
||||||
$("#newProgram").modal({
|
$("#newProgram").modal({
|
||||||
show: true,
|
show: true,
|
||||||
// keyboard: false,
|
// keyboard: false,
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
// $("#modalTailf").modal({
|
// $("#modalTailf").modal({
|
||||||
// // show: true,
|
// // show: true,
|
||||||
// // keyboard: false,
|
// // keyboard: false,
|
||||||
// // backdrop: 'static',
|
// // backdrop: 'static',
|
||||||
// })
|
// })
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,47 +1,47 @@
|
|||||||
/* Javascript */
|
/* Javascript */
|
||||||
function pathJoin(parts, sep) {
|
function pathJoin(parts, sep) {
|
||||||
var separator = sep || '/';
|
var separator = sep || '/';
|
||||||
var replace = new RegExp(separator + '{1,}', 'g');
|
var replace = new RegExp(separator + '{1,}', 'g');
|
||||||
return parts.join(separator).replace(replace, separator);
|
return parts.join(separator).replace(replace, separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryString(name) {
|
function getQueryString(name) {
|
||||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||||
var r = decodeURI(window.location.search).substr(1).match(reg);
|
var r = decodeURI(window.location.search).substr(1).match(reg);
|
||||||
if (r != null) return r[2].replace(/\+/g, ' ');
|
if (r != null) return r[2].replace(/\+/g, ' ');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function newWebsocket(pathname, opts) {
|
function newWebsocket(pathname, opts) {
|
||||||
var wsProtocol = location.protocol == "https:" ? "wss" : "ws";
|
var wsProtocol = location.protocol == "https:" ? "wss" : "ws";
|
||||||
var ws = new WebSocket(wsProtocol + "://" + location.host + pathname);
|
var ws = new WebSocket(wsProtocol + "://" + location.host + pathname);
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
ws.onopen = opts.onopen || function(evt) {
|
ws.onopen = opts.onopen || function(evt) {
|
||||||
console.log("WS OPEN", pathname);
|
console.log("WS OPEN", pathname);
|
||||||
}
|
}
|
||||||
ws.onclose = opts.onclose || function(evt) {
|
ws.onclose = opts.onclose || function(evt) {
|
||||||
console.log("CLOSE");
|
console.log("CLOSE");
|
||||||
ws = null;
|
ws = null;
|
||||||
}
|
}
|
||||||
ws.onmessage = opts.onmessage || function(evt) {
|
ws.onmessage = opts.onmessage || function(evt) {
|
||||||
console.log("response:" + evt.data);
|
console.log("response:" + evt.data);
|
||||||
}
|
}
|
||||||
ws.onerror = function(evt) {
|
ws.onerror = function(evt) {
|
||||||
console.error("error:", evt.data);
|
console.error("error:", evt.data);
|
||||||
}
|
}
|
||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(value) {
|
function formatBytes(value) {
|
||||||
var bytes = parseFloat(value);
|
var bytes = parseFloat(value);
|
||||||
if (bytes < 0) return "-";
|
if (bytes < 0) return "-";
|
||||||
else if (bytes < 1024) return bytes + " B";
|
else if (bytes < 1024) return bytes + " B";
|
||||||
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
|
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
|
||||||
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
|
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
|
||||||
else return (bytes / 1073741824).toFixed(1) + " GB";
|
else return (bytes / 1073741824).toFixed(1) + " GB";
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function(){
|
$(function(){
|
||||||
$(".tooltip-wraper").tooltip();
|
$(".tooltip-wraper").tooltip();
|
||||||
})
|
})
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,22 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
@ -1,22 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
Copyright (c) 2016 codeskyblue
|
|
||||||
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
||||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
||||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
||||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
|||||||
# kexec
|
|
||||||
[![GoDoc](https://godoc.org/github.com/codeskyblue/kexec?status.svg)](https://godoc.org/github.com/codeskyblue/kexec)
|
|
||||||
|
|
||||||
This is a golang lib, add a `Terminate` command to exec.
|
|
||||||
|
|
||||||
Tested on _windows, linux, darwin._
|
|
||||||
|
|
||||||
This lib has been used in [fswatch](https://github.com/codeskyblue/fswatch).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
go get -v github.com/codeskyblue/kexec
|
|
||||||
|
|
||||||
|
|
||||||
example1:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/codeskyblue/kexec"
|
|
||||||
|
|
||||||
func main(){
|
|
||||||
p := kexec.Command("python", "flask_main.py")
|
|
||||||
p.Start()
|
|
||||||
p.Terminate(syscall.SIGINT)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
example2: see more [examples](examples)
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/codeskyblue/kexec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// In unix will call: bash -c "python flask_main.py"
|
|
||||||
// In windows will call: cmd /c "python flask_main.py"
|
|
||||||
p := kexec.CommandString("python flask_main.py")
|
|
||||||
p.Stdout = os.Stdout
|
|
||||||
p.Stderr = os.Stderr
|
|
||||||
p.Start()
|
|
||||||
p.Terminate(syscall.SIGKILL)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
example3:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/codeskyblue/kexec"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
p := kexec.Command("whoami")
|
|
||||||
p.SetUser("codeskyblue") // Only works on darwin and linux
|
|
||||||
p.Run()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PS
|
|
||||||
This lib also support you call `Wait()` twice, which is not support by `os/exec`
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
[MIT](LICENSE)
|
|
@ -1,19 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/codeskyblue/kexec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
p := kexec.CommandString("python flask_main.py")
|
|
||||||
p.Start()
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
err := p.Terminate(syscall.SIGKILL)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import flask
|
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(port=46732, debug=True)
|
|
@ -1,54 +0,0 @@
|
|||||||
package kexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type KCommand struct {
|
|
||||||
*exec.Cmd
|
|
||||||
|
|
||||||
errCs []chan error
|
|
||||||
err error
|
|
||||||
finished bool
|
|
||||||
once sync.Once
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *KCommand) Run() error {
|
|
||||||
if err := c.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// This Wait wraps exec.Wait, but support multi call
|
|
||||||
func (k *KCommand) Wait() error {
|
|
||||||
if k.Process == nil {
|
|
||||||
return errors.New("exec: not started")
|
|
||||||
}
|
|
||||||
k.once.Do(func() {
|
|
||||||
if k.errCs == nil {
|
|
||||||
k.errCs = make([]chan error, 0)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
k.err = k.Cmd.Wait()
|
|
||||||
k.mu.Lock()
|
|
||||||
k.finished = true
|
|
||||||
for _, errC := range k.errCs {
|
|
||||||
errC <- k.err
|
|
||||||
}
|
|
||||||
k.mu.Unlock()
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
k.mu.Lock()
|
|
||||||
if k.finished {
|
|
||||||
k.mu.Unlock()
|
|
||||||
return k.err
|
|
||||||
}
|
|
||||||
errC := make(chan error, 1)
|
|
||||||
k.errCs = append(k.errCs, errC)
|
|
||||||
k.mu.Unlock()
|
|
||||||
return <-errC
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
// +build !windows
|
|
||||||
|
|
||||||
package kexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupCmd(cmd *exec.Cmd) {
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
||||||
cmd.SysProcAttr.Setsid = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func Command(name string, arg ...string) *KCommand {
|
|
||||||
cmd := exec.Command(name, arg...)
|
|
||||||
setupCmd(cmd)
|
|
||||||
return &KCommand{
|
|
||||||
Cmd: cmd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CommandString(command string) *KCommand {
|
|
||||||
cmd := exec.Command("/bin/bash", "-c", command)
|
|
||||||
setupCmd(cmd)
|
|
||||||
//cmd.Stdout = os.Stdout
|
|
||||||
//cmd.Stderr = os.Stderr
|
|
||||||
return &KCommand{
|
|
||||||
Cmd: cmd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *KCommand) Terminate(sig os.Signal) (err error) {
|
|
||||||
if p.Process == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// find pgid, ref: http://unix.stackexchange.com/questions/14815/process-descendants
|
|
||||||
group, err := os.FindProcess(-1 * p.Process.Pid)
|
|
||||||
//log.Println(group)
|
|
||||||
if err == nil {
|
|
||||||
err = group.Signal(sig)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ref: http://stackoverflow.com/questions/21705950/running-external-commands-through-os-exec-under-another-user
|
|
||||||
func (k *KCommand) SetUser(name string) (err error) {
|
|
||||||
u, err := user.Lookup(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uid, err := strconv.Atoi(u.Uid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
gid, err := strconv.Atoi(u.Gid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if k.SysProcAttr == nil {
|
|
||||||
k.SysProcAttr = &syscall.SysProcAttr{}
|
|
||||||
}
|
|
||||||
k.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package kexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCommand(t *testing.T) {
|
|
||||||
Convey("1 should equal 1", t, func() {
|
|
||||||
So(1, ShouldEqual, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("kexec should work as normal os/exec", t, func() {
|
|
||||||
cmd := Command("echo", "-n", "123")
|
|
||||||
data, err := cmd.Output()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(string(data), ShouldEqual, "123")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("the terminate should kill proc", t, func() {
|
|
||||||
cmd := CommandString("sleep 51")
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmd.Start()
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
cmd.Terminate(syscall.SIGINT)
|
|
||||||
err := cmd.Wait()
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
//So(err.Error(), ShouldEqual, "signal: interrupt")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Should ok with call Wait twice", t, func() {
|
|
||||||
cmd := CommandString("not-exists-command-xxl213 true")
|
|
||||||
var err error
|
|
||||||
err = cmd.Start()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
err1 := cmd.Wait()
|
|
||||||
So(err1, ShouldNotBeNil)
|
|
||||||
err2 := cmd.Wait()
|
|
||||||
So(err1, ShouldEqual, err2)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Set user works", t, func() {
|
|
||||||
u, err := user.Current()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
// Set user must be root
|
|
||||||
if u.Uid != "0" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := Command("whoami")
|
|
||||||
err = cmd.SetUser("qard2")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
output, err := cmd.Output()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(string(output), ShouldEqual, "qard2\n")
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package kexec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Command(name string, arg ...string) *KCommand {
|
|
||||||
return &KCommand{
|
|
||||||
Cmd: exec.Command(name, arg...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CommandString(command string) *KCommand {
|
|
||||||
cmd := exec.Command("cmd", "/c", command)
|
|
||||||
//cmd.Stdout = os.Stdout
|
|
||||||
//cmd.Stderr = os.Stderr
|
|
||||||
return &KCommand{
|
|
||||||
Cmd: cmd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *KCommand) Terminate(sig os.Signal) (err error) {
|
|
||||||
if p.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pid := p.Process.Pid
|
|
||||||
c := exec.Command("taskkill", "/t", "/f", "/pid", strconv.Itoa(pid))
|
|
||||||
c.Stdout = os.Stdout
|
|
||||||
c.Stderr = os.Stderr
|
|
||||||
return c.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUser not support on windws
|
|
||||||
func (k *KCommand) SetUser(name string) (err error) {
|
|
||||||
log.Printf("Can not set user(%s) on windows", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
web: python flask_main.py
|
|
@ -1,11 +0,0 @@
|
|||||||
import flask
|
|
||||||
|
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def homepage():
|
|
||||||
return 'Home'
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(debug=True, host='0.0.0.0')
|
|
@ -1,22 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"kproc"
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
p := kproc.ProcString("python flask_main.py")
|
|
||||||
p.Start()
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
err := p.Terminate(syscall.SIGKILL)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
out, _ := exec.Command("lsof", "-i:5000").CombinedOutput()
|
|
||||||
fmt.Println(string(out))
|
|
||||||
}
|
|
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.4
|
|
||||||
- 1.5
|
|
||||||
script: go test -v github.com/equinox-io/equinox github.com/equinox-io/equinox/proto
|
|
@ -1,99 +0,0 @@
|
|||||||
# equinox client SDK [![godoc reference](https://godoc.org/github.com/equinox-io/equinox?status.png)](https://godoc.org/github.com/equinox-io/equinox)
|
|
||||||
|
|
||||||
Package equinox allows applications to remotely update themselves with the [equinox.io](https://equinox.io) service.
|
|
||||||
|
|
||||||
## Minimal Working Example
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/equinox-io/equinox"
|
|
||||||
|
|
||||||
const appID = "<YOUR EQUINOX APP ID>"
|
|
||||||
|
|
||||||
var publicKey = []byte(`
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
|
|
||||||
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`)
|
|
||||||
|
|
||||||
func update(channel string) error {
|
|
||||||
opts := equinox.Options{Channel: channel}
|
|
||||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for the update
|
|
||||||
resp, err := equinox.Check(appID, opts)
|
|
||||||
switch {
|
|
||||||
case err == equinox.NotAvailableErr:
|
|
||||||
fmt.Println("No update available, already at the latest version!")
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch the update and apply it
|
|
||||||
err = resp.Apply()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Update To Specific Version
|
|
||||||
|
|
||||||
When you specify a channel in the update options, equinox will try to update the application
|
|
||||||
to the latest release of your application published to that channel. Instead, you may wish to
|
|
||||||
update the application to a specific (possibly older) version. You can do this by explicitly setting
|
|
||||||
Version in the Options struct:
|
|
||||||
|
|
||||||
```go
|
|
||||||
opts := equinox.Options{Version: "0.1.2"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prompt For Update
|
|
||||||
|
|
||||||
You may wish to ask the user for approval before updating to a new version. This is as simple
|
|
||||||
as calling the Check function and only calling Apply on the returned result if the user approves.
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// check for the update
|
|
||||||
resp, err := equinox.Check(appID, opts)
|
|
||||||
switch {
|
|
||||||
case err == equinox.NotAvailableErr:
|
|
||||||
fmt.Println("No update available, already at the latest version!")
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("New version available!")
|
|
||||||
fmt.Println("Version:", resp.ReleaseVersion)
|
|
||||||
fmt.Println("Name:", resp.ReleaseTitle)
|
|
||||||
fmt.Println("Details:", resp.ReleaseDescription)
|
|
||||||
|
|
||||||
ok := prompt("Would you like to update?")
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resp.Apply()
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Generating Keys
|
|
||||||
|
|
||||||
All equinox releases must be signed with a private ECDSA key, and all updates verified with the
|
|
||||||
public key portion. To do that, you'll need to generate a key pair. The equinox release tool can
|
|
||||||
generate an ecdsa key pair for you easily:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
equinox genkey
|
|
||||||
```
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
Package equinox allows applications to remotely update themselves with the equinox.io service.
|
|
||||||
|
|
||||||
Minimal Working Example
|
|
||||||
|
|
||||||
import "github.com/equinox-io/equinox"
|
|
||||||
|
|
||||||
const appID = "<YOUR EQUINOX APP ID>"
|
|
||||||
|
|
||||||
var publicKey = []byte(`
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
|
|
||||||
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`)
|
|
||||||
|
|
||||||
func update(channel string) error {
|
|
||||||
opts := equinox.Options{Channel: channel}
|
|
||||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for the update
|
|
||||||
resp, err := equinox.Check(appID, opts)
|
|
||||||
switch {
|
|
||||||
case err == equinox.NotAvailableErr:
|
|
||||||
fmt.Println("No update available, already at the latest version!")
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch the update and apply it
|
|
||||||
err = resp.Apply()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Update To Specific Version
|
|
||||||
|
|
||||||
When you specify a channel in the update options, equinox will try to update the application
|
|
||||||
to the latest release of your application published to that channel. Instead, you may wish to
|
|
||||||
update the application to a specific (possibly older) version. You can do this by explicitly setting
|
|
||||||
Version in the Options struct:
|
|
||||||
|
|
||||||
opts := equinox.Options{Version: "0.1.2"}
|
|
||||||
|
|
||||||
Prompt For Update
|
|
||||||
|
|
||||||
You may wish to ask the user for approval before updating to a new version. This is as simple
|
|
||||||
as calling the Check function and only calling Apply on the returned result if the user approves.
|
|
||||||
Example:
|
|
||||||
|
|
||||||
// check for the update
|
|
||||||
resp, err := equinox.Check(appID, opts)
|
|
||||||
switch {
|
|
||||||
case err == equinox.NotAvailableErr:
|
|
||||||
fmt.Println("No update available, already at the latest version!")
|
|
||||||
return nil
|
|
||||||
case err != nil:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("New version available!")
|
|
||||||
fmt.Println("Version:", resp.ReleaseVersion)
|
|
||||||
fmt.Println("Name:", resp.ReleaseTitle)
|
|
||||||
fmt.Println("Details:", resp.ReleaseDescription)
|
|
||||||
|
|
||||||
ok := prompt("Would you like to update?")
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resp.Apply()
|
|
||||||
// ...
|
|
||||||
|
|
||||||
Generating Keys
|
|
||||||
|
|
||||||
All equinox releases must be signed with a private ECDSA key, and all updates verified with the
|
|
||||||
public key portion. To do that, you'll need to generate a key pair. The equinox release tool can
|
|
||||||
generate an ecdsa key pair for you easily:
|
|
||||||
|
|
||||||
equinox genkey
|
|
||||||
|
|
||||||
*/
|
|
||||||
package equinox
|
|
@ -1,13 +0,0 @@
|
|||||||
Copyright 2015 Alan Shreve
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
@ -1,65 +0,0 @@
|
|||||||
# go-update: Build self-updating Go programs [![godoc reference](https://godoc.org/github.com/inconshreveable/go-update?status.png)](https://godoc.org/github.com/inconshreveable/go-update)
|
|
||||||
|
|
||||||
Package update provides functionality to implement secure, self-updating Go programs (or other single-file targets)
|
|
||||||
A program can update itself by replacing its executable file with a new version.
|
|
||||||
|
|
||||||
It provides the flexibility to implement different updating user experiences
|
|
||||||
like auto-updating, or manual user-initiated updates. It also boasts
|
|
||||||
advanced features like binary patching and code signing verification.
|
|
||||||
|
|
||||||
Example of updating from a URL:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/inconshreveable/go-update"
|
|
||||||
)
|
|
||||||
|
|
||||||
func doUpdate(url string) error {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
err := update.Apply(resp.Body, update.Options{})
|
|
||||||
if err != nil {
|
|
||||||
// error handling
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Cross platform support (Windows too!)
|
|
||||||
- Binary patch application
|
|
||||||
- Checksum verification
|
|
||||||
- Code signing verification
|
|
||||||
- Support for updating arbitrary files
|
|
||||||
|
|
||||||
## [equinox.io](https://equinox.io)
|
|
||||||
[equinox.io](https://equinox.io) is a complete ready-to-go updating solution built on top of go-update that provides:
|
|
||||||
|
|
||||||
- Hosted updates
|
|
||||||
- Update channels (stable, beta, nightly, ...)
|
|
||||||
- Dynamically computed binary diffs
|
|
||||||
- Automatic key generation and code
|
|
||||||
- Release tooling with proper code signing
|
|
||||||
- Update/download metrics
|
|
||||||
|
|
||||||
## API Compatibility Promises
|
|
||||||
The master branch of `go-update` is *not* guaranteed to have a stable API over time. For any production application, you should vendor
|
|
||||||
your dependency on `go-update` with a tool like git submodules, [gb](http://getgb.io/) or [govendor](https://github.com/kardianos/govendor).
|
|
||||||
|
|
||||||
The `go-update` package makes the following promises about API compatibility:
|
|
||||||
1. A list of all API-breaking changes will be documented in this README.
|
|
||||||
1. `go-update` will strive for as few API-breaking changes as possible.
|
|
||||||
|
|
||||||
## API Breaking Changes
|
|
||||||
- **Sept 3, 2015**: The `Options` struct passed to `Apply` was changed to be passed by value instead of passed by pointer. Old API at `28de026`.
|
|
||||||
- **Aug 9, 2015**: 2.0 API. Old API at `221d034` or `gopkg.in/inconshreveable/go-update.v0`.
|
|
||||||
|
|
||||||
## License
|
|
||||||
Apache
|
|
@ -1,322 +0,0 @@
|
|||||||
package update
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/equinox-io/equinox/internal/go-update/internal/osext"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
openFile = os.OpenFile
|
|
||||||
)
|
|
||||||
|
|
||||||
// Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.
|
|
||||||
//
|
|
||||||
// Apply performs the following actions to ensure a safe cross-platform update:
|
|
||||||
//
|
|
||||||
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
|
|
||||||
//
|
|
||||||
// 2. If configured, computes the checksum of the new executable and verifies it matches.
|
|
||||||
//
|
|
||||||
// 3. If configured, verifies the signature with a public key.
|
|
||||||
//
|
|
||||||
// 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
|
|
||||||
//
|
|
||||||
// 5. Renames /path/to/target to /path/to/.target.old
|
|
||||||
//
|
|
||||||
// 6. Renames /path/to/.target.new to /path/to/target
|
|
||||||
//
|
|
||||||
// 7. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
|
|
||||||
// the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
|
|
||||||
//
|
|
||||||
// 8. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
|
|
||||||
// back to /path/to/target.
|
|
||||||
//
|
|
||||||
// If the roll back operation fails, the file system is left in an inconsistent state (betweet steps 5 and 6) where
|
|
||||||
// there is no new executable file and the old executable file could not be be moved to its original location. In this
|
|
||||||
// case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
|
|
||||||
// the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
|
|
||||||
func Apply(update io.Reader, opts Options) error {
|
|
||||||
// validate
|
|
||||||
verify := false
|
|
||||||
switch {
|
|
||||||
case opts.Signature != nil && opts.PublicKey != nil:
|
|
||||||
// okay
|
|
||||||
verify = true
|
|
||||||
case opts.Signature != nil:
|
|
||||||
return errors.New("no public key to verify signature with")
|
|
||||||
case opts.PublicKey != nil:
|
|
||||||
return errors.New("No signature to verify with")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set defaults
|
|
||||||
if opts.Hash == 0 {
|
|
||||||
opts.Hash = crypto.SHA256
|
|
||||||
}
|
|
||||||
if opts.Verifier == nil {
|
|
||||||
opts.Verifier = NewECDSAVerifier()
|
|
||||||
}
|
|
||||||
if opts.TargetMode == 0 {
|
|
||||||
opts.TargetMode = 0755
|
|
||||||
}
|
|
||||||
|
|
||||||
// get target path
|
|
||||||
var err error
|
|
||||||
opts.TargetPath, err = opts.getPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBytes []byte
|
|
||||||
if opts.Patcher != nil {
|
|
||||||
if newBytes, err = opts.applyPatch(update); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no patch to apply, go on through
|
|
||||||
if newBytes, err = ioutil.ReadAll(update); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify checksum if requested
|
|
||||||
if opts.Checksum != nil {
|
|
||||||
if err = opts.verifyChecksum(newBytes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if verify {
|
|
||||||
if err = opts.verifySignature(newBytes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the directory the executable exists in
|
|
||||||
updateDir := filepath.Dir(opts.TargetPath)
|
|
||||||
filename := filepath.Base(opts.TargetPath)
|
|
||||||
|
|
||||||
// Copy the contents of newbinary to a new executable file
|
|
||||||
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
|
|
||||||
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fp.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(fp, bytes.NewReader(newBytes))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we don't call fp.Close(), windows won't let us move the new executable
|
|
||||||
// because the file will still be "in use"
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
// this is where we'll move the executable to so that we can swap in the updated replacement
|
|
||||||
oldPath := opts.OldSavePath
|
|
||||||
removeOld := opts.OldSavePath == ""
|
|
||||||
if removeOld {
|
|
||||||
oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete any existing old exec file - this is necessary on Windows for two reasons:
|
|
||||||
// 1. after a successful update, Windows can't remove the .old file because the process is still running
|
|
||||||
// 2. windows rename operations fail if the destination file already exists
|
|
||||||
_ = os.Remove(oldPath)
|
|
||||||
|
|
||||||
// move the existing executable to a new file in the same directory
|
|
||||||
err = os.Rename(opts.TargetPath, oldPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// move the new exectuable in to become the new program
|
|
||||||
err = os.Rename(newPath, opts.TargetPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// move unsuccessful
|
|
||||||
//
|
|
||||||
// The filesystem is now in a bad state. We have successfully
|
|
||||||
// moved the existing binary to a new location, but we couldn't move the new
|
|
||||||
// binary to take its place. That means there is no file where the current executable binary
|
|
||||||
// used to be!
|
|
||||||
// Try to rollback by restoring the old binary to its original path.
|
|
||||||
rerr := os.Rename(oldPath, opts.TargetPath)
|
|
||||||
if rerr != nil {
|
|
||||||
return &rollbackErr{err, rerr}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// move successful, remove the old binary if needed
|
|
||||||
if removeOld {
|
|
||||||
errRemove := os.Remove(oldPath)
|
|
||||||
|
|
||||||
// windows has trouble with removing old binaries, so hide it instead
|
|
||||||
if errRemove != nil {
|
|
||||||
_ = hideFile(oldPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollbackError takes an error value returned by Apply and returns the error, if any,
|
|
||||||
// that occurred when attempting to roll back from a failed update. Applications should
|
|
||||||
// always call this function on any non-nil errors returned by Apply.
|
|
||||||
//
|
|
||||||
// If no rollback was needed or if the rollback was successful, RollbackError returns nil,
|
|
||||||
// otherwise it returns the error encountered when trying to roll back.
|
|
||||||
func RollbackError(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if rerr, ok := err.(*rollbackErr); ok {
|
|
||||||
return rerr.rollbackErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type rollbackErr struct {
|
|
||||||
error // original error
|
|
||||||
rollbackErr error // error encountered while rolling back
|
|
||||||
}
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
// TargetPath defines the path to the file to update.
|
|
||||||
// The emptry string means 'the executable file of the running program'.
|
|
||||||
TargetPath string
|
|
||||||
|
|
||||||
// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
|
|
||||||
TargetMode os.FileMode
|
|
||||||
|
|
||||||
// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
|
|
||||||
Checksum []byte
|
|
||||||
|
|
||||||
// Public key to use for signature verification. If nil, no signature verification is done.
|
|
||||||
PublicKey crypto.PublicKey
|
|
||||||
|
|
||||||
// Signature to verify the updated file. If nil, no signature verification is done.
|
|
||||||
Signature []byte
|
|
||||||
|
|
||||||
// Pluggable signature verification algorithm. If nil, ECDSA is used.
|
|
||||||
Verifier Verifier
|
|
||||||
|
|
||||||
// Use this hash function to generate the checksum. If not set, SHA256 is used.
|
|
||||||
Hash crypto.Hash
|
|
||||||
|
|
||||||
// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
|
|
||||||
// If non-nil, treat the update contents as a patch and use this object to apply the patch.
|
|
||||||
Patcher Patcher
|
|
||||||
|
|
||||||
// Store the old executable file at this path after a successful update.
|
|
||||||
// The empty string means the old executable file will be removed after the update.
|
|
||||||
OldSavePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckPermissions determines whether the process has the correct permissions to
|
|
||||||
// perform the requested update. If the update can proceed, it returns nil, otherwise
|
|
||||||
// it returns the error that would occur if an update were attempted.
|
|
||||||
func (o *Options) CheckPermissions() error {
|
|
||||||
// get the directory the file exists in
|
|
||||||
path, err := o.getPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileDir := filepath.Dir(path)
|
|
||||||
fileName := filepath.Base(path)
|
|
||||||
|
|
||||||
// attempt to open a file in the file's directory
|
|
||||||
newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
|
|
||||||
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.TargetMode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fp.Close()
|
|
||||||
|
|
||||||
_ = os.Remove(newPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPublicKeyPEM is a convenience method to set the PublicKey property
|
|
||||||
// used for checking a completed update's signature by parsing a
|
|
||||||
// Public Key formatted as PEM data.
|
|
||||||
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
|
|
||||||
block, _ := pem.Decode(pembytes)
|
|
||||||
if block == nil {
|
|
||||||
return errors.New("couldn't parse PEM data")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.PublicKey = pub
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) getPath() (string, error) {
|
|
||||||
if o.TargetPath == "" {
|
|
||||||
return osext.Executable()
|
|
||||||
} else {
|
|
||||||
return o.TargetPath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) applyPatch(patch io.Reader) ([]byte, error) {
|
|
||||||
// open the file to patch
|
|
||||||
old, err := os.Open(o.TargetPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer old.Close()
|
|
||||||
|
|
||||||
// apply the patch
|
|
||||||
var applied bytes.Buffer
|
|
||||||
if err = o.Patcher.Patch(old, &applied, patch); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return applied.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) verifyChecksum(updated []byte) error {
|
|
||||||
checksum, err := checksumFor(o.Hash, updated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(o.Checksum, checksum) {
|
|
||||||
return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) verifySignature(updated []byte) error {
|
|
||||||
checksum, err := checksumFor(o.Hash, updated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
|
|
||||||
if !h.Available() {
|
|
||||||
return nil, errors.New("requested hash function not available")
|
|
||||||
}
|
|
||||||
hash := h.New()
|
|
||||||
hash.Write(payload) // guaranteed not to error
|
|
||||||
return hash.Sum([]byte{}), nil
|
|
||||||
}
|
|
@ -1,426 +0,0 @@
|
|||||||
package update
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/equinox-io/equinox/internal/go-update/internal/binarydist"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
oldFile = []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
|
||||||
newFile = []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
|
|
||||||
newFileChecksum = sha256.Sum256(newFile)
|
|
||||||
)
|
|
||||||
|
|
||||||
func cleanup(path string) {
|
|
||||||
os.Remove(path)
|
|
||||||
os.Remove(fmt.Sprintf(".%s.new", path))
|
|
||||||
}
|
|
||||||
|
|
||||||
// we write with a separate name for each test so that we can run them in parallel
|
|
||||||
func writeOldFile(path string, t *testing.T) {
|
|
||||||
if err := ioutil.WriteFile(path, oldFile, 0777); err != nil {
|
|
||||||
t.Fatalf("Failed to write file for testing preparation: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateUpdate(path string, err error, t *testing.T) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to update: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read file post-update: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(buf, newFile) {
|
|
||||||
t.Fatalf("File was not updated! Bytes read: %v, Bytes expected: %v", buf, newFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplySimple(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestApplySimple"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
})
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplyOldSavePath(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestApplyOldSavePath"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
oldfName := "OldSavePath"
|
|
||||||
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
OldSavePath: oldfName,
|
|
||||||
})
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
|
|
||||||
if _, err := os.Stat(oldfName); os.IsNotExist(err) {
|
|
||||||
t.Fatalf("Failed to find the old file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(oldfName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyChecksum(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifyChecksum"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Checksum: newFileChecksum[:],
|
|
||||||
})
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyChecksumNegative(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifyChecksumNegative"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
badChecksum := []byte{0x0A, 0x0B, 0x0C, 0xFF}
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Checksum: badChecksum,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Failed to detect bad checksum!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplyPatch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestApplyPatch"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
patch := new(bytes.Buffer)
|
|
||||||
err := binarydist.Diff(bytes.NewReader(oldFile), bytes.NewReader(newFile), patch)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create patch: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Apply(patch, Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Patcher: NewBSDiffPatcher(),
|
|
||||||
})
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCorruptPatch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestCorruptPatch"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
badPatch := []byte{0x44, 0x38, 0x86, 0x3c, 0x4f, 0x8d, 0x26, 0x54, 0xb, 0x11, 0xce, 0xfe, 0xc1, 0xc0, 0xf8, 0x31, 0x38, 0xa0, 0x12, 0x1a, 0xa2, 0x57, 0x2a, 0xe1, 0x3a, 0x48, 0x62, 0x40, 0x2b, 0x81, 0x12, 0xb1, 0x21, 0xa5, 0x16, 0xed, 0x73, 0xd6, 0x54, 0x84, 0x29, 0xa6, 0xd6, 0xb2, 0x1b, 0xfb, 0xe6, 0xbe, 0x7b, 0x70}
|
|
||||||
err := Apply(bytes.NewReader(badPatch), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Patcher: NewBSDiffPatcher(),
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Failed to detect corrupt patch!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyChecksumPatchNegative(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifyChecksumPatchNegative"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
patch := new(bytes.Buffer)
|
|
||||||
anotherFile := []byte{0x77, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
|
||||||
err := binarydist.Diff(bytes.NewReader(oldFile), bytes.NewReader(anotherFile), patch)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create patch: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Apply(patch, Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Checksum: newFileChecksum[:],
|
|
||||||
Patcher: NewBSDiffPatcher(),
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Failed to detect patch to wrong file!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ecdsaPublicKey = `
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEL8ThbSyEucsCxnd4dCZR2hIy5nea54ko
|
|
||||||
O+jUUfIjkvwhCWzASm0lpCVdVpXKZXIe+NZ+44RQRv3+OqJkCCGzUgJkPNI3lxdG
|
|
||||||
9zu8rbrnxISV06VQ8No7Ei9wiTpqmTBB
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
const ecdsaPrivateKey = `
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MIGkAgEBBDBttCB/1NOY4T+WrG4FSV49Ayn3gK1DNzfGaJ01JUXeiNFCWQM2pqpU
|
|
||||||
om8ATPP/dkegBwYFK4EEACKhZANiAAQvxOFtLIS5ywLGd3h0JlHaEjLmd5rniSg7
|
|
||||||
6NRR8iOS/CEJbMBKbSWkJV1Wlcplch741n7jhFBG/f46omQIIbNSAmQ80jeXF0b3
|
|
||||||
O7ytuufEhJXTpVDw2jsSL3CJOmqZMEE=
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
const rsaPublicKey = `
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxSWmu7trWKAwDFjiCN2D
|
|
||||||
Tk2jj2sgcr/CMlI4cSSiIOHrXCFxP1I8i9PvQkd4hasXQrLbT5WXKrRGv1HKUKab
|
|
||||||
b9ead+kD0kxk7i2bFYvKX43oq66IW0mOLTQBO7I9UyT4L7svcMD+HUQ2BqHoaQe4
|
|
||||||
y20C59dPr9Dpcz8DZkdLsBV6YKF6Ieb3iGk8oRLMWNaUqPa8f1BGgxAkvPHcqDjT
|
|
||||||
x4xRnjgTRRRlZvRtALHMUkIChgxDOhoEzKpGiqnX7HtMJfrhV6h0PAXNA4h9Kjv5
|
|
||||||
5fhJ08Rz7mmZmtH5JxTK5XTquo59sihSajR4bSjZbbkQ1uLkeFlY3eli3xdQ7Nrf
|
|
||||||
fQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----`
|
|
||||||
|
|
||||||
const rsaPrivateKey = `
|
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEogIBAAKCAQEAxSWmu7trWKAwDFjiCN2DTk2jj2sgcr/CMlI4cSSiIOHrXCFx
|
|
||||||
P1I8i9PvQkd4hasXQrLbT5WXKrRGv1HKUKabb9ead+kD0kxk7i2bFYvKX43oq66I
|
|
||||||
W0mOLTQBO7I9UyT4L7svcMD+HUQ2BqHoaQe4y20C59dPr9Dpcz8DZkdLsBV6YKF6
|
|
||||||
Ieb3iGk8oRLMWNaUqPa8f1BGgxAkvPHcqDjTx4xRnjgTRRRlZvRtALHMUkIChgxD
|
|
||||||
OhoEzKpGiqnX7HtMJfrhV6h0PAXNA4h9Kjv55fhJ08Rz7mmZmtH5JxTK5XTquo59
|
|
||||||
sihSajR4bSjZbbkQ1uLkeFlY3eli3xdQ7NrffQIDAQABAoIBAAkN+6RvrTR61voa
|
|
||||||
Mvd5RQiZpEN4Bht/Fyo8gH8h0Zh1B9xJZOwlmMZLS5fdtHlfLEhR8qSrGDBL61vq
|
|
||||||
I8KkhEsUufF78EL+YzxVN+Q7cWYGHIOWFokqza7hzpSxUQO6lPOMQ1eIZaNueJTB
|
|
||||||
Zu07/47ISPPg/bXzgGVcpYlTCPTjUwKjtfyMqvX9AD7fIyYRm6zfE7EHj1J2sBFt
|
|
||||||
Yz1OGELg6HfJwXfpnPfBvftD0hWGzJ78Bp71fPJe6n5gnqmSqRvrcXNWFnH/yqkN
|
|
||||||
d6vPIxD6Z3LjvyZpkA7JillLva2L/zcIFhg4HZvQnWd8/PpDnUDonu36hcj4SC5j
|
|
||||||
W4aVPLkCgYEA4XzNKWxqYcajzFGZeSxlRHupSAl2MT7Cc5085MmE7dd31wK2T8O4
|
|
||||||
n7N4bkm/rjTbX85NsfWdKtWb6mpp8W3VlLP0rp4a/12OicVOkg4pv9LZDmY0sRlE
|
|
||||||
YuDJk1FeCZ50UrwTZI3rZ9IhZHhkgVA6uWAs7tYndONkxNHG0pjqs4sCgYEA39MZ
|
|
||||||
JwMqo3qsPntpgP940cCLflEsjS9hYNO3+Sv8Dq3P0HLVhBYajJnotf8VuU0fsQZG
|
|
||||||
grmtVn1yThFbMq7X1oY4F0XBA+paSiU18c4YyUnwax2u4sw9U/Q9tmQUZad5+ueT
|
|
||||||
qriMBwGv+ewO+nQxqvAsMUmemrVzrfwA5Oct+hcCgYAfiyXoNZJsOy2O15twqBVC
|
|
||||||
j0oPGcO+/9iT89sg5lACNbI+EdMPNYIOVTzzsL1v0VUfAe08h++Enn1BPcG0VHkc
|
|
||||||
ZFBGXTfJoXzfKQrkw7ZzbzuOGB4m6DH44xlP0oIlNlVvfX/5ASF9VJf3RiBJNsAA
|
|
||||||
TsP6ZVr/rw/ZuL7nlxy+IQKBgDhL/HOXlE3yOQiuOec8WsNHTs7C1BXe6PtVxVxi
|
|
||||||
988pYK/pclL6zEq5G5NLSceF4obAMVQIJ9UtUGbabrncyGUo9UrFPLsjYvprSZo8
|
|
||||||
YHegpVwL50UcYgCP2kXZ/ldjPIcjYDz8lhvdDMor2cidGTEJn9P11HLNWP9V91Ob
|
|
||||||
4jCZAoGAPNRSC5cC8iP/9j+s2/kdkfWJiNaolPYAUrmrkL6H39PYYZM5tnhaIYJV
|
|
||||||
Oh9AgABamU0eb3p3vXTISClVgV7ifq1HyZ7BSUhMfaY2Jk/s3sUHCWFxPZe9sgEG
|
|
||||||
KinIY/373KIkIV/5g4h2v1w330IWcfptxKcY/Er3DJr38f695GE=
|
|
||||||
-----END RSA PRIVATE KEY-----`
|
|
||||||
|
|
||||||
func signec(privatePEM string, source []byte, t *testing.T) []byte {
|
|
||||||
parseFn := func(p []byte) (crypto.Signer, error) { return x509.ParseECPrivateKey(p) }
|
|
||||||
return sign(parseFn, privatePEM, source, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func signrsa(privatePEM string, source []byte, t *testing.T) []byte {
|
|
||||||
parseFn := func(p []byte) (crypto.Signer, error) { return x509.ParsePKCS1PrivateKey(p) }
|
|
||||||
return sign(parseFn, privatePEM, source, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sign(parsePrivKey func([]byte) (crypto.Signer, error), privatePEM string, source []byte, t *testing.T) []byte {
|
|
||||||
block, _ := pem.Decode([]byte(privatePEM))
|
|
||||||
if block == nil {
|
|
||||||
t.Fatalf("Failed to parse private key PEM")
|
|
||||||
}
|
|
||||||
|
|
||||||
priv, err := parsePrivKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse private key DER: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
checksum := sha256.Sum256(source)
|
|
||||||
sig, err := priv.Sign(rand.Reader, checksum[:], crypto.SHA256)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to sign: %v", sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sig
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyECSignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifySignature"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{TargetPath: fName}
|
|
||||||
err := opts.SetPublicKeyPEM([]byte(ecdsaPublicKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Signature = signec(ecdsaPrivateKey, newFile, t)
|
|
||||||
err = Apply(bytes.NewReader(newFile), opts)
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyRSASignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifySignature"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Verifier: NewRSAVerifier(),
|
|
||||||
}
|
|
||||||
err := opts.SetPublicKeyPEM([]byte(rsaPublicKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Signature = signrsa(rsaPrivateKey, newFile, t)
|
|
||||||
err = Apply(bytes.NewReader(newFile), opts)
|
|
||||||
validateUpdate(fName, err, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyFailBadSignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifyFailBadSignature"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Signature: []byte{0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA},
|
|
||||||
}
|
|
||||||
err := opts.SetPublicKeyPEM([]byte(ecdsaPublicKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Apply(bytes.NewReader(newFile), opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Did not fail with bad signature")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyFailNoSignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifySignatureWithPEM"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{TargetPath: fName}
|
|
||||||
err := opts.SetPublicKeyPEM([]byte(ecdsaPublicKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Apply(bytes.NewReader(newFile), opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Did not fail with empty signature")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrongKey = `
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MIGkAgEBBDBzqYp6N2s8YWYifBjS03/fFfmGeIPcxQEi+bbFeekIYt8NIKIkhD+r
|
|
||||||
hpaIwSmot+qgBwYFK4EEACKhZANiAAR0EC8Usbkc4k30frfEB2ECmsIghu9DJSqE
|
|
||||||
RbH7jfq2ULNv8tN/clRjxf2YXgp+iP3SQF1R1EYERKpWr8I57pgfIZtoZXjwpbQC
|
|
||||||
VBbP/Ff+05HOqwPC7rJMy1VAJLKg7Cw=
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestVerifyFailWrongSignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestVerifyFailWrongSignature"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{TargetPath: fName}
|
|
||||||
err := opts.SetPublicKeyPEM([]byte(ecdsaPublicKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Signature = signec(wrongKey, newFile, t)
|
|
||||||
err = Apply(bytes.NewReader(newFile), opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Verified an update that was signed by an untrusted key!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignatureButNoPublicKey(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestSignatureButNoPublicKey"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{
|
|
||||||
TargetPath: fName,
|
|
||||||
Signature: signec(ecdsaPrivateKey, newFile, t),
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Allowed an update with a signautre verification when no public key was specified!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublicKeyButNoSignature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fName := "TestPublicKeyButNoSignature"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
opts := Options{TargetPath: fName}
|
|
||||||
if err := opts.SetPublicKeyPEM([]byte(ecdsaPublicKey)); err != nil {
|
|
||||||
t.Fatalf("Could not parse public key: %v", err)
|
|
||||||
}
|
|
||||||
err := Apply(bytes.NewReader(newFile), opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Allowed an update with no signautre when a public key was specified!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
fName := "TestWriteError"
|
|
||||||
defer cleanup(fName)
|
|
||||||
writeOldFile(fName, t)
|
|
||||||
|
|
||||||
openFile = func(name string, flags int, perm os.FileMode) (*os.File, error) {
|
|
||||||
f, err := os.OpenFile(name, flags, perm)
|
|
||||||
|
|
||||||
// simulate Write() error by closing the file prematurely
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Apply(bytes.NewReader(newFile), Options{TargetPath: fName})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Allowed an update to an empty file")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
/*
|
|
||||||
Package update provides functionality to implement secure, self-updating Go programs (or other single-file targets).
|
|
||||||
|
|
||||||
For complete updating solutions please see Equinox (https://equinox.io) and go-tuf (https://github.com/flynn/go-tuf).
|
|
||||||
|
|
||||||
Basic Example
|
|
||||||
|
|
||||||
This example shows how to update a program remotely from a URL.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/inconshreveable/go-update"
|
|
||||||
)
|
|
||||||
|
|
||||||
func doUpdate(url string) error {
|
|
||||||
// request the new file
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
err := update.Apply(resp.Body, update.Options{})
|
|
||||||
if err != nil {
|
|
||||||
if rerr := update.RollbackError(err); rerr != nil {
|
|
||||||
fmt.Println("Failed to rollback from bad update: %v", rerr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Binary Patching
|
|
||||||
|
|
||||||
Go binaries can often be large. It can be advantageous to only ship a binary patch to a client
|
|
||||||
instead of the complete program text of a new version.
|
|
||||||
|
|
||||||
This example shows how to update a program with a bsdiff binary patch. Other patch formats
|
|
||||||
may be applied by implementing the Patcher interface.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/inconshreveable/go-update"
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateWithPatch(patch io.Reader) error {
|
|
||||||
err := update.Apply(patch, update.Options{
|
|
||||||
Patcher: update.NewBSDiffPatcher()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// error handling
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Checksum Verification
|
|
||||||
|
|
||||||
Updating executable code on a computer can be a dangerous operation unless you
|
|
||||||
take the appropriate steps to guarantee the authenticity of the new code. While
|
|
||||||
checksum verification is important, it should always be combined with signature
|
|
||||||
verification (next section) to guarantee that the code came from a trusted party.
|
|
||||||
|
|
||||||
go-update validates SHA256 checksums by default, but this is pluggable via the Hash
|
|
||||||
property on the Options struct.
|
|
||||||
|
|
||||||
This example shows how to guarantee that the newly-updated binary is verified to
|
|
||||||
have an appropriate checksum (that was otherwise retrived via a secure channel)
|
|
||||||
specified as a hex string.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
_ "crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/inconshreveable/go-update"
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateWithChecksum(binary io.Reader, hexChecksum string) error {
|
|
||||||
checksum, err := hex.DecodeString(hexChecksum)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = update.Apply(binary, update.Options{
|
|
||||||
Hash: crypto.SHA256, // this is the default, you don't need to specify it
|
|
||||||
Checksum: checksum,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// error handling
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Cryptographic Signature Verification
|
|
||||||
|
|
||||||
Cryptographic verification of new code from an update is an extremely important way to guarantee the
|
|
||||||
security and integrity of your updates.
|
|
||||||
|
|
||||||
Verification is performed by validating the signature of a hash of the new file. This
|
|
||||||
means nothing changes if you apply your update with a patch.
|
|
||||||
|
|
||||||
This example shows how to add signature verification to your updates. To make all of this work
|
|
||||||
an application distributor must first create a public/private key pair and embed the public key
|
|
||||||
into their application. When they issue a new release, the issuer must sign the new executable file
|
|
||||||
with the private key and distribute the signature along with the update.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
_ "crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/inconshreveable/go-update"
|
|
||||||
)
|
|
||||||
|
|
||||||
var publicKey = []byte(`
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
|
|
||||||
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`)
|
|
||||||
|
|
||||||
func verifiedUpdate(binary io.Reader, hexChecksum, hexSignature string) {
|
|
||||||
checksum, err := hex.DecodeString(hexChecksum)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
signature, err := hex.DecodeString(hexSignature)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opts := update.Options{
|
|
||||||
Checksum: checksum,
|
|
||||||
Signature: signature,
|
|
||||||
Hash: crypto.SHA256, // this is the default, you don't need to specify it
|
|
||||||
Verifier: update.NewECDSAVerifier(), // this is the default, you don't need to specify it
|
|
||||||
}
|
|
||||||
err = opts.SetPublicKeyPEM(publicKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = update.Apply(binary, opts)
|
|
||||||
if err != nil {
|
|
||||||
// error handling
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Building Single-File Go Binaries
|
|
||||||
|
|
||||||
In order to update a Go application with go-update, you must distributed it as a single executable.
|
|
||||||
This is often easy, but some applications require static assets (like HTML and CSS asset files or TLS certificates).
|
|
||||||
In order to update applications like these, you'll want to make sure to embed those asset files into
|
|
||||||
the distributed binary with a tool like go-bindata (my favorite): https://github.com/jteeuwen/go-bindata
|
|
||||||
|
|
||||||
Non-Goals
|
|
||||||
|
|
||||||
Mechanisms and protocols for determining whether an update should be applied and, if so, which one are
|
|
||||||
out of scope for this package. Please consult go-tuf (https://github.com/flynn/go-tuf) or Equinox (https://equinox.io)
|
|
||||||
for more complete solutions.
|
|
||||||
|
|
||||||
go-update only works for self-updating applications that are distributed as a single binary, i.e.
|
|
||||||
applications that do not have additional assets or dependency files.
|
|
||||||
Updating application that are distributed as mutliple on-disk files is out of scope, although this
|
|
||||||
may change in future versions of this library.
|
|
||||||
|
|
||||||
*/
|
|
||||||
package update
|
|
@ -1,7 +0,0 @@
|
|||||||
// +build !windows
|
|
||||||
|
|
||||||
package update
|
|
||||||
|
|
||||||
func hideFile(path string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package update
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func hideFile(path string) error {
|
|
||||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
|
||||||
setFileAttributes := kernel32.NewProc("SetFileAttributesW")
|
|
||||||
|
|
||||||
r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 2)
|
|
||||||
|
|
||||||
if r1 == 0 {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
22
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/License
generated
vendored
22
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/License
generated
vendored
@ -1,22 +0,0 @@
|
|||||||
Copyright 2012 Keith Rarick
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
|
||||||
obtaining a copy of this software and associated documentation
|
|
||||||
files (the "Software"), to deal in the Software without
|
|
||||||
restriction, including without limitation the rights to use,
|
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,7 +0,0 @@
|
|||||||
# binarydist
|
|
||||||
|
|
||||||
Package binarydist implements binary diff and patch as described on
|
|
||||||
<http://www.daemonology.net/bsdiff/>. It reads and writes files
|
|
||||||
compatible with the tools there.
|
|
||||||
|
|
||||||
Documentation at <http://go.pkgdoc.org/github.com/kr/binarydist>.
|
|
40
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/bzip2.go
generated
vendored
40
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/bzip2.go
generated
vendored
@ -1,40 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type bzip2Writer struct {
|
|
||||||
c *exec.Cmd
|
|
||||||
w io.WriteCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w bzip2Writer) Write(b []byte) (int, error) {
|
|
||||||
return w.w.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w bzip2Writer) Close() error {
|
|
||||||
if err := w.w.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.c.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Package compress/bzip2 implements only decompression,
|
|
||||||
// so we'll fake it by running bzip2 in another process.
|
|
||||||
func newBzip2Writer(w io.Writer) (wc io.WriteCloser, err error) {
|
|
||||||
var bw bzip2Writer
|
|
||||||
bw.c = exec.Command("bzip2", "-c")
|
|
||||||
bw.c.Stdout = w
|
|
||||||
|
|
||||||
if bw.w, err = bw.c.StdinPipe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = bw.c.Start(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bw, nil
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustOpen(path string) *os.File {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustReadAll(r io.Reader) []byte {
|
|
||||||
b, err := ioutil.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileCmp(a, b *os.File) int64 {
|
|
||||||
sa, err := a.Seek(0, 2)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb, err := b.Seek(0, 2)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sa != sb {
|
|
||||||
return sa
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = b.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pa, err := ioutil.ReadAll(a)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pb, err := ioutil.ReadAll(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range pa {
|
|
||||||
if pa[i] != pb[i] {
|
|
||||||
return int64(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustWriteRandFile(path string, size int) *os.File {
|
|
||||||
p := make([]byte, size)
|
|
||||||
_, err := rand.Read(p)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Write(p)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
408
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/diff.go
generated
vendored
408
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/diff.go
generated
vendored
@ -1,408 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func swap(a []int, i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
|
|
||||||
func split(I, V []int, start, length, h int) {
|
|
||||||
var i, j, k, x, jj, kk int
|
|
||||||
|
|
||||||
if length < 16 {
|
|
||||||
for k = start; k < start+length; k += j {
|
|
||||||
j = 1
|
|
||||||
x = V[I[k]+h]
|
|
||||||
for i = 1; k+i < start+length; i++ {
|
|
||||||
if V[I[k+i]+h] < x {
|
|
||||||
x = V[I[k+i]+h]
|
|
||||||
j = 0
|
|
||||||
}
|
|
||||||
if V[I[k+i]+h] == x {
|
|
||||||
swap(I, k+i, k+j)
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i = 0; i < j; i++ {
|
|
||||||
V[I[k+i]] = k + j - 1
|
|
||||||
}
|
|
||||||
if j == 1 {
|
|
||||||
I[k] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
x = V[I[start+length/2]+h]
|
|
||||||
jj = 0
|
|
||||||
kk = 0
|
|
||||||
for i = start; i < start+length; i++ {
|
|
||||||
if V[I[i]+h] < x {
|
|
||||||
jj++
|
|
||||||
}
|
|
||||||
if V[I[i]+h] == x {
|
|
||||||
kk++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jj += start
|
|
||||||
kk += jj
|
|
||||||
|
|
||||||
i = start
|
|
||||||
j = 0
|
|
||||||
k = 0
|
|
||||||
for i < jj {
|
|
||||||
if V[I[i]+h] < x {
|
|
||||||
i++
|
|
||||||
} else if V[I[i]+h] == x {
|
|
||||||
swap(I, i, jj+j)
|
|
||||||
j++
|
|
||||||
} else {
|
|
||||||
swap(I, i, kk+k)
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for jj+j < kk {
|
|
||||||
if V[I[jj+j]+h] == x {
|
|
||||||
j++
|
|
||||||
} else {
|
|
||||||
swap(I, jj+j, kk+k)
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if jj > start {
|
|
||||||
split(I, V, start, jj-start, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 0; i < kk-jj; i++ {
|
|
||||||
V[I[jj+i]] = kk - 1
|
|
||||||
}
|
|
||||||
if jj == kk-1 {
|
|
||||||
I[jj] = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if start+length > kk {
|
|
||||||
split(I, V, kk, start+length-kk, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func qsufsort(obuf []byte) []int {
|
|
||||||
var buckets [256]int
|
|
||||||
var i, h int
|
|
||||||
I := make([]int, len(obuf)+1)
|
|
||||||
V := make([]int, len(obuf)+1)
|
|
||||||
|
|
||||||
for _, c := range obuf {
|
|
||||||
buckets[c]++
|
|
||||||
}
|
|
||||||
for i = 1; i < 256; i++ {
|
|
||||||
buckets[i] += buckets[i-1]
|
|
||||||
}
|
|
||||||
copy(buckets[1:], buckets[:])
|
|
||||||
buckets[0] = 0
|
|
||||||
|
|
||||||
for i, c := range obuf {
|
|
||||||
buckets[c]++
|
|
||||||
I[buckets[c]] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
I[0] = len(obuf)
|
|
||||||
for i, c := range obuf {
|
|
||||||
V[i] = buckets[c]
|
|
||||||
}
|
|
||||||
|
|
||||||
V[len(obuf)] = 0
|
|
||||||
for i = 1; i < 256; i++ {
|
|
||||||
if buckets[i] == buckets[i-1]+1 {
|
|
||||||
I[buckets[i]] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
I[0] = -1
|
|
||||||
|
|
||||||
for h = 1; I[0] != -(len(obuf) + 1); h += h {
|
|
||||||
var n int
|
|
||||||
for i = 0; i < len(obuf)+1; {
|
|
||||||
if I[i] < 0 {
|
|
||||||
n -= I[i]
|
|
||||||
i -= I[i]
|
|
||||||
} else {
|
|
||||||
if n != 0 {
|
|
||||||
I[i-n] = -n
|
|
||||||
}
|
|
||||||
n = V[I[i]] + 1 - i
|
|
||||||
split(I, V, i, n, h)
|
|
||||||
i += n
|
|
||||||
n = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n != 0 {
|
|
||||||
I[i-n] = -n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 0; i < len(obuf)+1; i++ {
|
|
||||||
I[V[i]] = i
|
|
||||||
}
|
|
||||||
return I
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchlen(a, b []byte) (i int) {
|
|
||||||
for i < len(a) && i < len(b) && a[i] == b[i] {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
func search(I []int, obuf, nbuf []byte, st, en int) (pos, n int) {
|
|
||||||
if en-st < 2 {
|
|
||||||
x := matchlen(obuf[I[st]:], nbuf)
|
|
||||||
y := matchlen(obuf[I[en]:], nbuf)
|
|
||||||
|
|
||||||
if x > y {
|
|
||||||
return I[st], x
|
|
||||||
} else {
|
|
||||||
return I[en], y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x := st + (en-st)/2
|
|
||||||
if bytes.Compare(obuf[I[x]:], nbuf) < 0 {
|
|
||||||
return search(I, obuf, nbuf, x, en)
|
|
||||||
} else {
|
|
||||||
return search(I, obuf, nbuf, st, x)
|
|
||||||
}
|
|
||||||
panic("unreached")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diff computes the difference between old and new, according to the bsdiff
|
|
||||||
// algorithm, and writes the result to patch.
|
|
||||||
func Diff(old, new io.Reader, patch io.Writer) error {
|
|
||||||
obuf, err := ioutil.ReadAll(old)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nbuf, err := ioutil.ReadAll(new)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pbuf, err := diffBytes(obuf, nbuf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = patch.Write(pbuf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func diffBytes(obuf, nbuf []byte) ([]byte, error) {
|
|
||||||
var patch seekBuffer
|
|
||||||
err := diff(obuf, nbuf, &patch)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return patch.buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func diff(obuf, nbuf []byte, patch io.WriteSeeker) error {
|
|
||||||
var lenf int
|
|
||||||
I := qsufsort(obuf)
|
|
||||||
db := make([]byte, len(nbuf))
|
|
||||||
eb := make([]byte, len(nbuf))
|
|
||||||
var dblen, eblen int
|
|
||||||
|
|
||||||
var hdr header
|
|
||||||
hdr.Magic = magic
|
|
||||||
hdr.NewSize = int64(len(nbuf))
|
|
||||||
err := binary.Write(patch, signMagLittleEndian{}, &hdr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the differences, writing ctrl as we go
|
|
||||||
pfbz2, err := newBzip2Writer(patch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var scan, pos, length int
|
|
||||||
var lastscan, lastpos, lastoffset int
|
|
||||||
for scan < len(nbuf) {
|
|
||||||
var oldscore int
|
|
||||||
scan += length
|
|
||||||
for scsc := scan; scan < len(nbuf); scan++ {
|
|
||||||
pos, length = search(I, obuf, nbuf[scan:], 0, len(obuf))
|
|
||||||
|
|
||||||
for ; scsc < scan+length; scsc++ {
|
|
||||||
if scsc+lastoffset < len(obuf) &&
|
|
||||||
obuf[scsc+lastoffset] == nbuf[scsc] {
|
|
||||||
oldscore++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length == oldscore && length != 0) || length > oldscore+8 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if scan+lastoffset < len(obuf) && obuf[scan+lastoffset] == nbuf[scan] {
|
|
||||||
oldscore--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if length != oldscore || scan == len(nbuf) {
|
|
||||||
var s, Sf int
|
|
||||||
lenf = 0
|
|
||||||
for i := 0; lastscan+i < scan && lastpos+i < len(obuf); {
|
|
||||||
if obuf[lastpos+i] == nbuf[lastscan+i] {
|
|
||||||
s++
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
if s*2-i > Sf*2-lenf {
|
|
||||||
Sf = s
|
|
||||||
lenf = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lenb := 0
|
|
||||||
if scan < len(nbuf) {
|
|
||||||
var s, Sb int
|
|
||||||
for i := 1; (scan >= lastscan+i) && (pos >= i); i++ {
|
|
||||||
if obuf[pos-i] == nbuf[scan-i] {
|
|
||||||
s++
|
|
||||||
}
|
|
||||||
if s*2-i > Sb*2-lenb {
|
|
||||||
Sb = s
|
|
||||||
lenb = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastscan+lenf > scan-lenb {
|
|
||||||
overlap := (lastscan + lenf) - (scan - lenb)
|
|
||||||
s := 0
|
|
||||||
Ss := 0
|
|
||||||
lens := 0
|
|
||||||
for i := 0; i < overlap; i++ {
|
|
||||||
if nbuf[lastscan+lenf-overlap+i] == obuf[lastpos+lenf-overlap+i] {
|
|
||||||
s++
|
|
||||||
}
|
|
||||||
if nbuf[scan-lenb+i] == obuf[pos-lenb+i] {
|
|
||||||
s--
|
|
||||||
}
|
|
||||||
if s > Ss {
|
|
||||||
Ss = s
|
|
||||||
lens = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lenf += lens - overlap
|
|
||||||
lenb -= lens
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < lenf; i++ {
|
|
||||||
db[dblen+i] = nbuf[lastscan+i] - obuf[lastpos+i]
|
|
||||||
}
|
|
||||||
for i := 0; i < (scan-lenb)-(lastscan+lenf); i++ {
|
|
||||||
eb[eblen+i] = nbuf[lastscan+lenf+i]
|
|
||||||
}
|
|
||||||
|
|
||||||
dblen += lenf
|
|
||||||
eblen += (scan - lenb) - (lastscan + lenf)
|
|
||||||
|
|
||||||
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(lenf))
|
|
||||||
if err != nil {
|
|
||||||
pfbz2.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
val := (scan - lenb) - (lastscan + lenf)
|
|
||||||
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
|
|
||||||
if err != nil {
|
|
||||||
pfbz2.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
val = (pos - lenb) - (lastpos + lenf)
|
|
||||||
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
|
|
||||||
if err != nil {
|
|
||||||
pfbz2.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastscan = scan - lenb
|
|
||||||
lastpos = pos - lenb
|
|
||||||
lastoffset = pos - scan
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = pfbz2.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute size of compressed ctrl data
|
|
||||||
l64, err := patch.Seek(0, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hdr.CtrlLen = int64(l64 - 32)
|
|
||||||
|
|
||||||
// Write compressed diff data
|
|
||||||
pfbz2, err = newBzip2Writer(patch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err := pfbz2.Write(db[:dblen])
|
|
||||||
if err != nil {
|
|
||||||
pfbz2.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n != dblen {
|
|
||||||
pfbz2.Close()
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
err = pfbz2.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute size of compressed diff data
|
|
||||||
n64, err := patch.Seek(0, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hdr.DiffLen = n64 - l64
|
|
||||||
|
|
||||||
// Write compressed extra data
|
|
||||||
pfbz2, err = newBzip2Writer(patch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n, err = pfbz2.Write(eb[:eblen])
|
|
||||||
if err != nil {
|
|
||||||
pfbz2.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n != eblen {
|
|
||||||
pfbz2.Close()
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
err = pfbz2.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek to the beginning, write the header, and close the file
|
|
||||||
_, err = patch.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(patch, signMagLittleEndian{}, &hdr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
67
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/diff_test.go
generated
vendored
67
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/diff_test.go
generated
vendored
@ -1,67 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var diffT = []struct {
|
|
||||||
old *os.File
|
|
||||||
new *os.File
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
old: mustWriteRandFile("test.old", 1e3),
|
|
||||||
new: mustWriteRandFile("test.new", 1e3),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
old: mustOpen("testdata/sample.old"),
|
|
||||||
new: mustOpen("testdata/sample.new"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiff(t *testing.T) {
|
|
||||||
for _, s := range diffT {
|
|
||||||
got, err := ioutil.TempFile("/tmp", "bspatch.")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Remove(got.Name())
|
|
||||||
|
|
||||||
exp, err := ioutil.TempFile("/tmp", "bspatch.")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("bsdiff", s.old.Name(), s.new.Name(), exp.Name())
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
err = cmd.Run()
|
|
||||||
os.Remove(exp.Name())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Diff(s.old, s.new, got)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = got.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
gotBuf := mustReadAll(got)
|
|
||||||
expBuf := mustReadAll(exp)
|
|
||||||
|
|
||||||
if !bytes.Equal(gotBuf, expBuf) {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf("diff %s %s", s.old.Name(), s.new.Name())
|
|
||||||
t.Logf("%s: len(got) = %d", got.Name(), len(gotBuf))
|
|
||||||
t.Logf("%s: len(exp) = %d", exp.Name(), len(expBuf))
|
|
||||||
i := matchlen(gotBuf, expBuf)
|
|
||||||
t.Logf("produced different output at pos %d; %d != %d", i, gotBuf[i], expBuf[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
24
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/doc.go
generated
vendored
24
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/doc.go
generated
vendored
@ -1,24 +0,0 @@
|
|||||||
// Package binarydist implements binary diff and patch as described on
|
|
||||||
// http://www.daemonology.net/bsdiff/. It reads and writes files
|
|
||||||
// compatible with the tools there.
|
|
||||||
package binarydist
|
|
||||||
|
|
||||||
var magic = [8]byte{'B', 'S', 'D', 'I', 'F', 'F', '4', '0'}
|
|
||||||
|
|
||||||
// File format:
|
|
||||||
// 0 8 "BSDIFF40"
|
|
||||||
// 8 8 X
|
|
||||||
// 16 8 Y
|
|
||||||
// 24 8 sizeof(newfile)
|
|
||||||
// 32 X bzip2(control block)
|
|
||||||
// 32+X Y bzip2(diff block)
|
|
||||||
// 32+X+Y ??? bzip2(extra block)
|
|
||||||
// with control block a set of triples (x,y,z) meaning "add x bytes
|
|
||||||
// from oldfile to x bytes from the diff block; copy y bytes from the
|
|
||||||
// extra block; seek forwards in oldfile by z bytes".
|
|
||||||
type header struct {
|
|
||||||
Magic [8]byte
|
|
||||||
CtrlLen int64
|
|
||||||
DiffLen int64
|
|
||||||
NewSize int64
|
|
||||||
}
|
|
53
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/encoding.go
generated
vendored
53
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/encoding.go
generated
vendored
@ -1,53 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
// SignMagLittleEndian is the numeric encoding used by the bsdiff tools.
|
|
||||||
// It implements binary.ByteOrder using a sign-magnitude format
|
|
||||||
// and little-endian byte order. Only methods Uint64 and String
|
|
||||||
// have been written; the rest panic.
|
|
||||||
type signMagLittleEndian struct{}
|
|
||||||
|
|
||||||
func (signMagLittleEndian) Uint16(b []byte) uint16 { panic("unimplemented") }
|
|
||||||
|
|
||||||
func (signMagLittleEndian) PutUint16(b []byte, v uint16) { panic("unimplemented") }
|
|
||||||
|
|
||||||
func (signMagLittleEndian) Uint32(b []byte) uint32 { panic("unimplemented") }
|
|
||||||
|
|
||||||
func (signMagLittleEndian) PutUint32(b []byte, v uint32) { panic("unimplemented") }
|
|
||||||
|
|
||||||
func (signMagLittleEndian) Uint64(b []byte) uint64 {
|
|
||||||
y := int64(b[0]) |
|
|
||||||
int64(b[1])<<8 |
|
|
||||||
int64(b[2])<<16 |
|
|
||||||
int64(b[3])<<24 |
|
|
||||||
int64(b[4])<<32 |
|
|
||||||
int64(b[5])<<40 |
|
|
||||||
int64(b[6])<<48 |
|
|
||||||
int64(b[7]&0x7f)<<56
|
|
||||||
|
|
||||||
if b[7]&0x80 != 0 {
|
|
||||||
y = -y
|
|
||||||
}
|
|
||||||
return uint64(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (signMagLittleEndian) PutUint64(b []byte, v uint64) {
|
|
||||||
x := int64(v)
|
|
||||||
neg := x < 0
|
|
||||||
if neg {
|
|
||||||
x = -x
|
|
||||||
}
|
|
||||||
|
|
||||||
b[0] = byte(x)
|
|
||||||
b[1] = byte(x >> 8)
|
|
||||||
b[2] = byte(x >> 16)
|
|
||||||
b[3] = byte(x >> 24)
|
|
||||||
b[4] = byte(x >> 32)
|
|
||||||
b[5] = byte(x >> 40)
|
|
||||||
b[6] = byte(x >> 48)
|
|
||||||
b[7] = byte(x >> 56)
|
|
||||||
if neg {
|
|
||||||
b[7] |= 0x80
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (signMagLittleEndian) String() string { return "signMagLittleEndian" }
|
|
109
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/patch.go
generated
vendored
109
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/patch.go
generated
vendored
@ -1,109 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/bzip2"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrCorrupt = errors.New("corrupt patch")
|
|
||||||
|
|
||||||
// Patch applies patch to old, according to the bspatch algorithm,
|
|
||||||
// and writes the result to new.
|
|
||||||
func Patch(old io.Reader, new io.Writer, patch io.Reader) error {
|
|
||||||
var hdr header
|
|
||||||
err := binary.Read(patch, signMagLittleEndian{}, &hdr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if hdr.Magic != magic {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
if hdr.CtrlLen < 0 || hdr.DiffLen < 0 || hdr.NewSize < 0 {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrlbuf := make([]byte, hdr.CtrlLen)
|
|
||||||
_, err = io.ReadFull(patch, ctrlbuf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cpfbz2 := bzip2.NewReader(bytes.NewReader(ctrlbuf))
|
|
||||||
|
|
||||||
diffbuf := make([]byte, hdr.DiffLen)
|
|
||||||
_, err = io.ReadFull(patch, diffbuf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dpfbz2 := bzip2.NewReader(bytes.NewReader(diffbuf))
|
|
||||||
|
|
||||||
// The entire rest of the file is the extra block.
|
|
||||||
epfbz2 := bzip2.NewReader(patch)
|
|
||||||
|
|
||||||
obuf, err := ioutil.ReadAll(old)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nbuf := make([]byte, hdr.NewSize)
|
|
||||||
|
|
||||||
var oldpos, newpos int64
|
|
||||||
for newpos < hdr.NewSize {
|
|
||||||
var ctrl struct{ Add, Copy, Seek int64 }
|
|
||||||
err = binary.Read(cpfbz2, signMagLittleEndian{}, &ctrl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity-check
|
|
||||||
if newpos+ctrl.Add > hdr.NewSize {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read diff string
|
|
||||||
_, err = io.ReadFull(dpfbz2, nbuf[newpos:newpos+ctrl.Add])
|
|
||||||
if err != nil {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add old data to diff string
|
|
||||||
for i := int64(0); i < ctrl.Add; i++ {
|
|
||||||
if oldpos+i >= 0 && oldpos+i < int64(len(obuf)) {
|
|
||||||
nbuf[newpos+i] += obuf[oldpos+i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust pointers
|
|
||||||
newpos += ctrl.Add
|
|
||||||
oldpos += ctrl.Add
|
|
||||||
|
|
||||||
// Sanity-check
|
|
||||||
if newpos+ctrl.Copy > hdr.NewSize {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read extra string
|
|
||||||
_, err = io.ReadFull(epfbz2, nbuf[newpos:newpos+ctrl.Copy])
|
|
||||||
if err != nil {
|
|
||||||
return ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust pointers
|
|
||||||
newpos += ctrl.Copy
|
|
||||||
oldpos += ctrl.Seek
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the new file
|
|
||||||
for len(nbuf) > 0 {
|
|
||||||
n, err := new.Write(nbuf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nbuf = nbuf[n:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPatch(t *testing.T) {
|
|
||||||
mustWriteRandFile("test.old", 1e3)
|
|
||||||
mustWriteRandFile("test.new", 1e3)
|
|
||||||
|
|
||||||
got, err := ioutil.TempFile("/tmp", "bspatch.")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Remove(got.Name())
|
|
||||||
|
|
||||||
err = exec.Command("bsdiff", "test.old", "test.new", "test.patch").Run()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Patch(mustOpen("test.old"), got, mustOpen("test.patch"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ref, err := got.Seek(0, 2)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("got %d bytes", ref)
|
|
||||||
if n := fileCmp(got, mustOpen("test.new")); n > -1 {
|
|
||||||
t.Fatalf("produced different output at pos %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPatchHk(t *testing.T) {
|
|
||||||
got, err := ioutil.TempFile("/tmp", "bspatch.")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Remove(got.Name())
|
|
||||||
|
|
||||||
err = Patch(mustOpen("testdata/sample.old"), got, mustOpen("testdata/sample.patch"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ref, err := got.Seek(0, 2)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("got %d bytes", ref)
|
|
||||||
if n := fileCmp(got, mustOpen("testdata/sample.new")); n > -1 {
|
|
||||||
t.Fatalf("produced different output at pos %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
43
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/seek.go
generated
vendored
43
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/seek.go
generated
vendored
@ -1,43 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type seekBuffer struct {
|
|
||||||
buf []byte
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *seekBuffer) Write(p []byte) (n int, err error) {
|
|
||||||
n = copy(b.buf[b.pos:], p)
|
|
||||||
if n == len(p) {
|
|
||||||
b.pos += n
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
b.buf = append(b.buf, p[n:]...)
|
|
||||||
b.pos += len(p)
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *seekBuffer) Seek(offset int64, whence int) (ret int64, err error) {
|
|
||||||
var abs int64
|
|
||||||
switch whence {
|
|
||||||
case 0:
|
|
||||||
abs = offset
|
|
||||||
case 1:
|
|
||||||
abs = int64(b.pos) + offset
|
|
||||||
case 2:
|
|
||||||
abs = int64(len(b.buf)) + offset
|
|
||||||
default:
|
|
||||||
return 0, errors.New("binarydist: invalid whence")
|
|
||||||
}
|
|
||||||
if abs < 0 {
|
|
||||||
return 0, errors.New("binarydist: negative position")
|
|
||||||
}
|
|
||||||
if abs >= 1<<31 {
|
|
||||||
return 0, errors.New("binarydist: position out of range")
|
|
||||||
}
|
|
||||||
b.pos = int(abs)
|
|
||||||
return abs, nil
|
|
||||||
}
|
|
33
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/sort_test.go
generated
vendored
33
vendor/github.com/equinox-io/equinox/internal/go-update/internal/binarydist/sort_test.go
generated
vendored
@ -1,33 +0,0 @@
|
|||||||
package binarydist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sortT = [][]byte{
|
|
||||||
mustRandBytes(1000),
|
|
||||||
mustReadAll(mustOpen("test.old")),
|
|
||||||
[]byte("abcdefabcdef"),
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQsufsort(t *testing.T) {
|
|
||||||
for _, s := range sortT {
|
|
||||||
I := qsufsort(s)
|
|
||||||
for i := 1; i < len(I); i++ {
|
|
||||||
if bytes.Compare(s[I[i-1]:], s[I[i]:]) > 0 {
|
|
||||||
t.Fatalf("unsorted at %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustRandBytes(n int) []byte {
|
|
||||||
b := make([]byte, n)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
27
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/LICENSE
generated
vendored
27
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/LICENSE
generated
vendored
@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
16
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/README.md
generated
vendored
16
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/README.md
generated
vendored
@ -1,16 +0,0 @@
|
|||||||
### Extensions to the "os" package.
|
|
||||||
|
|
||||||
## Find the current Executable and ExecutableFolder.
|
|
||||||
|
|
||||||
There is sometimes utility in finding the current executable file
|
|
||||||
that is running. This can be used for upgrading the current executable
|
|
||||||
or finding resources located relative to the executable file. Both
|
|
||||||
working directory and the os.Args[0] value are arbitrary and cannot
|
|
||||||
be relied on; os.Args[0] can be "faked".
|
|
||||||
|
|
||||||
Multi-platform and supports:
|
|
||||||
* Linux
|
|
||||||
* OS X
|
|
||||||
* Windows
|
|
||||||
* Plan 9
|
|
||||||
* BSDs.
|
|
27
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext.go
generated
vendored
27
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext.go
generated
vendored
@ -1,27 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Extensions to the standard "os" package.
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import "path/filepath"
|
|
||||||
|
|
||||||
// Executable returns an absolute path that can be used to
|
|
||||||
// re-invoke the current program.
|
|
||||||
// It may not be valid after the current program exits.
|
|
||||||
func Executable() (string, error) {
|
|
||||||
p, err := executable()
|
|
||||||
return filepath.Clean(p), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns same path as Executable, returns just the folder
|
|
||||||
// path. Excludes the executable name and any trailing slash.
|
|
||||||
func ExecutableFolder() (string, error) {
|
|
||||||
p, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Dir(p), nil
|
|
||||||
}
|
|
20
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_plan9.go
generated
vendored
20
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_plan9.go
generated
vendored
@ -1,20 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return syscall.Fd2path(int(f.Fd()))
|
|
||||||
}
|
|
36
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_procfs.go
generated
vendored
36
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_procfs.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build linux netbsd openbsd solaris dragonfly
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
const deletedTag = " (deleted)"
|
|
||||||
execpath, err := os.Readlink("/proc/self/exe")
|
|
||||||
if err != nil {
|
|
||||||
return execpath, err
|
|
||||||
}
|
|
||||||
execpath = strings.TrimSuffix(execpath, deletedTag)
|
|
||||||
execpath = strings.TrimPrefix(execpath, deletedTag)
|
|
||||||
return execpath, nil
|
|
||||||
case "netbsd":
|
|
||||||
return os.Readlink("/proc/curproc/exe")
|
|
||||||
case "openbsd", "dragonfly":
|
|
||||||
return os.Readlink("/proc/curproc/file")
|
|
||||||
case "solaris":
|
|
||||||
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
|
|
||||||
}
|
|
||||||
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
|
|
||||||
}
|
|
79
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_sysctl.go
generated
vendored
79
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_sysctl.go
generated
vendored
@ -1,79 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build darwin freebsd
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var initCwd, initCwdErr = os.Getwd()
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
var mib [4]int32
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "freebsd":
|
|
||||||
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
|
|
||||||
case "darwin":
|
|
||||||
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
|
|
||||||
}
|
|
||||||
|
|
||||||
n := uintptr(0)
|
|
||||||
// Get length.
|
|
||||||
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
|
||||||
if errNum != 0 {
|
|
||||||
return "", errNum
|
|
||||||
}
|
|
||||||
if n == 0 { // This shouldn't happen.
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
buf := make([]byte, n)
|
|
||||||
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
|
||||||
if errNum != 0 {
|
|
||||||
return "", errNum
|
|
||||||
}
|
|
||||||
if n == 0 { // This shouldn't happen.
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
for i, v := range buf {
|
|
||||||
if v == 0 {
|
|
||||||
buf = buf[:i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
execPath := string(buf)
|
|
||||||
// execPath will not be empty due to above checks.
|
|
||||||
// Try to get the absolute path if the execPath is not rooted.
|
|
||||||
if execPath[0] != '/' {
|
|
||||||
execPath, err = getAbs(execPath)
|
|
||||||
if err != nil {
|
|
||||||
return execPath, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
|
|
||||||
// actual executable.
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
|
|
||||||
return execPath, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return execPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAbs(execPath string) (string, error) {
|
|
||||||
if initCwdErr != nil {
|
|
||||||
return execPath, initCwdErr
|
|
||||||
}
|
|
||||||
// The execPath may begin with a "../" or a "./" so clean it first.
|
|
||||||
// Join the two paths, trailing and starting slashes undetermined, so use
|
|
||||||
// the generic Join function.
|
|
||||||
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
|
|
||||||
}
|
|
203
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_test.go
generated
vendored
203
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_test.go
generated
vendored
@ -1,203 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build darwin linux freebsd netbsd windows
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
executableEnvVar = "OSTEST_OUTPUT_EXECUTABLE"
|
|
||||||
|
|
||||||
executableEnvValueMatch = "match"
|
|
||||||
executableEnvValueDelete = "delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrintExecutable(t *testing.T) {
|
|
||||||
ef, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
t.Log("Executable:", ef)
|
|
||||||
}
|
|
||||||
func TestPrintExecutableFolder(t *testing.T) {
|
|
||||||
ef, err := ExecutableFolder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
|
||||||
}
|
|
||||||
t.Log("Executable Folder:", ef)
|
|
||||||
}
|
|
||||||
func TestExecutableFolder(t *testing.T) {
|
|
||||||
ef, err := ExecutableFolder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
|
||||||
}
|
|
||||||
if ef[len(ef)-1] == filepath.Separator {
|
|
||||||
t.Fatal("ExecutableFolder ends with a trailing slash.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestExecutableMatch(t *testing.T) {
|
|
||||||
ep, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fullpath to be of the form "dir/prog".
|
|
||||||
dir := filepath.Dir(filepath.Dir(ep))
|
|
||||||
fullpath, err := filepath.Rel(dir, ep)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("filepath.Rel: %v", err)
|
|
||||||
}
|
|
||||||
// Make child start with a relative program path.
|
|
||||||
// Alter argv[0] for child to verify getting real path without argv[0].
|
|
||||||
cmd := &exec.Cmd{
|
|
||||||
Dir: dir,
|
|
||||||
Path: fullpath,
|
|
||||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueMatch)},
|
|
||||||
}
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec(self) failed: %v", err)
|
|
||||||
}
|
|
||||||
outs := string(out)
|
|
||||||
if !filepath.IsAbs(outs) {
|
|
||||||
t.Fatalf("Child returned %q, want an absolute path", out)
|
|
||||||
}
|
|
||||||
if !sameFile(outs, ep) {
|
|
||||||
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecutableDelete(t *testing.T) {
|
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
fpath, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w := io.Pipe()
|
|
||||||
stderrBuff := &bytes.Buffer{}
|
|
||||||
stdoutBuff := &bytes.Buffer{}
|
|
||||||
cmd := &exec.Cmd{
|
|
||||||
Path: fpath,
|
|
||||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueDelete)},
|
|
||||||
Stdin: r,
|
|
||||||
Stderr: stderrBuff,
|
|
||||||
Stdout: stdoutBuff,
|
|
||||||
}
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec(self) start failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempPath := fpath + "_copy"
|
|
||||||
_ = os.Remove(tempPath)
|
|
||||||
|
|
||||||
err = copyFile(tempPath, fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("copy file failed: %v", err)
|
|
||||||
}
|
|
||||||
err = os.Remove(fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("remove running test file failed: %v", err)
|
|
||||||
}
|
|
||||||
err = os.Rename(tempPath, fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("rename copy to previous name failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte{0})
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec wait failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
childPath := stderrBuff.String()
|
|
||||||
if !filepath.IsAbs(childPath) {
|
|
||||||
t.Fatalf("Child returned %q, want an absolute path", childPath)
|
|
||||||
}
|
|
||||||
if !sameFile(childPath, fpath) {
|
|
||||||
t.Fatalf("Child returned %q, not the same file as %q", childPath, fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sameFile(fn1, fn2 string) bool {
|
|
||||||
fi1, err := os.Stat(fn1)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fi2, err := os.Stat(fn2)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return os.SameFile(fi1, fi2)
|
|
||||||
}
|
|
||||||
func copyFile(dest, src string) error {
|
|
||||||
df, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer df.Close()
|
|
||||||
|
|
||||||
sf, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sf.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(df, sf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
env := os.Getenv(executableEnvVar)
|
|
||||||
switch env {
|
|
||||||
case "":
|
|
||||||
os.Exit(m.Run())
|
|
||||||
case executableEnvValueMatch:
|
|
||||||
// First chdir to another path.
|
|
||||||
dir := "/"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
dir = filepath.VolumeName(".")
|
|
||||||
}
|
|
||||||
os.Chdir(dir)
|
|
||||||
if ep, err := Executable(); err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, ep)
|
|
||||||
}
|
|
||||||
case executableEnvValueDelete:
|
|
||||||
bb := make([]byte, 1)
|
|
||||||
var err error
|
|
||||||
n, err := os.Stdin.Read(bb)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if n != 1 {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: n != 1, n == ", n)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if ep, err := Executable(); err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
34
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_windows.go
generated
vendored
34
vendor/github.com/equinox-io/equinox/internal/go-update/internal/osext/osext_windows.go
generated
vendored
@ -1,34 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unicode/utf16"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
kernel = syscall.MustLoadDLL("kernel32.dll")
|
|
||||||
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetModuleFileName() with hModule = NULL
|
|
||||||
func executable() (exePath string, err error) {
|
|
||||||
return getModuleFileName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getModuleFileName() (string, error) {
|
|
||||||
var n uint32
|
|
||||||
b := make([]uint16, syscall.MAX_PATH)
|
|
||||||
size := uint32(len(b))
|
|
||||||
|
|
||||||
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
|
|
||||||
n = uint32(r0)
|
|
||||||
if n == 0 {
|
|
||||||
return "", e1
|
|
||||||
}
|
|
||||||
return string(utf16.Decode(b[0:n])), nil
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package update
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/equinox-io/equinox/internal/go-update/internal/binarydist"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Patcher defines an interface for applying binary patches to an old item to get an updated item.
|
|
||||||
type Patcher interface {
|
|
||||||
Patch(old io.Reader, new io.Writer, patch io.Reader) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type patchFn func(io.Reader, io.Writer, io.Reader) error
|
|
||||||
|
|
||||||
func (fn patchFn) Patch(old io.Reader, new io.Writer, patch io.Reader) error {
|
|
||||||
return fn(old, new, patch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBSDifferPatcher returns a new Patcher that applies binary patches using
|
|
||||||
// the bsdiff algorithm. See http://www.daemonology.net/bsdiff/
|
|
||||||
func NewBSDiffPatcher() Patcher {
|
|
||||||
return patchFn(binarydist.Patch)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package update
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/asn1"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verifier defines an interface for verfiying an update's signature with a public key.
|
|
||||||
type Verifier interface {
|
|
||||||
VerifySignature(checksum, signature []byte, h crypto.Hash, publicKey crypto.PublicKey) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type verifyFn func([]byte, []byte, crypto.Hash, crypto.PublicKey) error
|
|
||||||
|
|
||||||
func (fn verifyFn) VerifySignature(checksum []byte, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
|
|
||||||
return fn(checksum, signature, hash, publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRSAVerifier returns a Verifier that uses the RSA algorithm to verify updates.
|
|
||||||
func NewRSAVerifier() Verifier {
|
|
||||||
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
|
|
||||||
key, ok := publicKey.(*rsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("not a valid RSA public key")
|
|
||||||
}
|
|
||||||
return rsa.VerifyPKCS1v15(key, hash, checksum, signature)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type rsDER struct {
|
|
||||||
R *big.Int
|
|
||||||
S *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewECDSAVerifier returns a Verifier that uses the ECDSA algorithm to verify updates.
|
|
||||||
func NewECDSAVerifier() Verifier {
|
|
||||||
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
|
|
||||||
key, ok := publicKey.(*ecdsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("not a valid ECDSA public key")
|
|
||||||
}
|
|
||||||
var rs rsDER
|
|
||||||
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ecdsa.Verify(key, checksum, rs.R, rs.S) {
|
|
||||||
return errors.New("failed to verify ecsda signature")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDSAVerifier returns a Verifier that uses the DSA algorithm to verify updates.
|
|
||||||
func NewDSAVerifier() Verifier {
|
|
||||||
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
|
|
||||||
key, ok := publicKey.(*dsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("not a valid DSA public key")
|
|
||||||
}
|
|
||||||
var rs rsDER
|
|
||||||
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !dsa.Verify(key, checksum, rs.R, rs.S) {
|
|
||||||
return errors.New("failed to verify ecsda signature")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,16 +0,0 @@
|
|||||||
### Extensions to the "os" package.
|
|
||||||
|
|
||||||
## Find the current Executable and ExecutableFolder.
|
|
||||||
|
|
||||||
There is sometimes utility in finding the current executable file
|
|
||||||
that is running. This can be used for upgrading the current executable
|
|
||||||
or finding resources located relative to the executable file. Both
|
|
||||||
working directory and the os.Args[0] value are arbitrary and cannot
|
|
||||||
be relied on; os.Args[0] can be "faked".
|
|
||||||
|
|
||||||
Multi-platform and supports:
|
|
||||||
* Linux
|
|
||||||
* OS X
|
|
||||||
* Windows
|
|
||||||
* Plan 9
|
|
||||||
* BSDs.
|
|
@ -1,27 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Extensions to the standard "os" package.
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import "path/filepath"
|
|
||||||
|
|
||||||
// Executable returns an absolute path that can be used to
|
|
||||||
// re-invoke the current program.
|
|
||||||
// It may not be valid after the current program exits.
|
|
||||||
func Executable() (string, error) {
|
|
||||||
p, err := executable()
|
|
||||||
return filepath.Clean(p), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns same path as Executable, returns just the folder
|
|
||||||
// path. Excludes the executable name and any trailing slash.
|
|
||||||
func ExecutableFolder() (string, error) {
|
|
||||||
p, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Dir(p), nil
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return syscall.Fd2path(int(f.Fd()))
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build linux netbsd openbsd solaris dragonfly
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
const deletedTag = " (deleted)"
|
|
||||||
execpath, err := os.Readlink("/proc/self/exe")
|
|
||||||
if err != nil {
|
|
||||||
return execpath, err
|
|
||||||
}
|
|
||||||
execpath = strings.TrimSuffix(execpath, deletedTag)
|
|
||||||
execpath = strings.TrimPrefix(execpath, deletedTag)
|
|
||||||
return execpath, nil
|
|
||||||
case "netbsd":
|
|
||||||
return os.Readlink("/proc/curproc/exe")
|
|
||||||
case "openbsd", "dragonfly":
|
|
||||||
return os.Readlink("/proc/curproc/file")
|
|
||||||
case "solaris":
|
|
||||||
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
|
|
||||||
}
|
|
||||||
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build darwin freebsd
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var initCwd, initCwdErr = os.Getwd()
|
|
||||||
|
|
||||||
func executable() (string, error) {
|
|
||||||
var mib [4]int32
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "freebsd":
|
|
||||||
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
|
|
||||||
case "darwin":
|
|
||||||
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
|
|
||||||
}
|
|
||||||
|
|
||||||
n := uintptr(0)
|
|
||||||
// Get length.
|
|
||||||
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
|
||||||
if errNum != 0 {
|
|
||||||
return "", errNum
|
|
||||||
}
|
|
||||||
if n == 0 { // This shouldn't happen.
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
buf := make([]byte, n)
|
|
||||||
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
|
||||||
if errNum != 0 {
|
|
||||||
return "", errNum
|
|
||||||
}
|
|
||||||
if n == 0 { // This shouldn't happen.
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
for i, v := range buf {
|
|
||||||
if v == 0 {
|
|
||||||
buf = buf[:i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
execPath := string(buf)
|
|
||||||
// execPath will not be empty due to above checks.
|
|
||||||
// Try to get the absolute path if the execPath is not rooted.
|
|
||||||
if execPath[0] != '/' {
|
|
||||||
execPath, err = getAbs(execPath)
|
|
||||||
if err != nil {
|
|
||||||
return execPath, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
|
|
||||||
// actual executable.
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
|
|
||||||
return execPath, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return execPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAbs(execPath string) (string, error) {
|
|
||||||
if initCwdErr != nil {
|
|
||||||
return execPath, initCwdErr
|
|
||||||
}
|
|
||||||
// The execPath may begin with a "../" or a "./" so clean it first.
|
|
||||||
// Join the two paths, trailing and starting slashes undetermined, so use
|
|
||||||
// the generic Join function.
|
|
||||||
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
|
|
||||||
}
|
|
@ -1,203 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build darwin linux freebsd netbsd windows
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
executableEnvVar = "OSTEST_OUTPUT_EXECUTABLE"
|
|
||||||
|
|
||||||
executableEnvValueMatch = "match"
|
|
||||||
executableEnvValueDelete = "delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrintExecutable(t *testing.T) {
|
|
||||||
ef, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
t.Log("Executable:", ef)
|
|
||||||
}
|
|
||||||
func TestPrintExecutableFolder(t *testing.T) {
|
|
||||||
ef, err := ExecutableFolder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
|
||||||
}
|
|
||||||
t.Log("Executable Folder:", ef)
|
|
||||||
}
|
|
||||||
func TestExecutableFolder(t *testing.T) {
|
|
||||||
ef, err := ExecutableFolder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
|
||||||
}
|
|
||||||
if ef[len(ef)-1] == filepath.Separator {
|
|
||||||
t.Fatal("ExecutableFolder ends with a trailing slash.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestExecutableMatch(t *testing.T) {
|
|
||||||
ep, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fullpath to be of the form "dir/prog".
|
|
||||||
dir := filepath.Dir(filepath.Dir(ep))
|
|
||||||
fullpath, err := filepath.Rel(dir, ep)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("filepath.Rel: %v", err)
|
|
||||||
}
|
|
||||||
// Make child start with a relative program path.
|
|
||||||
// Alter argv[0] for child to verify getting real path without argv[0].
|
|
||||||
cmd := &exec.Cmd{
|
|
||||||
Dir: dir,
|
|
||||||
Path: fullpath,
|
|
||||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueMatch)},
|
|
||||||
}
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec(self) failed: %v", err)
|
|
||||||
}
|
|
||||||
outs := string(out)
|
|
||||||
if !filepath.IsAbs(outs) {
|
|
||||||
t.Fatalf("Child returned %q, want an absolute path", out)
|
|
||||||
}
|
|
||||||
if !sameFile(outs, ep) {
|
|
||||||
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecutableDelete(t *testing.T) {
|
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
fpath, err := Executable()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Executable failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w := io.Pipe()
|
|
||||||
stderrBuff := &bytes.Buffer{}
|
|
||||||
stdoutBuff := &bytes.Buffer{}
|
|
||||||
cmd := &exec.Cmd{
|
|
||||||
Path: fpath,
|
|
||||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueDelete)},
|
|
||||||
Stdin: r,
|
|
||||||
Stderr: stderrBuff,
|
|
||||||
Stdout: stdoutBuff,
|
|
||||||
}
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec(self) start failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempPath := fpath + "_copy"
|
|
||||||
_ = os.Remove(tempPath)
|
|
||||||
|
|
||||||
err = copyFile(tempPath, fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("copy file failed: %v", err)
|
|
||||||
}
|
|
||||||
err = os.Remove(fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("remove running test file failed: %v", err)
|
|
||||||
}
|
|
||||||
err = os.Rename(tempPath, fpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("rename copy to previous name failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte{0})
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("exec wait failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
childPath := stderrBuff.String()
|
|
||||||
if !filepath.IsAbs(childPath) {
|
|
||||||
t.Fatalf("Child returned %q, want an absolute path", childPath)
|
|
||||||
}
|
|
||||||
if !sameFile(childPath, fpath) {
|
|
||||||
t.Fatalf("Child returned %q, not the same file as %q", childPath, fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sameFile(fn1, fn2 string) bool {
|
|
||||||
fi1, err := os.Stat(fn1)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fi2, err := os.Stat(fn2)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return os.SameFile(fi1, fi2)
|
|
||||||
}
|
|
||||||
func copyFile(dest, src string) error {
|
|
||||||
df, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer df.Close()
|
|
||||||
|
|
||||||
sf, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sf.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(df, sf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
env := os.Getenv(executableEnvVar)
|
|
||||||
switch env {
|
|
||||||
case "":
|
|
||||||
os.Exit(m.Run())
|
|
||||||
case executableEnvValueMatch:
|
|
||||||
// First chdir to another path.
|
|
||||||
dir := "/"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
dir = filepath.VolumeName(".")
|
|
||||||
}
|
|
||||||
os.Chdir(dir)
|
|
||||||
if ep, err := Executable(); err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, ep)
|
|
||||||
}
|
|
||||||
case executableEnvValueDelete:
|
|
||||||
bb := make([]byte, 1)
|
|
||||||
var err error
|
|
||||||
n, err := os.Stdin.Read(bb)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if n != 1 {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: n != 1, n == ", n)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if ep, err := Executable(); err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
// Copyright 2012 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package osext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unicode/utf16"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
kernel = syscall.MustLoadDLL("kernel32.dll")
|
|
||||||
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetModuleFileName() with hModule = NULL
|
|
||||||
func executable() (exePath string, err error) {
|
|
||||||
return getModuleFileName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getModuleFileName() (string, error) {
|
|
||||||
var n uint32
|
|
||||||
b := make([]uint16, syscall.MAX_PATH)
|
|
||||||
size := uint32(len(b))
|
|
||||||
|
|
||||||
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
|
|
||||||
n = uint32(r0)
|
|
||||||
if n == 0 {
|
|
||||||
return "", e1
|
|
||||||
}
|
|
||||||
return string(utf16.Decode(b[0:n])), nil
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
package proto defines a set of structures used to negotiate an update between an
|
|
||||||
an application (the client) and an Equinox update service.
|
|
||||||
*/
|
|
||||||
package proto
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type PatchKind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PatchNone PatchKind = "none"
|
|
||||||
PatchBSDiff PatchKind = "bsdiff"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
AppID string `json:"app_id"`
|
|
||||||
Channel string `json:"channel"`
|
|
||||||
OS string `json:"os"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
GoARM string `json:"goarm"`
|
|
||||||
TargetVersion string `json:"target_version"`
|
|
||||||
|
|
||||||
CurrentVersion string `json:"current_version"`
|
|
||||||
CurrentSHA256 string `json:"current_sha256"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
DownloadURL string `json:"download_url"`
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
Signature string `json:"signature"`
|
|
||||||
Patch PatchKind `json:"patch_type"`
|
|
||||||
Release Release `json:"release"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Release struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreateDate time.Time `json:"create_date"`
|
|
||||||
}
|
|
@ -1,305 +0,0 @@
|
|||||||
package equinox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/equinox-io/equinox/internal/go-update"
|
|
||||||
"github.com/equinox-io/equinox/internal/osext"
|
|
||||||
"github.com/equinox-io/equinox/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
const protocolVersion = "1"
|
|
||||||
const defaultCheckURL = "https://update.equinox.io/check"
|
|
||||||
const userAgent = "EquinoxSDK/1.0"
|
|
||||||
|
|
||||||
var NotAvailableErr = errors.New("No update available")
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
// Channel specifies the name of an Equinox release channel to check for
|
|
||||||
// a newer version of the application.
|
|
||||||
//
|
|
||||||
// If empty, defaults to 'stable'.
|
|
||||||
Channel string
|
|
||||||
|
|
||||||
// Version requests an update to a specific version of the application.
|
|
||||||
// If specified, `Channel` is ignored.
|
|
||||||
Version string
|
|
||||||
|
|
||||||
// TargetPath defines the path to the file to update.
|
|
||||||
// The emptry string means 'the executable file of the running program'.
|
|
||||||
TargetPath string
|
|
||||||
|
|
||||||
// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
|
|
||||||
TargetMode os.FileMode
|
|
||||||
|
|
||||||
// Public key to use for signature verification. If nil, no signature
|
|
||||||
// verification is done. Use `SetPublicKeyPEM` to set this field with PEM data.
|
|
||||||
PublicKey crypto.PublicKey
|
|
||||||
|
|
||||||
// Target operating system of the update. Uses the same standard OS names used
|
|
||||||
// by Go build tags (windows, darwin, linux, etc).
|
|
||||||
// If empty, it will be populated by consulting runtime.GOOS
|
|
||||||
OS string
|
|
||||||
|
|
||||||
// Target architecture of the update. Uses the same standard Arch names used
|
|
||||||
// by Go build tags (amd64, 386, arm, etc).
|
|
||||||
// If empty, it will be populated by consulting runtime.GOARCH
|
|
||||||
Arch string
|
|
||||||
|
|
||||||
// Target ARM architecture, if a specific one if required. Uses the same names
|
|
||||||
// as the GOARM environment variable (5, 6, 7).
|
|
||||||
//
|
|
||||||
// GoARM is ignored if Arch != 'arm'.
|
|
||||||
// GoARM is ignored if it is the empty string. Omit it if you do not need
|
|
||||||
// to distinguish between ARM versions.
|
|
||||||
GoARM string
|
|
||||||
|
|
||||||
// The current application version. This is used for statistics and reporting only,
|
|
||||||
// it is optional.
|
|
||||||
CurrentVersion string
|
|
||||||
|
|
||||||
// CheckURL is the URL to request an update check from. You should only set
|
|
||||||
// this if you are running an on-prem Equinox server.
|
|
||||||
// If empty the default Equinox update service endpoint is used.
|
|
||||||
CheckURL string
|
|
||||||
|
|
||||||
// HTTPClient is used to make all HTTP requests necessary for the update check protocol.
|
|
||||||
// You may configure it to use custom timeouts, proxy servers or other behaviors.
|
|
||||||
HTTPClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is returned by Check when an update is available. It may be
|
|
||||||
// passed to Apply to perform the update.
|
|
||||||
type Response struct {
|
|
||||||
// Version of the release that will be updated to if applied.
|
|
||||||
ReleaseVersion string
|
|
||||||
|
|
||||||
// Title of the the release
|
|
||||||
ReleaseTitle string
|
|
||||||
|
|
||||||
// Additional details about the release
|
|
||||||
ReleaseDescription string
|
|
||||||
|
|
||||||
// Creation date of the release
|
|
||||||
ReleaseDate time.Time
|
|
||||||
|
|
||||||
downloadURL string
|
|
||||||
checksum []byte
|
|
||||||
signature []byte
|
|
||||||
patch proto.PatchKind
|
|
||||||
opts Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPublicKeyPEM is a convenience method to set the PublicKey property
|
|
||||||
// used for checking a completed update's signature by parsing a
|
|
||||||
// Public Key formatted as PEM data.
|
|
||||||
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
|
|
||||||
block, _ := pem.Decode(pembytes)
|
|
||||||
if block == nil {
|
|
||||||
return errors.New("couldn't parse PEM data")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.PublicKey = pub
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check communicates with an Equinox update service to determine if
|
|
||||||
// an update for the given application matching the specified options is
|
|
||||||
// available. The returned error is nil only if an update is available.
|
|
||||||
//
|
|
||||||
// The appID is issued to you when creating an application at https://equinox.io
|
|
||||||
//
|
|
||||||
// You can compare the returned error to NotAvailableErr to differentiate between
|
|
||||||
// a successful check that found no update from other errors like a failed
|
|
||||||
// network connection.
|
|
||||||
func Check(appID string, opts Options) (Response, error) {
|
|
||||||
var r Response
|
|
||||||
|
|
||||||
if opts.Channel == "" {
|
|
||||||
opts.Channel = "stable"
|
|
||||||
}
|
|
||||||
if opts.TargetPath == "" {
|
|
||||||
var err error
|
|
||||||
opts.TargetPath, err = osext.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opts.OS == "" {
|
|
||||||
opts.OS = runtime.GOOS
|
|
||||||
}
|
|
||||||
if opts.Arch == "" {
|
|
||||||
opts.Arch = runtime.GOARCH
|
|
||||||
}
|
|
||||||
if opts.CheckURL == "" {
|
|
||||||
opts.CheckURL = defaultCheckURL
|
|
||||||
}
|
|
||||||
if opts.HTTPClient == nil {
|
|
||||||
opts.HTTPClient = new(http.Client)
|
|
||||||
}
|
|
||||||
opts.HTTPClient.Transport = newUserAgentTransport(userAgent, opts.HTTPClient.Transport)
|
|
||||||
|
|
||||||
checksum := computeChecksum(opts.TargetPath)
|
|
||||||
|
|
||||||
payload, err := json.Marshal(proto.Request{
|
|
||||||
AppID: appID,
|
|
||||||
Channel: opts.Channel,
|
|
||||||
OS: opts.OS,
|
|
||||||
Arch: opts.Arch,
|
|
||||||
GoARM: opts.GoARM,
|
|
||||||
TargetVersion: opts.Version,
|
|
||||||
CurrentVersion: opts.CurrentVersion,
|
|
||||||
CurrentSHA256: checksum,
|
|
||||||
})
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", opts.CheckURL, bytes.NewReader(payload))
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", fmt.Sprintf("application/json; q=1; version=%s; charset=utf-8", protocolVersion))
|
|
||||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
|
|
||||||
resp, err := opts.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
return r, fmt.Errorf("Server responded with %s: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
var protoResp proto.Response
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&protoResp)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !protoResp.Available {
|
|
||||||
return r, NotAvailableErr
|
|
||||||
}
|
|
||||||
|
|
||||||
r.ReleaseVersion = protoResp.Release.Version
|
|
||||||
r.ReleaseTitle = protoResp.Release.Title
|
|
||||||
r.ReleaseDescription = protoResp.Release.Description
|
|
||||||
r.ReleaseDate = protoResp.Release.CreateDate
|
|
||||||
r.downloadURL = protoResp.DownloadURL
|
|
||||||
r.patch = protoResp.Patch
|
|
||||||
r.opts = opts
|
|
||||||
r.checksum, err = hex.DecodeString(protoResp.Checksum)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
r.signature, err = hex.DecodeString(protoResp.Signature)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeChecksum(path string) string {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
h := sha256.New()
|
|
||||||
_, err = io.Copy(h, f)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply performs an update of the current executable (or TargetFile, if it was
|
|
||||||
// set on the Options) with the update specified by Response.
|
|
||||||
//
|
|
||||||
// Error is nil if and only if the entire update completes successfully.
|
|
||||||
func (r Response) Apply() error {
|
|
||||||
opts := update.Options{
|
|
||||||
TargetPath: r.opts.TargetPath,
|
|
||||||
TargetMode: r.opts.TargetMode,
|
|
||||||
Checksum: r.checksum,
|
|
||||||
Signature: r.signature,
|
|
||||||
Verifier: update.NewECDSAVerifier(),
|
|
||||||
PublicKey: r.opts.PublicKey,
|
|
||||||
}
|
|
||||||
switch r.patch {
|
|
||||||
case proto.PatchBSDiff:
|
|
||||||
opts.Patcher = update.NewBSDiffPatcher()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := opts.CheckPermissions(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", r.downloadURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch the update
|
|
||||||
resp, err := r.opts.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// check that we got a patch
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
msg := "error downloading patch"
|
|
||||||
|
|
||||||
id := resp.Header.Get("Request-Id")
|
|
||||||
if id != "" {
|
|
||||||
msg += ", request " + id
|
|
||||||
}
|
|
||||||
|
|
||||||
blob, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err == nil {
|
|
||||||
msg += ": " + string(bytes.TrimSpace(blob))
|
|
||||||
}
|
|
||||||
return fmt.Errorf(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return update.Apply(resp.Body, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
type userAgentTransport struct {
|
|
||||||
userAgent string
|
|
||||||
http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUserAgentTransport(userAgent string, rt http.RoundTripper) *userAgentTransport {
|
|
||||||
if rt == nil {
|
|
||||||
rt = http.DefaultTransport
|
|
||||||
}
|
|
||||||
return &userAgentTransport{userAgent, rt}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *userAgentTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
if r.Header.Get("User-Agent") == "" {
|
|
||||||
r.Header.Set("User-Agent", t.userAgent)
|
|
||||||
}
|
|
||||||
return t.RoundTripper.RoundTrip(r)
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
package equinox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/equinox-io/equinox/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fakeAppID = "fake_app_id"
|
|
||||||
|
|
||||||
var (
|
|
||||||
fakeBinary = []byte{0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}
|
|
||||||
newFakeBinary = []byte{0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2}
|
|
||||||
ts *httptest.Server
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
sha string
|
|
||||||
newSHA string
|
|
||||||
signature string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
shaBytes := sha256.Sum256(fakeBinary)
|
|
||||||
sha = hex.EncodeToString(shaBytes[:])
|
|
||||||
newSHABytes := sha256.Sum256(newFakeBinary)
|
|
||||||
newSHA = hex.EncodeToString(newSHABytes[:])
|
|
||||||
|
|
||||||
var err error
|
|
||||||
key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to generate ecdsa key: %v", err))
|
|
||||||
}
|
|
||||||
sig, err := key.Sign(rand.Reader, newSHABytes[:], nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to sign new binary: %v", err))
|
|
||||||
}
|
|
||||||
signature = hex.EncodeToString(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotAvailable(t *testing.T) {
|
|
||||||
opts := setup(t, "TestNotAvailable", proto.Response{
|
|
||||||
Available: false,
|
|
||||||
})
|
|
||||||
defer cleanup(opts)
|
|
||||||
|
|
||||||
_, err := Check(fakeAppID, opts)
|
|
||||||
if err != NotAvailableErr {
|
|
||||||
t.Fatalf("Expected not available error, got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndToEnd(t *testing.T) {
|
|
||||||
opts := setup(t, "TestEndtoEnd", proto.Response{
|
|
||||||
Available: true,
|
|
||||||
Release: proto.Release{
|
|
||||||
Version: "0.1.2.3",
|
|
||||||
Title: "Release Title",
|
|
||||||
Description: "Release Description",
|
|
||||||
CreateDate: time.Now(),
|
|
||||||
},
|
|
||||||
Checksum: newSHA,
|
|
||||||
Signature: signature,
|
|
||||||
})
|
|
||||||
defer cleanup(opts)
|
|
||||||
|
|
||||||
resp, err := Check(fakeAppID, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed check: %v", err)
|
|
||||||
}
|
|
||||||
err = resp.Apply()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed apply: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadFile(opts.TargetPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read file: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf, newFakeBinary) {
|
|
||||||
t.Fatalf("Binary did not update to new expected value. Got %v, expected %v", buf, newFakeBinary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidPatch(t *testing.T) {
|
|
||||||
opts := setup(t, "TestInavlidPatch", proto.Response{
|
|
||||||
Available: true,
|
|
||||||
Release: proto.Release{
|
|
||||||
Version: "0.1.2.3",
|
|
||||||
Title: "Release Title",
|
|
||||||
Description: "Release Description",
|
|
||||||
CreateDate: time.Now(),
|
|
||||||
},
|
|
||||||
DownloadURL: "bad-request",
|
|
||||||
Checksum: newSHA,
|
|
||||||
Signature: signature,
|
|
||||||
Patch: proto.PatchBSDiff,
|
|
||||||
})
|
|
||||||
defer cleanup(opts)
|
|
||||||
|
|
||||||
resp, err := Check(fakeAppID, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed check: %v", err)
|
|
||||||
}
|
|
||||||
err = resp.Apply()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Apply succeeded")
|
|
||||||
}
|
|
||||||
if err.Error() != "error downloading patch: bad-request" {
|
|
||||||
t.Fatalf("Expected a different error message: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup(t *testing.T, name string, resp proto.Response) Options {
|
|
||||||
checkUserAgent := func(req *http.Request) {
|
|
||||||
if req.Header.Get("User-Agent") != userAgent {
|
|
||||||
t.Errorf("Expected user agent to be %s, not %s", userAgent, req.Header.Get("User-Agent"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/check", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
checkUserAgent(r)
|
|
||||||
var req proto.Request
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to decode proto request: %v", err)
|
|
||||||
}
|
|
||||||
if resp.Available {
|
|
||||||
if req.AppID != fakeAppID {
|
|
||||||
t.Fatalf("Unexpected app ID. Got %v, expected %v", err)
|
|
||||||
}
|
|
||||||
if req.CurrentSHA256 != sha {
|
|
||||||
t.Fatalf("Unexpected request SHA: %v", sha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Keying off the download URL may not be the best idea...
|
|
||||||
if resp.DownloadURL == "bad-request" {
|
|
||||||
mux.HandleFunc("/bin", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
checkUserAgent(r)
|
|
||||||
http.Error(w, "bad-request", http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
mux.HandleFunc("/bin", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
checkUserAgent(r)
|
|
||||||
w.Write(newFakeBinary)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ts = httptest.NewServer(mux)
|
|
||||||
resp.DownloadURL = ts.URL + "/bin"
|
|
||||||
|
|
||||||
var opts Options
|
|
||||||
opts.CheckURL = ts.URL + "/check"
|
|
||||||
opts.PublicKey = key.Public()
|
|
||||||
|
|
||||||
if name != "" {
|
|
||||||
opts.TargetPath = name
|
|
||||||
ioutil.WriteFile(name, fakeBinary, 0644)
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanup(opts Options) {
|
|
||||||
if opts.TargetPath != "" {
|
|
||||||
os.Remove(opts.TargetPath)
|
|
||||||
}
|
|
||||||
ts.Close()
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
|
|
||||||
src
|
|
@ -1,8 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.5.3
|
|
||||||
- tip
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
- ionathan@gmail.com
|
|
||||||
- marcosnils@gmail.com
|
|
@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013 Jonathan Leibiusky and Marcos Lilljedahl
|
|
||||||
|
|
||||||
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.
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue