master
李光春 2 years ago
parent 3744b1b28c
commit 97132fd582

@ -3,7 +3,7 @@ sudo: false
go:
- 1.7
install:
- go get -t -v
- go get -v
script:
- go test -v
after_success:

@ -1,176 +1,82 @@
# gosuv
[![Build Status](https://travis-ci.org/codeskyblue/gosuv.svg)](https://travis-ci.org/codeskyblue/gosuv)
[中文README](README_ZH.md) 不是很全,能看懂英文的还是尽量看英文
## current is in beta
Process management writtern by golang, inspired by python-supervisor
## So why write another supervisor?
I have been using python-supervisor for many years and there are something uncomfortable feelings.
1. Log can't contains ANSI color chars
1. The configuration file can add on the web, often forgot some settings.
1. `supervisorctl reload` will cause supervisord restarted
1. Hard to set status change to fatal notifications.
1. No process performance monitor page.
1. Program starts with no common environ, eg, missing HOME and USER variable
1. Kill process default is not group kill which make sub process still running.
1. More... will added when I think of it.
## Features
* [x] Web control page
* [x] Start, Stop, Tail, Reload
* [x] Realtime log
* [x] Add program support
* [x] Edit support
* [x] Delete support
* [x] Memory and CPU monitor
* [ ] Path auto complete <https://github.com/twitter/typeahead.js>
* [x] HTTP Basic auth
* [x] Github webhook
* [ ] Single log page, include search support
* [ ] 中文文档
## Requirements
Go version at least `1.6+`
## Installation
### Binaries
The fastest way is run with. Default install location is `/usr/local/bin`, change env-var `BINDIR` will also change install location.
# 概述
GoSuv是GO语言重写的类supervisor的一个进程管理程序在[codeskyblue/gosuv](https://github.com/codeskyblue/gosuv)基础上增加了环境变量的配置以及主机名称的配置等用rice封装对静态文件进行了封装单文件运行简单易用界面美感十足且对用户友好
```
curl https://raw.githubusercontent.com/codeskyblue/gosuv/master/get.sh | bash
```
Or just download binaries
<https://github.com/codeskyblue/gosuv/releases>
Thanks to [goreleaser](https://github.com/goreleaser/goreleaser) which makes publish binaries automaticly.
### Build from source
```sh
go get -d github.com/codeskyblue/gosuv
cd $GOPATH/src/github.com/codeskyblue/gosuv
go generate # package html resources into go
go build -tags vfs
```
## Quick start
After you installed gosuv, the first thing is to start server.
```sh
gosuv start-server
```
Basic operations
## 特点
```sh
$ gosuv status
PROGRAM NAME STATUS
test running
test_again stopped
- [x] web 控制与配置页面
- [x] 启动, 停止, 日志跟踪, 重载配置
- [x] 实时查看日志
- [x] 添加服务
- [x] 编辑服务
- [x] 删除服务
- [x] 可视化查看服务对应的cpu与内存
- [x] HTTP 基本安全认证
- [x] Github webhook
-
$ gosuv stop test
$ gosuv start test
```
## 使用
* 启动服务
Open web <http://localhost:11313> to see the manager page. And follow the gif to add a program to gosuv.
```
gosuv start-server
```
查看服务状态
![gosuv web](docs/gosuv.gif)
$ gosuv status
PROGRAM NAME STATUS
test running
test_again stopped
$ gosuv stop test
$ gosuv start test
## Configuration
Default config file stored in directory `$HOME/.gosuv/`, Set env-var `GOSUV_HOME_DIR` can change config file store directory.
默认端口 11113 本机测试请使用[http://localhost:11313](http://localhost:11313)
- file `programs.yml` contains all program settings.
- file `config.yml` contains server config
![例子](https://github.com/tiezhong2004/gosuv/blob/master/docs/des.gif)
File `config.yml` can be generated by `gosuv conftest`
## 配置
默认配置文件都放在 `$HOME/.gosuv/`
* 项目文件名 programs.yml
* 服务器配置文件名: config.yml
Example config.yaml
验证信息配置
```
```yml
server:
httpauth:
enabled: true
username: admin
password: admin
addr: :8083
name: 10.10.99.177
master: ""
notifications:
dingtalk:
groups:
- secret: c1b8032******************aa736a
mobile:
- "153********"
servername: locllhost
httpauth:
enabled: false
username: uu
password: pp
addr: :11313
client:
server_url: http://admin:admin@localhost:8083
server_url: http://localhost:11313
```
master 的支持来自 https://github.com/ihaiker/distributed-gosuv
Logs can be found in `$HOME/.gosuv/log/`
## 默认日志文件位置
`$HOME/.gosuv/log/`
Edit config file(default located in `$HOME/.gosuv/programs.yml`) and run `gosuv reload` will take effect immediately.
http RESTFul 接口
## Design
HTTP is follow the RESTFul guide.
Get or Update program
获取或更新
`<GET|PUT> /api/programs/:name`
Add new program
添加新的服务
`POST /api/programs`
Del program
删除服务
`DELETE /api/programs/:name`
## State
Only 4 states. [ref](http://supervisord.org/subprocess.html#process-states)
![states](docs/states.png)
## Notification
Configuration example
```yaml
- demo-program:
command: ...
notifications:
pushover:
api_key: [token]
users:
- [user1]
- [user2]
```
Now only support [pushover](https://pushover.net/api), and only status change to fatal will get notified.
## Integrate with github (todo)
This is feature that will helps update your deployment environment once your updated in the github.
This part is set in the `programs.yml`, take look the example
```yml
- demo-program:
command: python app.py
directory: /opt/demo
webhook:
github:
secret: 123456
command: git pull origin master
```
## Alternative
- Go implementation of supervisor <https://github.com/ochinchina/supervisord>
## 待续
内容不是很多,还是推荐能看懂英语的去看[英文的README](README.md)
## LICENSE
[MIT](LICENSE)
## 贡献人
- [Docking](http://miaomia.com)
- [wutiezhong](https://blog.csdn.net/tiezhong2004)

@ -1,48 +0,0 @@
# 概述
GoSuv是GO语言重写的类supervisor的一个进程管理程序简单易用界面美感十足且对用户友好
## 使用
* 启动服务
```
gosuv start-server
```
查看服务状态
```
$ gosuv status
Server is running
```
默认端口 11113 本机测试请使用[http://localhost:11313](http://localhost:11313)
![RunImage](docs/gosuv.gif)
## 配置
默认配置文件都放在 `$HOME/.gosuv/`
* 项目文件名 programs.yml
* 服务器配置文件名: config.yml
验证信息配置
```yml
server:
httpauth:
enabled: false
username: uu
password: pp
addr: :11313
client:
server_url: http://localhost:11313
```
## 默认日志文件位置
`$HOME/.gosuv/log/`
## 待续
内容不是很多,还是推荐能看懂英语的去看[英文的README](README.md)
## 贡献人
- [Docking](http://miaomia.com)

@ -3,7 +3,13 @@
package main
import "net/http"
import (
"net/http"
)
// Assets contains project assets.
var Assets http.FileSystem = http.Dir("res")
//var Assets http.FileSystem = http.Dir("res")
var Assets http.FileSystem //= rice.MustFindBox("res").HTTPBox()
//var fff, _ = rice.FindBox("res")
//var Assets http.FileSystem = fff.HTTPBox()

@ -10,7 +10,7 @@ import (
)
func main() {
var fs http.FileSystem = http.Dir("res")
var fs http.FileSystem = rice.MustFindBox("res").HTTPBox()
err := vfsgen.Generate(fs, vfsgen.Options{
PackageName: "main",

@ -1,39 +0,0 @@
package main
import "sync/atomic"
type Int64 struct {
v int64
}
func (a *Int64) Get() int64 {
return atomic.LoadInt64(&a.v)
}
func (a *Int64) Set(v int64) {
atomic.StoreInt64(&a.v, v)
}
func (a *Int64) CompareAndSwap(o, n int64) bool {
return atomic.CompareAndSwapInt64(&a.v, o, n)
}
func (a *Int64) Swap(v int64) int64 {
return atomic.SwapInt64(&a.v, v)
}
func (a *Int64) Add(v int64) int64 {
return atomic.AddInt64(&a.v, v)
}
func (a *Int64) Sub(v int64) int64 {
return a.Add(-v)
}
func (a *Int64) Incr() int64 {
return a.Add(1)
}
func (a *Int64) Decr() int64 {
return a.Add(-1)
}

@ -0,0 +1,56 @@
package main
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
// func TestBroadcast(t *testing.T) {
// bs := NewBroadcastString()
// bs.WriteMessage("hello")
// time.Sleep(10 * time.Millisecond)
// c1 := bs.AddListener(nil)
// go func() {
// bs.WriteMessage("world")
// }()
// message := <-c1
// if message != "world" {
// t.Fatalf("expect message world, but got %s", message)
// }
// c2 := bs.AddListener(nil)
// go func() {
// bs.WriteMessage("tab")
// }()
// // test write multi
// wg := sync.WaitGroup{}
// wg.Add(2)
// go func() {
// message = <-c2
// if message != "tab" {
// t.Errorf("expect tab, but got %s", message)
// }
// wg.Done()
// }()
// go func() {
// message = <-c1
// if message != "tab" {
// t.Errorf("expect tab, but got %s", message)
// }
// wg.Done()
// }()
// wg.Wait()
// }
func TestRingBuffer(t *testing.T) {
Convey("Write some string to ring buffer", t, func() {
// buf := rbuf.NewFixedSizeRingBuf(5)
// buf.Write([]byte("abcde"))
// So(string(buf.Bytes()), ShouldEqual, "abcde")
// buf.Advance(2)
// buf.Write([]byte("fg"))
// So(string(buf.Bytes()), ShouldEqual, "cdefg")
})
}

@ -1,40 +0,0 @@
package main
import (
"bytes"
)
// BufferPool implements a pool of bytes.Buffers in the form of a bounded
// channel.
type BufferPool struct {
c chan *bytes.Buffer
}
// NewBufferPool creates a new BufferPool bounded to the given size.
func NewBufferPool(size int) (bp *BufferPool) {
return &BufferPool{
c: make(chan *bytes.Buffer, size),
}
}
// Get gets a Buffer from the BufferPool, or creates a new one if none are
// available in the pool.
func (bp *BufferPool) Get() (b *bytes.Buffer) {
select {
case b = <-bp.c:
// reuse existing buffer
default:
// create new buffer
b = bytes.NewBuffer([]byte{})
}
return
}
// Put returns the given Buffer to the BufferPool.
func (bp *BufferPool) Put(b *bytes.Buffer) {
b.Reset()
select {
case bp.c <- b:
default: // Discard the buffer if the pool is full.
}
}

@ -14,9 +14,8 @@ import (
"strings"
"time"
// "github.com/franela/goreq"
"github.com/franela/goreq"
"github.com/goji/httpauth"
"github.com/imroc/req"
"github.com/urfave/cli"
)
@ -42,30 +41,23 @@ func actionStartServer(c *cli.Context) error {
if err != nil {
log.Fatal(err)
}
if c.Bool("foreground") {
if err = newDistributed(suv, hdlr); err != nil {
log.Fatal(err)
}
}
auth := cfg.Server.HttpAuth
if auth.Enabled {
hdlr = httpauth.SimpleBasicAuth(auth.User, auth.Password)(hdlr)
}
http.Handle("/", hdlr)
addr := cfg.Server.Addr
if c.Bool("foreground") {
suv.AutoStartPrograms()
log.Printf("server listen on %v", addr)
log.Fatal(http.ListenAndServe(addr, nil))
log.Fatal(http.ListenAndServe(addr, nil)) //http.FileServer(rice.MustFindBox("res").HTTPBox())))
} else {
if checkServerStatus() == nil {
fmt.Println("server is already running")
return nil
}
logPath := filepath.Join(defaultGosuvDir, "gosuv.log")
logPath := filepath.Join(defaultConfigDir, "gosuv.log")
logFd, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("create file %s failed: %v", logPath, err)
@ -77,9 +69,6 @@ func actionStartServer(c *cli.Context) error {
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("/var/run/gosuv.pid", []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil {
log.Fatalln(err)
}
select {
case err = <-GoFunc(cmd.Wait):
log.Fatalf("server started failed, %v", err)
@ -95,10 +84,9 @@ func actionStartServer(c *cli.Context) error {
}
func actionStatus(c *cli.Context) error {
// res, err := goreq.Request{
// Uri: cfg.Client.ServerURL + "/api/programs",
// }.Do()
res, err := req.Get(cfg.Client.ServerURL + "/api/programs")
res, err := goreq.Request{
Uri: cfg.Client.ServerURL + "/api/programs",
}.Do()
if err != nil {
return err
}
@ -106,10 +94,7 @@ func actionStatus(c *cli.Context) error {
Program Program `json:"program"`
Status string `json:"status"`
}, 0)
// if err := res.Body.FromJsonTo(&programs); err != nil {
// return err
// }
if err := res.ToJSON(&programs); err != nil {
if err := res.Body.FromJsonTo(&programs); err != nil {
return err
}
format := "%-23s\t%-8s\n"
@ -122,22 +107,17 @@ func actionStatus(c *cli.Context) error {
// cmd: <start|stop>
func programOperate(cmd, name string) (err error, success bool) {
// res, err := goreq.Request{
// Method: "POST",
// Uri: cfg.Client.ServerURL + "/api/programs/" + name + "/" + cmd,
// }.Do()
res, err := req.Post(cfg.Client.ServerURL + "/api/programs/" + name + "/" + cmd)
res, err := goreq.Request{
Method: "POST",
Uri: cfg.Client.ServerURL + "/api/programs/" + name + "/" + cmd,
}.Do()
if err != nil {
return
}
var v = struct {
Status int `json:"status"`
}{}
// if err = res.Body.FromJsonTo(&v); err != nil {
// return
// }
if err = res.ToJSON(&v); err != nil {
if err = res.Body.FromJsonTo(&v); err != nil {
return
}
success = v.Status == 0
@ -184,7 +164,7 @@ func actionShutdown(c *cli.Context) error {
}
func actionUpdateSelf(c *cli.Context) error {
return githubUpdate(c.Bool("yes"))
return equinoxUpdate(c.String("channel"), c.Bool("yes"))
}
func actionEdit(c *cli.Context) error {

@ -10,16 +10,14 @@ import (
type Configuration struct {
Server struct {
ServerName string `yaml:"servername"`
HttpAuth struct {
Enabled bool `yaml:"enabled"`
User string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"httpauth"`
Addr string `yaml:"addr"`
Name string `yaml:"name"`
Master string `yaml:"master"`
} `yaml:"server,omitempty"`
Notifications Notifications `yaml:"notifications,omitempty" json:"-"`
Addr string `yaml:"addr"`
} `yaml:"server"`
Client struct {
ServerURL string `yaml:"server_url"`
@ -28,6 +26,7 @@ type Configuration struct {
func readConf(filename string) (c Configuration, err error) {
// initial default value
c.Server.ServerName = "localhost"
c.Server.Addr = ":11313" // in memory of 08-31 13:13
c.Client.ServerURL = "http://localhost:11313"

@ -1,249 +0,0 @@
/*
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/bluele/gcache"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/qiniu/log"
)
type Cluster struct {
slaves gcache.Cache
client *http.Client
suv *Supervisor
}
func (cluster *Cluster) join() {
data := url.Values{"slave": []string{cfg.Server.Addr}}
request, err := http.NewRequest(http.MethodPost, "http://"+cfg.Server.Master+"/distributed/join", strings.NewReader(data.Encode()))
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
if err != nil {
log.Errorf("join cluster %s : %s", cfg.Server.Master, err)
return
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
log.Errorf("join cluster %s : %s", cfg.Server.Master, err)
return
}
if resp.StatusCode == http.StatusOK {
log.Debugf("join to master %s", cfg.Server.Master)
} else {
log.Debugf("join to master %s error: %d", cfg.Server.Master, resp.StatusCode)
}
}
func (cluster *Cluster) auth(request *http.Request) {
if cfg.Server.HttpAuth.Enabled {
request.SetBasicAuth(cfg.Server.HttpAuth.User, cfg.Server.HttpAuth.Password)
}
}
func (cluster *Cluster) dialWebSocket(wsUrl string) (*websocket.Conn, *http.Response, error) {
var dialer *websocket.Dialer
if cfg.Server.HttpAuth.Enabled {
dialer = &websocket.Dialer{Proxy: func(r *http.Request) (*url.URL, error) {
cluster.auth(r)
return websocket.DefaultDialer.Proxy(r)
}}
} else {
dialer = websocket.DefaultDialer
}
return dialer.Dial(wsUrl, nil)
}
func (cluster *Cluster) requestSlave(url, method string, bodyBuffer *bytes.Buffer) ([]byte, error) {
request, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func (cluster *Cluster) cmdJoinCluster(w http.ResponseWriter, r *http.Request) {
slave := r.PostFormValue("slave")
if slave == "" {
w.WriteHeader(http.StatusForbidden)
return
}
if strings.HasPrefix(slave, ":") {
idx := strings.LastIndex(r.RemoteAddr, ":")
slave = r.RemoteAddr[:idx] + slave
}
log.Debugf("%s join cluster.", slave)
if out, err := cluster.slaves.Get(slave); err != nil || out == nil {
cluster.suv.broadcastEvent("new slave : " + slave)
}
cluster.slaves.Set(slave, slave)
w.WriteHeader(http.StatusOK)
}
//获取分布式系统下所有的内容
func (cluster *Cluster) cmdQueryDistributedPrograms(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
slaves := []string{}
for _, v := range cluster.slaves.GetALL() {
if slave, ok := v.(string); ok {
slaves = append(slaves, slave)
}
}
sort.Strings(slaves)
jsonOut := "{"
idx := 0
for _, slave := range slaves {
reqUrl := fmt.Sprintf("http://%s/api/programs", slave)
if body, err := cluster.requestSlave(reqUrl, http.MethodGet, nil); err == nil {
jsonOut += fmt.Sprintf("\"%s\":%s", slave, body)
}
if idx < cluster.slaves.Len()-1 {
jsonOut += ","
}
idx += 1
}
jsonOut += "}"
w.Write([]byte(jsonOut))
}
func (cluster *Cluster) cmdSetting(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
slave := mux.Vars(r)["slave"]
cluster.suv.renderHTML(w, "setting", map[string]string{
"Name": name,
"Slave": slave,
})
}
func (cluster *Cluster) cmdWebSocketProxy(w http.ResponseWriter, r *http.Request) {
sock, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error("upgrade:", err)
return
}
defer sock.Close()
slave := mux.Vars(r)["slave"]
slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1)
wsUrl := fmt.Sprintf("ws://%s%s", slave, slaveUri)
log.Infof("proxy websocket :%s", wsUrl)
ws, _, err := cluster.dialWebSocket(wsUrl)
if err != nil {
log.Error("dial:", err)
return
}
defer ws.Close()
for {
messageType, data, err := ws.ReadMessage()
if err != nil {
log.Error("read message:", err)
return
}
if messageType == websocket.CloseMessage {
log.Infof("close socket")
return
}
w, err := sock.NextWriter(messageType)
if err != nil {
log.Error("write err:", err)
return
}
_, err = w.Write(data)
if err != nil {
log.Error("read:", err)
return
}
}
}
func (cluster *Cluster) slaveHttpProxy(w http.ResponseWriter, r *http.Request) {
slave := mux.Vars(r)["slave"]
slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1)
requestUrl := fmt.Sprintf("http://%s%s", slave, slaveUri)
log.Infof("proxy :%s %s", r.Method, requestUrl)
request, err := http.NewRequest(r.Method, requestUrl, r.Body)
for k, v := range r.Header {
request.Header.Set(k, strings.Join(v, ","))
}
if err != nil {
log.Error(err)
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
log.Error(err)
}
defer resp.Body.Close()
if body, err := ioutil.ReadAll(resp.Body); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
} else {
for k, v := range resp.Header {
w.Header().Set(k, strings.Join(v, ","))
}
w.Write(body)
cluster.suv.broadcastEvent("execute ok : " + slaveUri)
}
}
func newDistributed(suv *Supervisor, hdlr http.Handler) error {
cluster.suv = suv
r := hdlr.(*mux.Router)
r.HandleFunc("/distributed/join", cluster.cmdJoinCluster).Methods("POST")
r.HandleFunc("/distributed/api/programs", cluster.cmdQueryDistributedPrograms).Methods("GET")
r.HandleFunc("/distributed/{slave}/settings/{name}", cluster.cmdSetting)
for _, path := range []string{
"/distributed/{slave}/api/programs", "/distributed/{slave}/api/programs/{name}",
"/distributed/{slave}/api/programs/{name}/start", "/distributed/{slave}/api/programs/{name}/stop",
} {
r.HandleFunc(path, cluster.slaveHttpProxy)
}
r.HandleFunc("/distributed/{slave}/ws/logs/{name}", cluster.cmdWebSocketProxy)
r.HandleFunc("/distributed/{slave}/ws/perfs/{name}", cluster.cmdWebSocketProxy)
if cfg.Server.Master != "" {
go func() {
t1 := time.NewTimer(time.Second)
for {
select {
case <-t1.C:
cluster.join()
t1.Reset(time.Second)
}
}
}()
}
return nil
}
var cluster = Cluster{
slaves: gcache.New(1000).LRU().Expiration(time.Second * 3).Build(),
client: new(http.Client),
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

149
fsm.go

@ -23,19 +23,15 @@ import (
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/axgle/pinyin"
"github.com/codeskyblue/gosuv/pushover"
"github.com/codeskyblue/kexec"
"github.com/kennygrant/sanitize"
"github.com/lunny/dingtalk_webhook"
"github.com/natefinch/lumberjack"
"github.com/qiniu/log"
"github.com/soopsio/gosuv/pushover"
"github.com/soopsio/kexec"
)
type FSMState string
@ -117,32 +113,20 @@ type Program struct {
StartRetries int `yaml:"start_retries" json:"startRetries"`
StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"`
StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"`
retryCount int
User string `yaml:"user,omitempty" json:"user"`
Notifications Notifications `yaml:"notifications,omitempty" json:"-"`
WebHook WebHook `yaml:"webhook,omitempty" json:"-"`
}
type Notifications struct {
Pushover struct {
ApiKey string `yaml:"api_key"`
Users []string `yaml:"users"`
} `yaml:"pushover,omitempty"`
Dingtalk struct {
Groups []struct {
Secret string `yaml:"secret"`
Mobiles []string `yaml:"mobile"`
} `yaml:"groups"`
} `yaml:"dingtalk,omitempty"`
}
type WebHook struct {
Github struct {
Secret string `yaml:"secret"`
} `yaml:"github"`
Command string `yaml:"command"`
Timeout int `yaml:"timeout"`
User string `yaml:"user,omitempty" json:"user"`
Notifications struct {
Pushover struct {
ApiKey string `yaml:"api_key"`
Users []string `yaml:"users"`
} `yaml:"pushover,omitempty"`
} `yaml:"notifications,omitempty" json:"-"`
WebHook struct {
Github struct {
Secret string `yaml:"secret"`
} `yaml:"github"`
Command string `yaml:"command"`
Timeout int `yaml:"timeout"`
} `yaml:"webhook,omitempty" json:"-"`
}
func (p *Program) Check() error {
@ -159,44 +143,18 @@ func (p *Program) Check() error {
return nil
}
func (p *Program) RunNotification(state FSMState) {
notis := []Notifications{}
notis = append(notis, cfg.Notifications)
t := time.Now().Format("2006-01-02 15:04:05")
host := ""
if cfg.Server.Name != "" {
host = cfg.Server.Name
} else {
host, _ = os.Hostname()
}
msg := fmt.Sprintf("[%s] %s: \"%s\" changed: \"%s\"", t, host, p.Name, state)
if state == RetryWait {
msg += " retryCount:" + strconv.Itoa(p.retryCount)
}
for _, noti := range notis {
po := noti.Pushover
if po.ApiKey != "" && len(po.Users) > 0 {
for _, user := range po.Users {
err := pushover.Notify(pushover.Params{
Token: po.ApiKey,
User: user,
Title: "gosuv",
Message: msg,
})
if err != nil {
log.Warnf("pushover error: %v", err)
}
}
}
pw := noti.Dingtalk
if len(pw.Groups) > 0 {
for _, group := range pw.Groups {
ding := dingtalk.NewWebhook(group.Secret)
err := ding.SendTextMsg(msg, false, group.Mobiles...)
if err != nil {
log.Error("钉钉通知失败:", msg, err)
}
func (p *Program) RunNotification() {
po := p.Notifications.Pushover
if po.ApiKey != "" && len(po.Users) > 0 {
for _, user := range po.Users {
err := pushover.Notify(pushover.Params{
Token: po.ApiKey,
User: user,
Title: "gosuv",
Message: fmt.Sprintf("%s change to fatal", p.Name),
})
if err != nil {
log.Warnf("pushover error: %v", err)
}
}
}
@ -214,7 +172,7 @@ type Process struct {
Stdout *QuickLossBroadcastWriter `json:"-"`
Stderr *QuickLossBroadcastWriter `json:"-"`
Output *QuickLossBroadcastWriter `json:"-"`
OutputFile io.WriteCloser `json:"-"`
OutputFile *os.File `json:"-"`
stopC chan syscall.Signal
retryLeft int
Status string `json:"status"`
@ -226,23 +184,13 @@ type Process struct {
func (p *Process) buildCommand() *kexec.KCommand {
cmd := kexec.CommandString(p.Command)
// cmd := kexec.Command(p.Command[0], p.Command[1:]...)
logDir := filepath.Join(defaultGosuvDir, "log", sanitize.Name(pinyin.Convert(p.Name)))
logDir := filepath.Join(defaultConfigDir, "log", sanitize.Name(p.Name))
if !IsDir(logDir) {
os.MkdirAll(logDir, 0755)
}
var fout io.Writer
var err error
// p.OutputFile, err = os.OpenFile(filepath.Join(logDir, "output.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
// p.OutputFile = NewRotate(filepath.Join(logDir, "output.log"))
p.OutputFile = &lumberjack.Logger{
Filename: filepath.Join(logDir, "output.log"),
MaxSize: 1024,
MaxAge: 14,
MaxBackups: 14,
Compress: false,
LocalTime: true,
}
p.OutputFile, err = os.OpenFile(filepath.Join(logDir, "output.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Warn("create stdout log failed:", err)
fout = ioutil.Discard
@ -289,14 +237,10 @@ func (p *Process) buildCommand() *kexec.KCommand {
}
func (p *Process) waitNextRetry() {
if p.OutputFile != nil {
p.OutputFile.Close()
}
p.SetState(RetryWait)
if p.retryLeft <= 0 {
p.retryLeft = p.StartRetries
p.SetState(Fatal)
p.cmd = nil
return
}
p.retryLeft -= 1
@ -315,28 +259,14 @@ func (p *Process) stopCommand() {
if p.cmd == nil {
return
}
p.SetState(Stopping)
if p.cmd.Process != nil {
stopch := make(chan bool)
go func() {
p.cmd.Process.Signal(syscall.SIGTERM)
stopch <- true
}()
select {
case <-stopch: // TODO(ssx): add it to config
log.Println(p.Name, "停止完成")
case <-time.After(10 * time.Second):
log.Println(p.Name, "停止超时,强制 kill")
p.cmd.Process.Signal(syscall.SIGKILL)
}
p.cmd.Process.Signal(syscall.SIGTERM) // TODO(ssx): add it to config
}
select {
case <-GoFunc(p.cmd.Wait):
p.RunNotification(FSMState("quit normally"))
log.Printf("program(%s) quit normally", p.Name)
case <-time.After(time.Duration(p.StopTimeout) * time.Second): // TODO: add 3s to config
p.RunNotification(FSMState("terminate all"))
log.Printf("program(%s) terminate all", p.Name)
p.cmd.Terminate(syscall.SIGKILL) // cleanup
}
@ -369,11 +299,8 @@ func (p *Process) startCommand() {
if err := p.cmd.Start(); err != nil {
log.Warnf("program %s start failed: %v", p.Name, err)
p.SetState(Fatal)
p.cmd = nil
return
}
// 如果是running状态重置 retryLeft
p.retryLeft = p.StartRetries
go func() {
errC := GoFunc(p.cmd.Wait)
startTime := time.Now()
@ -385,19 +312,15 @@ func (p *Process) startCommand() {
if time.Since(startTime) < time.Duration(p.StartSeconds)*time.Second {
if p.retryLeft == p.StartRetries { // If first time quit so fast, just set to fatal
p.SetState(Fatal)
p.cmd = nil
p.RunNotification(Fatal)
log.Printf("program(%s) exit too quick, status -> fatal", p.Name)
return
}
}
p.waitNextRetry()
case <-p.stopC:
log.Println("recv stop command")
p.stopCommand() // clean up all process
}
}()
}
@ -415,13 +338,11 @@ func NewProcess(pg Program) *Process {
}
pr.StateChange = func(_, newStatus FSMState) {
pr.Status = string(newStatus)
// TODO: status need to filter with config, not hard coded.
// if newStatus == Fatal {
if newStatus == RetryWait {
pr.Program.retryCount++
if newStatus == Fatal {
go pr.Program.RunNotification()
}
go pr.Program.RunNotification(newStatus)
// }
}
if pr.StartSeconds <= 0 {
pr.StartSeconds = 3

@ -9,7 +9,7 @@ usage() {
$this: download go binaries for codeskyblue/gosuv
Usage: $this [-b] bindir [version]
-b sets bindir or installation directory, default "/usr/local/bin"
-b sets bindir or installation directory, default "./bin"
[version] is a version number from
https://github.com/codeskyblue/gosuv/releases
If version is missing, then an attempt to find the latest will be found.
@ -25,7 +25,7 @@ parse_args() {
#BINDIR is /usr/local/bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-/usr/local/bin}
BINDIR=${BINDIR:-"/usr/local/bin"}
while getopts "b:h?" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
@ -58,20 +58,9 @@ is_supported_platform() {
found=1
case "$platform" in
linux/amd64) found=0 ;;
linux/386) found=0 ;;
darwin/amd64) found=0 ;;
windows/amd64) found=0 ;;
linux/armv6) found=0 ;;
linux/armv7) found=0 ;;
darwin/armv6) found=0 ;;
darwin/armv7) found=0 ;;
windows/armv6) found=0 ;;
windows/armv7) found=0 ;;
esac
case "$platform" in
darwin/arm) found=1 ;;
darwin/386) found=0 ;;
esac
return $found
}
@ -128,9 +117,9 @@ uname_arch() {
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
armv5*) arch="arm5" ;;
armv6*) arch="arm6" ;;
armv7*) arch="arm7" ;;
esac
echo ${arch}
}
@ -161,6 +150,7 @@ uname_arch_check() {
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
armv7l) arch="armv7"; return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
@ -304,11 +294,11 @@ adjust_arch
echo "$PREFIX: found version ${VERSION} for ${OS}/${ARCH}"
NAME=${BINARY}_${VERSION#v}_${OS}_${ARCH}
NAME=${BINARY}_${VERSION}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${VERSION}/${TARBALL}
TARBALL_URL=${GITHUB_DOWNLOAD}/v${VERSION}/${TARBALL}
CHECKSUM=${BINARY}_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${VERSION}/${CHECKSUM}
CHECKSUM_URL=${GITHUB_DOWNLOAD}/v${VERSION}/${CHECKSUM}
# Adjust binary name if windows
if [ "$OS" = "windows" ]; then

70
glide.lock generated

@ -0,0 +1,70 @@
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,19 +1,18 @@
package: github.com/soopsio/gosuv
package: github.com/codeskyblue/gosuv
import:
- package: github.com/soopsio/kexec
- package: github.com/codeskyblue/kexec
- package: github.com/equinox-io/equinox
- package: github.com/imroc/req
- package: github.com/franela/goreq
- package: github.com/glycerine/rbuf
- package: github.com/go-yaml/yaml
- package: github.com/goji/httpauth
- package: github.com/gorilla/mux
- package: github.com/gorilla/websocket
- package: github.com/kennygrant/sanitize
- package: github.com/shirou/gopsutil/process
- package: github.com/mitchellh/go-ps
- package: github.com/qiniu/log
- package: github.com/shurcooL/vfsgen
- package: github.com/urfave/cli
- package: github.com/lunny/dingtalk_webhook
testImport:
- package: github.com/smartystreets/goconvey
subpackages:

@ -0,0 +1,22 @@
module gosuv
go 1.18
require (
github.com/codeskyblue/kexec v0.0.0-20160908020525-863094f94c7f
github.com/equinox-io/equinox v0.0.0-20151227023206-6f97d0d39708
github.com/franela/goreq v0.0.0-20170418064916-b5b0f5eb2d16
github.com/glycerine/rbuf v0.0.0-20170809002439-96ad00d7fa74
github.com/go-yaml/yaml v0.0.0-20170721122051-25c4ec802a7d
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d
github.com/gorilla/context v1.1.1
github.com/gorilla/mux v1.4.1-0.20170704074345-ac112f7d75a0
github.com/gorilla/websocket v1.2.1-0.20170718202341-a69d9f6de432
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
github.com/shurcooL/httpfs v0.0.0-20170514040059-bc35257962c2
github.com/shurcooL/vfsgen v0.0.0-20170728162603-385e5833a54a
github.com/urfave/cli v1.20.0
golang.org/x/net v0.0.0-20170809000501-1c05540f6879
golang.org/x/tools v0.0.0-20170808144645-5831d16d1802
)

@ -8,84 +8,64 @@ import (
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/imroc/req"
"github.com/equinox-io/equinox"
"github.com/qiniu/log"
"github.com/urfave/cli"
)
const appID = "app_8Gji4eEAdDx"
//const appID = "app_8Gji4eEAdDx"
const appID = "aaaaaaaaaaaaaa"
var (
version string = "master"
cfg Configuration
version string = "master"
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEY8xsSkcFs8XXUicw3n7E77qN/vqKUQ/6
/X5aBiOVF1yTIRYRXrV3aEvJRzErvQxziT9cLxQq+BFUZqn9pISnPSf9dn0wf9kU
TxI79zIvne9UT/rDsM0BxSydwtjG00MT
-----END ECDSA PUBLIC KEY-----
`)
cfg Configuration
)
type TagInfo struct {
Version string `json:"tag_name"`
Body string `json:"body"`
CreatedAt string `json:"created_at"`
}
func githubLatestVersion(repo, name string) (tag TagInfo, err error) {
githubURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repo, name)
r := req.New()
h := req.Header{}
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken != "" {
h["Authorization"] = "token " + ghToken
}
res, err := r.Get(githubURL, h)
if err != nil {
return
}
err = res.ToJSON(&tag)
return
}
func githubUpdate(skipConfirm bool) error {
repo, name := "soopsio", "gosuv"
tag, err := githubLatestVersion(repo, name)
if err != nil {
fmt.Println("Update failed:", err)
func equinoxUpdate(channel string, skipConfirm bool) error {
var opts equinox.Options
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return err
}
if tag.Version == version {
opts.Channel = channel
// 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:
fmt.Println("Update failed:", err)
return err
}
fmt.Println("New version available -- ", tag.Version)
fmt.Print(tag.Body)
fmt.Println("New version available!")
fmt.Println("Version:", resp.ReleaseVersion)
fmt.Println("Name:", resp.ReleaseTitle)
fmt.Println("Details:", resp.ReleaseDescription)
if !skipConfirm {
if !askForConfirmation("Would you like to update [Y/n]? ", true) {
fmt.Printf("Would you like to update [y/n]? ")
if !askForConfirmation() {
return nil
}
}
fmt.Printf("New version available: %s downloading ... \n", tag.Version)
// // fetch the update and apply it
// err = resp.Apply()
// if err != nil {
// return err
// }
cleanVersion := tag.Version
if strings.HasPrefix(cleanVersion, "v") {
cleanVersion = cleanVersion[1:]
//fmt.Printf("New version available: %s downloading ... \n", resp.ReleaseVersion)
// fetch the update and apply it
err = resp.Apply()
if err != nil {
return err
}
osArch := runtime.GOOS + "_" + runtime.GOARCH
downloadURL := StringFormat("https://github.com/{repo}/{name}/releases/download/{tag}/{name}_{version}_{os_arch}.tar.gz", map[string]interface{}{
"repo": "codeskyblue",
"name": "gosuv",
"tag": tag.Version,
"version": cleanVersion,
"os_arch": osArch,
})
fmt.Println("Not finished yet. download from:", downloadURL)
// fmt.Printf("Updated to new version: %s!\n", tag.Version)
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
return nil
}
@ -111,7 +91,7 @@ func checkServerStatus() error {
}
func main() {
var defaultConfigPath = filepath.Join(defaultGosuvDir, "conf/config.yml")
var defaultConfigPath = filepath.Join(defaultConfigDir, "config.yml")
app := cli.NewApp()
app.Name = "gosuv"
@ -128,8 +108,8 @@ func main() {
}
app.Authors = []cli.Author{
cli.Author{
Name: "codeskyblue",
Email: "codeskyblue@gmail.com",
Name: "wutz",
Email: "58516795@qq.com",
},
}
app.Flags = []cli.Flag{

@ -1,152 +0,0 @@
package main
import (
"bytes"
"fmt"
"io"
"time"
log "github.com/qiniu/log"
)
type Bool struct {
c Int64
}
func (b *Bool) Get() bool {
return b.c.Get() != 0
}
func (b *Bool) toInt64(v bool) int64 {
if v {
return 1
} else {
return 0
}
}
func (b *Bool) Set(v bool) {
b.c.Set(b.toInt64(v))
}
func (b *Bool) CompareAndSwap(o, n bool) bool {
return b.c.CompareAndSwap(b.toInt64(o), b.toInt64(n))
}
func (b *Bool) Swap(v bool) bool {
return b.c.Swap(b.toInt64(v)) != 0
}
var bufferPool *BufferPool
func init() {
// 4000行日志缓存
bufferPool = NewBufferPool(4000)
}
type MergeWriter struct {
lines chan *bytes.Buffer
writer io.Writer
closed Bool
}
func NewMergeWriter(writer io.Writer) *MergeWriter {
merger := &MergeWriter{
lines: make(chan *bytes.Buffer, 1000),
writer: writer,
}
merger.closed.Set(false)
merger.drainLines()
return merger
}
func (m *MergeWriter) Close() {
// log.Printf("Close MergeWriter")
if m.closed.CompareAndSwap(false, true) {
// log.Printf("Close lines chan")
close(m.lines)
}
}
func (m *MergeWriter) WriteStrLine(line string) {
if m.closed.Get() {
return
} else {
buffer := bufferPool.Get()
buffer.WriteString(line)
m.lines <- buffer
}
}
func (m *MergeWriter) WriteLine(line *bytes.Buffer) {
if m.closed.Get() {
// 需要回收Buffer
// log.Printf("Write to closed MergeWrite...")
bufferPool.Put(line)
return
} else {
m.lines <- line
}
}
func (m *MergeWriter) drainLines() {
go func() {
for line := range m.lines {
m.writer.Write(line.Bytes())
// 回收
bufferPool.Put(line)
}
}()
}
// 创建新的BufferWriter
func (m *MergeWriter) NewWriter(index int) io.Writer {
writer := &BufferWriter{
merge: m,
prefix: fmt.Sprintf(" [P%02d] ", index),
}
// 分配
writer.Buffer = bufferPool.Get()
writer.Buffer.WriteString(writer.prefix)
return writer
}
type BufferWriter struct {
Buffer *bytes.Buffer
prefix string
merge *MergeWriter
}
func (b *BufferWriter) Write(p []byte) (n int, err error) {
n = len(p)
for len(p) > 0 {
index := bytes.IndexByte(p, '\n')
if index != -1 {
// 写完完整的一行
_, err = b.Buffer.Write(p[0 : index+1])
if err != nil {
log.Error(err, "Writer Buffer failed")
return n, err
}
// 将buffer转移到merge中
b.merge.WriteLine(b.Buffer)
// 分配:写入新数据
b.Buffer = bufferPool.Get()
b.Buffer.WriteString(time.Now().Format("15:04:05") + b.prefix)
p = p[index+1:]
} else {
// 剩下不足一行,一口气全部写入
_, err = b.Buffer.Write(p)
if err != nil {
log.Error(err, "Writer Buffer failed")
return n, err
}
break
}
}
return n, nil
}

@ -37,11 +37,3 @@ small.user {
.color-red {
color: red;
}
.panel-body{
padding: 0px;
}
.table {
margin: 0px;
}

@ -2,308 +2,265 @@
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Distributed Go Supervisor 1.0</title>
<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/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>后台服务管理</title>
<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/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
</head>
<body id="app">
<nav class="navbar navbar-inverse">
<nav class="navbar navbar-inverse">
<div class="container">
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Distributed Go Supervisor 1.0
<small class="user">[[.User]]</small>
</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<!-- <li class="hidden-xs">
<a>
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li> -->
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">后台服务管理 <small class="user">[[.User]]</small></a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<!-- <li class="hidden-xs">
<a>
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li> -->
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</nav>
<div class="container">
</nav>
<div class="container">
<div class="col-md-12">
<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>
</div>
<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>
</div>
</div>
<!--
<div class="col-md-12">
<button class="btn btn-primary" v-on:click="refresh">
<span class="glyphicon glyphicon-refresh"></span> Refresh
</button>
<button class="btn btn-default btn-sm" id="launchNewProgram">
<span class="glyphicon glyphicon-plus"></span> New Program
</button>
<button class="btn btn-default btn-sm" v-on:click="refresh">
<span class="glyphicon glyphicon-refresh"></span> Refresh
</button>
<button class="btn btn-default btn-sm" v-on:click="reload">
<span class="glyphicon glyphicon-repeat"></span> Reload
</button>
<button class="btn btn-default btn-sm" v-on:click="input config" data-toggle="tooltip" data-placement="bottom" title="todo: input config on bottom">
<span class="glyphicon glyphicon-import"></span> Input Config
</button>
<button class="btn btn-default btn-sm" v-on:click="output config" data-toggle="tooltip" data-placement="bottom" title="todo: output config on bottom">
<span class="glyphicon glyphicon-export"></span> Onput Config
</button>
<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
</button>
</div>
-->
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Master
<span class="panel-icon pull-right">
<button class="btn btn-success btn-sm" rel="tooltip" v-on:click="addNewProgram()" data-original-title="Click to add programs"><i class="fa fa-plus"></i>New Program</button>
</span>
</div>
<div class="panel-body">
<table class="table table-hover table-bordered">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in programs">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<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
</button>
<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
</button>
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-for="(slave,items) in slaves" class="panel panel-default">
<div class="panel-heading">
{{slave}}
<span class="panel-icon pull-right">
<button class="btn btn-success btn-sm" rel="tooltip" v-on:click="addNewProgram(slave)"
data-original-title="Click to add programs">
<i class="fa fa-plus"></i>New Program
</button>
</span>
</div>
<div class="panel-body">
<table class="table table-hover table-bordered">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in items">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name,slave)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/distributed/{{slave}}/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<button v-on:click="cmdStart(p.program.name,slave)" class="btn btn-default btn-xs"
:disabled='["running", "stopping"].indexOf(p.status) != -1'>
<span class="glyphicon glyphicon-play"></span> Start
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name,slave)"
:disabled="!canStop(p.status)">
<span class="glyphicon glyphicon-stop"></span> Stop
</button>
<button v-on:click="showEditProgram(p.program,slave)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name,slave)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<table class="table table-hover">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in programs">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<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
</button>
<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
</button>
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12">
<div id="footer" class="pull-right" style="margin: 2em 1em">
<div>
<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>
<a href="https://github.com/ihaiker/distributed-gosuv">distributed-gosuv ([[.Version]])</a>, written by
<a href="https://github.com/ihaiker">haiker</a>. 2017. go1.8
</div>
</div>
<div id="footer" class="pull-right" style="margin: 2em 1em">
<a >后台服务管理器1.0</a>, update by <a href="https://github.com/tiezhong2004">wutz</a>. 2020
</div>
</div>
<!-- panels -->
<!-- modals -->
<div class="modal" id="newProgram">
<div class="modal-dialog">
<div class="modal-content">
<form id="formNewProgram" action="" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">New program</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" v-model="program.name" class="form-control"
placeholder="name (must be unique)" required>
</div>
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control"
placeholder="shell command, ex: redis-server --port 6379">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" placeholder="user, optional">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0"
step="1" value="3">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" v-on:click="formNewProgram()">Add program</button>
</div>
</form>
<div class="modal-dialog">
<div class="modal-content">
<form id="formNewProgram" action="/api/programs" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">New program</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" v-model="program.name" class="form-control" placeholder="name (must be unique)" required>
</div>
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" placeholder="shell command, ex: redis-server --port 6379">
</div>
<div class="form-group">
<label>Environment</label>
<input type="text" name="environ" class="form-control" placeholder="Environment, ex: LD_LIB=ABC;">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" placeholder="user, optional">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" value="3">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Add program</button>
</div>
</form>
</div>
</div>
</div>
<!-- model edit -->
<div class="modal" id="programEdit">
<div class="modal-dialog">
<div class="modal-content">
<form v-on:submit.prevent="editProgram">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span
v-text="edit.program.name"></span></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" v-model="edit.program.command">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" v-model="edit.program.user">
</div>
<div class="form-group">
<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">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
<div class="modal-dialog">
<div class="modal-content">
<form v-on:submit.prevent="editProgram">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span v-text="edit.program.name"></span></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" v-model="edit.program.command">
</div>
<div class="form-group">
<label>Environment</label>
<input type="text" name="environ" class="form-control" v-model="edit.program.environ">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" v-model="edit.program.user">
</div>
<div class="form-group">
<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">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
<!-- /.modal -->
<div class="modal" id="modalTailf">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Tail</h4>
</div>
<div class="modal-body">
<p>Line: {{log.line_count}}</p>
<pre v-html="log.content" class="realtime-log"></pre>
<div class="checkbox text-right">
<label>
<input v-model="log.follow" type="checkbox"> Follow
</label>
</div>
</div>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="formNewProgram" action="/api/programs" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Tail</h4>
</div>
<div class="modal-body">
<p>Line: {{log.line_count}}</p>
<pre v-html="log.content" class="realtime-log"></pre>
<div class="checkbox text-right">
<label>
<input v-model="log.follow" type="checkbox"> Follow
</label>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- polyfill make browser support ECMAScript 6th edition -->
<script src="/res/js/promise-polyfill.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/js/moment.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/common.js"></script>
<script src="/res/js/index.js"></script>
</div>
<!-- polyfill make browser support ECMAScript 6th edition -->
<script src="/res/js/promise-polyfill.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/js/moment.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/common.js"></script>
<script src="/res/js/index.js"></script>
<script type="text/javascript">
$(function() {
$("#launchNewProgram").click(function() {
$("#newProgram").modal({
show: true,
// keyboard: false,
backdrop: 'static',
})
});
// $("#modalTailf").modal({
// // show: true,
// // keyboard: false,
// // backdrop: 'static',
// })
});
</script>
</body>
</html>

@ -1,291 +1,263 @@
/* index.js */
var W = {};
var testPrograms = [{
program: {
name: "gggg",
command: "",
dir: "",
autoStart: true,
},
status: "running",
}];
var vm = new Vue({
el: "#app",
data: {
isConnectionAlive: true,
log: {
content: '',
follow: true,
line_count: 0,
},
programs: [],
slaves: [],
edit: {
program: null,
el: "#app",
data: {
isConnectionAlive: true,
log: {
content: '',
follow: true,
line_count: 0,
},
programs: [],
edit: {
program: null,
}
},
methods: {
addNewProgram: function() {
console.log("Add")
var form = $("#formNewProgram");
form.submit(function(e) {
e.preventDefault();
$("#newProgram").modal('hide')
return false;
});
},
showEditProgram: function(p) {
this.edit.program = Object.assign({}, p); // here require polyfill.min.js
$("#programEdit").modal('show');
},
editProgram: function() {
var p = this.edit.program;
$.ajax({
url: "/api/programs/" + p.name,
method: "PUT",
data: JSON.stringify(p),
})
.then(function(ret) {
console.log(ret);
$("#programEdit").modal('hide');
})
// console.log(JSON.stringify(p));
},
updateBreadcrumb: function() {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
},
refresh: function() {
// ws.send("Hello")
$.ajax({
url: "/api/programs",
success: function(data) {
vm.programs = data;
Vue.nextTick(function() {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
},
methods: {
addNewProgram: function (slave) {
$("#newProgram").modal({
show: true,
backdrop: 'static',
}).data("slave", slave);
},
formNewProgram: function () {
var url = "/api/programs",
data = $("#formNewProgram").serialize(),
name = $("#formNewProgram").find("[name=name]").val(),
disablechars = "./\\";
if (!name) {
alert("\"" + name + "\" is empty ")
return false
}
if (disablechars.indexOf(name[0]) != -1) {
alert("\"" + name + "\" Can't starts with \".\" \"/\" \"\\\"")
return false
}
var slave = $("#newProgram").data("slave");
if (slave !== undefined && slave !== "") {
url = "/distributed/" + slave + url;
}
$.ajax({
type: "POST",
url: url,
data: data,
success: function (data) {
if (data.status === 0) {
$("#newProgram").modal('hide');
} else {
window.alert(data.error);
}
},
error: function (err) {
alert(err.responseText)
}
});
},
showEditProgram: function (p, slave) {
this.edit.program = Object.assign({}, p); // here require polyfill.min.js
$("#programEdit").data("slave",slave).modal('show');
},
editProgram: function () {
var p = this.edit.program;
var requestUrl = "/api/programs/" + p.name
var slave = $("#programEdit").data("slave");
if (slave !== undefined && slave !== "") {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: "PUT",
data: JSON.stringify(p),
}).then(function (ret) {
$("#programEdit").modal('hide');
})
},
updateBreadcrumb: function () {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
reload: function() {
$.ajax({
url: "/api/reload",
method: "POST",
success: function(data) {
if (data.status == 0) {
alert("reload success");
} else {
alert(data.value);
}
}
});
},
test: function() {
console.log("test");
},
cmdStart: function(name) {
console.log(name);
$.ajax({
url: "/api/programs/" + name + "/start",
method: 'post',
success: function(data) {
console.log(data);
}
})
},
cmdStop: function(name) {
$.ajax({
url: "/api/programs/" + name + "/stop",
method: 'post',
success: function(data) {
console.log(data);
}
})
},
cmdTail: function(name) {
var that = this;
if (W.wsLog) {
W.wsLog.close()
}
W.wsLog = newWebsocket("/ws/logs/" + name, {
onopen: function(evt) {
that.log.content = "";
that.log.line_count = 0;
},
refresh: function () {
// ws.send("Hello")
$.ajax({
url: "/api/programs",
success: function (data) {
vm.programs = data;
Vue.nextTick(function () {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
onmessage: function(evt) {
// strip ansi color
// console.log("DT:", evt.data)
that.log.content += evt.data.replace(/\033\[[0-9;]*m/g, "");
that.log.line_count = $.trim(that.log.content).split(/\r\n|\r|\n/).length;
if (that.log.follow) {
var pre = $(".realtime-log")[0];
setTimeout(function() {
pre.scrollTop = pre.scrollHeight - pre.clientHeight;
}, 1);
}
}
});
this.log.follow = true;
$("#modalTailf").modal({
show: true,
keyboard: true,
// keyboard: false,
// backdrop: 'static',
})
},
cmdDelete: function(name) {
if (!confirm("Confirm delete \"" + name + "\"")) {
return
}
$.ajax({
url: "/api/programs/" + name,
method: 'delete',
success: function(data) {
console.log(data);
}
})
},
canStop: function(status) {
switch (status) {
case "running":
case "retry wait":
return true;
}
},
}
})
$.ajax({
url: "/distributed/api/programs",
success: function (data) {
vm.slaves = data;
Vue.nextTick(function () {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
},
/*reload: function () {
$.ajax({
url: "/api/reload",
method: "POST",
success: function (data) {
if (data.status == 0) {
alert("reload success");
} else {
alert(data.value);
}
}
});
},
test: function () {
console.log("test");
},*/
cmdStart: function (name, slave) {
console.log(name, slave);
requestUrl = "/api/programs/" + name + "/start";
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: 'post',
success: function (data) {
console.log(data);
}
});
},
cmdStop: function (name, slave) {
requestUrl = "/api/programs/" + name + "/stop";
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: 'post',
success: function (data) {
console.log(data);
}
})
},
cmdTail: function (name, slave) {
requestUrl = "/ws/logs/" + name;
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
var that = this;
if (W.wsLog) {
W.wsLog.close()
}
W.wsLog = newWebsocket(requestUrl, {
onopen: function (evt) {
that.log.content = "";
that.log.line_count = 0;
},
onmessage: function (evt) {
that.log.content += evt.data.replace(/\033\[[0-9;]*m/g, "");
that.log.line_count = $.trim(that.log.content).split(/\r\n|\r|\n/).length;
if (that.log.follow) {
var pre = $(".realtime-log")[0];
setTimeout(function () {
pre.scrollTop = pre.scrollHeight - pre.clientHeight;
}, 1);
}
}
});
this.log.follow = true;
$("#modalTailf").modal({
show: true,
keyboard: true,
}).on("hide.bs.modal", function (e) {
W.wsLog.close();
})
},
cmdDelete: function (name, slave) {
if (!confirm("Confirm delete \"" + name + "\"")) {
return
}
requestUrl = "/api/programs/" + name;
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl
}
$.ajax({
url: requestUrl,
method: 'delete',
success: function (data) {
console.log(data);
}
})
},
canStop: function (status) {
switch (status) {
case "running":
case "retry wait":
return true;
}
},
}
Vue.filter('fromNow', function(value) {
return moment(value).fromNow();
})
Vue.filter('fromNow', function (value) {
return moment(value).fromNow();
Vue.filter('formatBytes', function(value) {
var bytes = parseFloat(value);
if (bytes < 0) return "-";
else if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
else return (bytes / 1073741824).toFixed(1) + " GB";
})
Vue.filter('formatBytes', function (value) {
var bytes = parseFloat(value);
if (bytes < 0) return "-";
else if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
else return (bytes / 1073741824).toFixed(1) + " GB";
Vue.filter('colorStatus', function(value) {
var makeColorText = function(text, color) {
return "<span class='status' style='background-color:" + color + "'>" + text + "</span>";
}
switch (value) {
case "stopping":
return makeColorText(value, "#996633");
case "running":
return makeColorText(value, "green");
case "fatal":
return makeColorText(value, "red");
default:
return makeColorText(value, "gray");
}
})
Vue.filter('colorStatus', function (value) {
var makeColorText = function (text, color) {
return "<span class='status' style='background-color:" + color + "'>" + text + "</span>";
};
switch (value) {
case "stopping":
return makeColorText(value, "#996633");
case "running":
return makeColorText(value, "green");
case "fatal":
return makeColorText(value, "red");
default:
return makeColorText(value, "gray");
}
});
Vue.directive('disable', function(value) {
this.el.disabled = !!value
})
Vue.directive('disable', function (value) {
this.el.disabled = !!value
});
$(function() {
vm.refresh();
$(function () {
vm.refresh();
$("#formNewProgram").submit(function(e) {
var url = "/api/programs",
data = $(this).serialize();
$.ajax({
type: "POST",
url: url,
data: data,
success: function(data) {
if (data.status === 0) {
$("#newProgram").modal('hide');
} else {
window.alert(data.error);
}
},
error: function(err) {
console.log(err.responseText);
}
})
e.preventDefault()
});
function newEventWatcher() {
W.events = newWebsocket("/ws/events", {
onopen: function (evt) {
vm.isConnectionAlive = true;
},
onmessage: function (evt) {
console.log("response:" + evt.data);
vm.refresh();
},
onclose: function (evt) {
W.events = null;
vm.isConnectionAlive = false;
console.log("Reconnect after 3s");
setTimeout(newEventWatcher, 3000)
}
});
};
function newEventWatcher() {
W.events = newWebsocket("/ws/events", {
onopen: function(evt) {
vm.isConnectionAlive = true;
},
onmessage: function(evt) {
console.log("response:" + evt.data);
vm.refresh();
},
onclose: function(evt) {
W.events = null;
vm.isConnectionAlive = false;
console.log("Reconnect after 3s")
setTimeout(newEventWatcher, 3000)
}
});
};
newEventWatcher();
newEventWatcher();
// cancel follow log if people want to see the original data
$(".realtime-log").bind('mousewheel', function (evt) {
if (evt.originalEvent.wheelDelta >= 0) {
vm.log.follow = false;
}
});
$('#modalTailf').on('hidden.bs.modal', function () {
// do something…
console.log("Hiddeen");
if (W.wsLog) {
console.log("wsLog closed");
W.wsLog.close()
}
})
// cancel follow log if people want to see the original data
$(".realtime-log").bind('mousewheel', function(evt) {
if (evt.originalEvent.wheelDelta >= 0) {
vm.log.follow = false;
}
})
$('#modalTailf').on('hidden.bs.modal', function() {
// do something…
console.log("Hiddeen")
if (W.wsLog) {
console.log("wsLog closed")
W.wsLog.close()
}
})
});

@ -1,52 +1,48 @@
/* javascript */
var vm = new Vue({
el: '#app',
data: {
name: name,
pid: '-',
childPids: [],
}
el: '#app',
data: {
name: name,
pid: '-',
childPids: [],
}
});
var maxDataCount = 30;
var requstUrl = "/ws/perfs/" + name;
if ("" !== slave) {
requstUrl = "/distributed/" + slave + requstUrl;
}
var ws = newWebsocket(requstUrl, {
onopen: function(evt) {
console.log(evt);
},
onmessage: function(evt) {
var data = JSON.parse(evt.data);
vm.pid = data.pid;
vm.childPids = data.pids;
console.log("pid", data.pid, data); //evt.data.pid);
if (memData && data.rss) {
memData.push({
value: [new Date(), data.rss],
})
if (memData.length > maxDataCount) {
memData.shift();
}
chartMem.setOption({
series: [{
data: memData,
}]
});
}
if (cpuData && data.pcpu !== undefined) {
cpuData.push({
value: [new Date(), data.pcpu],
})
if (cpuData.length > maxDataCount) {
cpuData.shift();
}
chartCpu.setOption({
series: [{
data: cpuData,
}]
})
}
var ws = newWebsocket('/ws/perfs/' + name, {
onopen: function(evt) {
console.log(evt);
},
onmessage: function(evt) {
var data = JSON.parse(evt.data);
vm.pid = data.pid;
vm.childPids = data.pids;
console.log("pid", data.pid, data); //evt.data.pid);
if (memData && data.rss) {
memData.push({
value: [new Date(), data.rss],
})
if (memData.length > maxDataCount) {
memData.shift();
}
chartMem.setOption({
series: [{
data: memData,
}]
});
}
if (cpuData && data.pcpu !== undefined) {
cpuData.push({
value: [new Date(), data.pcpu],
})
if (cpuData.length > maxDataCount) {
cpuData.shift();
}
chartCpu.setOption({
series: [{
data: cpuData,
}]
})
}
}
})

@ -2,191 +2,193 @@
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>gosuv</title>
<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/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>后台服务管理</title>
<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/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
</head>
<body id="app">
<nav class="navbar navbar-inverse">
<nav class="navbar navbar-inverse">
<div class="container">
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Go Supervisor</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">后台服务管理</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</nav>
<div class="container">
</nav>
<div class="container">
<div class="col-md-12">
<h1>{{name}}</h1>
<p>
pid: {{pid}}
<br> sub pids: {{childPids.join(',')}}
</p>
<p>
<br> status: running
<br> time: 2016/09/07 12:00:00
</p>
<h1>{{name}}</h1>
<p>
pid: {{pid}}
<br> sub pids: {{childPids.join(',')}}
</p>
<p>
<br> status: running
<br> time: 2016/09/07 12:00:00
</p>
</div>
<div class="col-md-12">
<div id="chart-cpu" style="width: 100%;height:250px;"></div>
<div id="chart-mem" style="width: 100%;height:250px;"></div>
<div id="chart-cpu" style="width: 100%;height:250px;"></div>
<div id="chart-mem" style="width: 100%;height:250px;"></div>
</div>
</div>
<script type="text/javascript">
var name = "[[.Name]]";
var slave = "[[.Slave]]"
</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/js/vue-1.0.min.js"></script>
<script src="/res/js/echarts.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/settings.js"></script>
<script type="text/javascript">
// 基于准备好的dom初始化echarts实例
var chartCpu = echarts.init(document.getElementById('chart-cpu'));
var chartMem = echarts.init(document.getElementById('chart-mem'));
</div>
<script type="text/javascript">
var name = "[[.Name]]";
</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/js/moment.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/echarts.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/settings.js"></script>
<script type="text/javascript">
// 基于准备好的dom初始化echarts实例
var chartCpu = echarts.init(document.getElementById('chart-cpu'));
var chartMem = echarts.init(document.getElementById('chart-mem'));
// 指定图表的配置项和数据
// 指定图表的配置项和数据
var cpuData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
cpuData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var cpuData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
cpuData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'CPU'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
// formatter: function(params) {
// params = params[0];
// console.log(params)
// var date = new Date(params.value[0]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
// },
axisPointer: {
animation: false
}
},
legend: {
data: ['Total']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: {
formatter: '{value} %'
},
},
series: [{
name: 'Total',
type: 'line',
data: cpuData,
animation: false,
smooth: true,
areaStyle: {
normal: {}
},
}]
}
var option = {
title: {
text: 'CPU'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
// formatter: function(params) {
// params = params[0];
// console.log(params)
// var date = new Date(params.value[0]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
// },
axisPointer: {
animation: false
}
},
legend: {
data: ['Total']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: {
formatter: '{value} %'
},
},
series: [{
name: 'Total',
type: 'line',
data: cpuData,
animation: false,
smooth: true,
areaStyle: {
normal: {}
},
}]
}
chartCpu.setOption(option);
chartCpu.setOption(option);
var memData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
memData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'Memory'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
formatter: function (params) {
params = params[0];
// console.log(params)
var date = new Date(params.value[0]);
return formatBytes(params.value[1]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
},
axisPointer: {
animation: false
}
},
legend: {
data: ['RSS']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value, index) {
return formatBytes(value);
}
},
},
series: [{
name: 'RSS',
type: 'line',
data: memData,
animation: false,
smooth: true,
areaStyle: {
normal: {
color: "gray",
}
},
}]
}
chartMem.setOption(option);
</script>
var memData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
memData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'Memory'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
params = params[0];
// console.log(params)
var date = new Date(params.value[0]);
return formatBytes(params.value[1]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
},
axisPointer: {
animation: false
}
},
legend: {
data: ['RSS']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value, index) {
return formatBytes(value);
}
},
},
series: [{
name: 'RSS',
type: 'line',
data: memData,
animation: false,
smooth: true,
areaStyle: {
normal: {
color: "gray",
}
},
}]
}
chartMem.setOption(option);
</script>
</body>
</header>
</html>

BIN
rice

Binary file not shown.

File diff suppressed because one or more lines are too long

@ -5,7 +5,6 @@ import (
"fmt"
"os"
"runtime"
"strings"
"time"
"github.com/qiniu/log"
@ -71,12 +70,11 @@ func UserHomeDir() string {
// confirmations. If the input is not recognized, it will ask again. The function does not return
// until it gets a valid response from the user. Typically, you should use fmt to print out a question
// before calling askForConfirmation. E.g. fmt.Println("WARNING: Are you sure? (yes/no)")
func askForConfirmation(prompt string, _default bool) bool {
func askForConfirmation() bool {
var response string
fmt.Print(prompt)
_, err := fmt.Scanln(&response)
if err != nil {
return _default
log.Fatal(err)
}
okayResponses := []string{"y", "Y", "yes", "Yes", "YES"}
nokayResponses := []string{"n", "N", "no", "No", "NO"}
@ -85,7 +83,8 @@ func askForConfirmation(prompt string, _default bool) bool {
} else if containsString(nokayResponses, response) {
return false
} else {
return askForConfirmation(prompt, _default)
fmt.Println("Please type yes or no and then press enter:")
return askForConfirmation()
}
}
@ -106,10 +105,3 @@ func posString(slice []string, element string) int {
func containsString(slice []string, element string) bool {
return !(posString(slice, element) == -1)
}
func StringFormat(format string, m map[string]interface{}) string {
for k, v := range m {
format = strings.Replace(format, "{"+k+"}", fmt.Sprintf("%v", v), -1)
}
return format
}

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

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

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

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

@ -0,0 +1,6 @@
import flask
app = flask.Flask(__name__)
if __name__ == '__main__':
app.run(port=46732, debug=True)

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

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

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

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

@ -0,0 +1 @@
web: python flask_main.py

@ -0,0 +1,11 @@
import flask
app = flask.Flask(__name__)
@app.route('/')
def homepage():
return 'Home'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

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

@ -0,0 +1,5 @@
language: go
go:
- 1.4
- 1.5
script: go test -v github.com/equinox-io/equinox github.com/equinox-io/equinox/proto

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

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

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

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

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

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

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

@ -0,0 +1,7 @@
// +build !windows
package update
func hideFile(path string) error {
return nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,42 @@
/*
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"`
}

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

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

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

@ -0,0 +1,8 @@
language: go
go:
- 1.5.3
- tip
notifications:
email:
- ionathan@gmail.com
- marcosnils@gmail.com

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

@ -0,0 +1,3 @@
test:
go get -v -d -t ./...
go test -v

@ -0,0 +1,444 @@
[![Build Status](https://img.shields.io/travis/franela/goreq/master.svg)](https://travis-ci.org/franela/goreq)
[![GoDoc](https://godoc.org/github.com/franela/goreq?status.svg)](https://godoc.org/github.com/franela/goreq)
GoReq
=======
Simple and sane HTTP request library for Go language.
**Table of Contents**
- [Why GoReq?](#user-content-why-goreq)
- [How do I install it?](#user-content-how-do-i-install-it)
- [What can I do with it?](#user-content-what-can-i-do-with-it)
- [Making requests with different methods](#user-content-making-requests-with-different-methods)
- [GET](#user-content-get)
- [Tags](#user-content-tags)
- [POST](#user-content-post)
- [Sending payloads in the Body](#user-content-sending-payloads-in-the-body)
- [Specifiying request headers](#user-content-specifiying-request-headers)
- [Sending Cookies](#cookie-support)
- [Setting timeouts](#user-content-setting-timeouts)
- [Using the Response and Error](#user-content-using-the-response-and-error)
- [Receiving JSON](#user-content-receiving-json)
- [Sending/Receiving Compressed Payloads](#user-content-sendingreceiving-compressed-payloads)
- [Using gzip compression:](#user-content-using-gzip-compression)
- [Using deflate compression:](#user-content-using-deflate-compression)
- [Using compressed responses:](#user-content-using-compressed-responses)
- [Proxy](#proxy)
- [Debugging requests](#debug)
- [Getting raw Request & Response](#getting-raw-request--response)
- [TODO:](#user-content-todo)
Why GoReq?
==========
Go has very nice native libraries that allows you to do lots of cool things. But sometimes those libraries are too low level, which means that to do a simple thing, like an HTTP Request, it takes some time. And if you want to do something as simple as adding a timeout to a request, you will end up writing several lines of code.
This is why we think GoReq is useful. Because you can do all your HTTP requests in a very simple and comprehensive way, while enabling you to do more advanced stuff by giving you access to the native API.
How do I install it?
====================
```bash
go get github.com/franela/goreq
```
What can I do with it?
======================
## Making requests with different methods
#### GET
```go
res, err := goreq.Request{ Uri: "http://www.google.com" }.Do()
```
GoReq default method is GET.
You can also set value to GET method easily
```go
type Item struct {
Limit int
Skip int
Fields string
}
item := Item {
Limit: 3,
Skip: 5,
Fields: "Value",
}
res, err := goreq.Request{
Uri: "http://localhost:3000/",
QueryString: item,
}.Do()
```
The sample above will send `http://localhost:3000/?limit=3&skip=5&fields=Value`
Alternatively the `url` tag can be used in struct fields to customize encoding properties
```go
type Item struct {
TheLimit int `url:"the_limit"`
TheSkip string `url:"the_skip,omitempty"`
TheFields string `url:"-"`
}
item := Item {
TheLimit: 3,
TheSkip: "",
TheFields: "Value",
}
res, err := goreq.Request{
Uri: "http://localhost:3000/",
QueryString: item,
}.Do()
```
The sample above will send `http://localhost:3000/?the_limit=3`
QueryString also support url.Values
```go
item := url.Values{}
item.Set("Limit", 3)
item.Add("Field", "somefield")
item.Add("Field", "someotherfield")
res, err := goreq.Request{
Uri: "http://localhost:3000/",
QueryString: item,
}.Do()
```
The sample above will send `http://localhost:3000/?limit=3&field=somefield&field=someotherfield`
### Tags
Struct field `url` tag is mainly used as the request parameter name.
Tags can be comma separated multiple values, 1st value is for naming and rest has special meanings.
- special tag for 1st value
- `-`: value is ignored if set this
- special tag for rest 2nd value
- `omitempty`: zero-value is ignored if set this
- `squash`: the fields of embedded struct is used for parameter
#### Tag Examples
```go
type Place struct {
Country string `url:"country"`
City string `url:"city"`
ZipCode string `url:"zipcode,omitempty"`
}
type Person struct {
Place `url:",squash"`
FirstName string `url:"first_name"`
LastName string `url:"last_name"`
Age string `url:"age,omitempty"`
Password string `url:"-"`
}
johnbull := Person{
Place: Place{ // squash the embedded struct value
Country: "UK",
City: "London",
ZipCode: "SW1",
},
FirstName: "John",
LastName: "Doe",
Age: "35",
Password: "my-secret", // ignored for parameter
}
goreq.Request{
Uri: "http://localhost/",
QueryString: johnbull,
}.Do()
// => `http://localhost/?first_name=John&last_name=Doe&age=35&country=UK&city=London&zip_code=SW1`
// age and zipcode will be ignored because of `omitempty`
// but firstname isn't.
samurai := Person{
Place: Place{ // squash the embedded struct value
Country: "Japan",
City: "Tokyo",
},
LastName: "Yagyu",
}
goreq.Request{
Uri: "http://localhost/",
QueryString: samurai,
}.Do()
// => `http://localhost/?first_name=&last_name=yagyu&country=Japan&city=Tokyo`
```
#### POST
```go
res, err := goreq.Request{ Method: "POST", Uri: "http://www.google.com" }.Do()
```
## Sending payloads in the Body
You can send ```string```, ```Reader``` or ```interface{}``` in the body. The first two will be sent as text. The last one will be marshalled to JSON, if possible.
```go
type Item struct {
Id int
Name string
}
item := Item{ Id: 1111, Name: "foobar" }
res, err := goreq.Request{
Method: "POST",
Uri: "http://www.google.com",
Body: item,
}.Do()
```
## Specifiying request headers
We think that most of the times the request headers that you use are: ```Host```, ```Content-Type```, ```Accept``` and ```User-Agent```. This is why we decided to make it very easy to set these headers.
```go
res, err := goreq.Request{
Uri: "http://www.google.com",
Host: "foobar.com",
Accept: "application/json",
ContentType: "application/json",
UserAgent: "goreq",
}.Do()
```
But sometimes you need to set other headers. You can still do it.
```go
req := goreq.Request{ Uri: "http://www.google.com" }
req.AddHeader("X-Custom", "somevalue")
req.Do()
```
Alternatively you can use the `WithHeader` function to keep the syntax short
```go
res, err = goreq.Request{ Uri: "http://www.google.com" }.WithHeader("X-Custom", "somevalue").Do()
```
## Cookie support
Cookies can be either set at the request level by sending a [CookieJar](http://golang.org/pkg/net/http/cookiejar/) in the `CookieJar` request field
or you can use goreq's one-liner WithCookie method as shown below
```go
res, err := goreq.Request{
Uri: "http://www.google.com",
}.
WithCookie(&http.Cookie{Name: "c1", Value: "v1"}).
Do()
```
## Setting timeouts
GoReq supports 2 kind of timeouts. A general connection timeout and a request specific one. By default the connection timeout is of 1 second. There is no default for request timeout, which means it will wait forever.
You can change the connection timeout doing:
```go
goreq.SetConnectTimeout(100 * time.Millisecond)
```
And specify the request timeout doing:
```go
res, err := goreq.Request{
Uri: "http://www.google.com",
Timeout: 500 * time.Millisecond,
}.Do()
```
## Using the Response and Error
GoReq will always return 2 values: a ```Response``` and an ```Error```.
If ```Error``` is not ```nil``` it means that an error happened while doing the request and you shouldn't use the ```Response``` in any way.
You can check what happened by getting the error message:
```go
fmt.Println(err.Error())
```
And to make it easy to know if it was a timeout error, you can ask the error or return it:
```go
if serr, ok := err.(*goreq.Error); ok {
if serr.Timeout() {
...
}
}
return err
```
If you don't get an error, you can safely use the ```Response```.
```go
res.Uri // return final URL location of the response (fulfilled after redirect was made)
res.StatusCode // return the status code of the response
res.Body // gives you access to the body
res.Body.ToString() // will return the body as a string
res.Header.Get("Content-Type") // gives you access to all the response headers
```
Remember that you should **always** close `res.Body` if it's not `nil`
## Receiving JSON
GoReq will help you to receive and unmarshal JSON.
```go
type Item struct {
Id int
Name string
}
var item Item
res.Body.FromJsonTo(&item)
```
## Sending/Receiving Compressed Payloads
GoReq supports gzip, deflate and zlib compression of requests' body and transparent decompression of responses provided they have a correct `Content-Encoding` header.
##### Using gzip compression:
```go
res, err := goreq.Request{
Method: "POST",
Uri: "http://www.google.com",
Body: item,
Compression: goreq.Gzip(),
}.Do()
```
##### Using deflate/zlib compression:
```go
res, err := goreq.Request{
Method: "POST",
Uri: "http://www.google.com",
Body: item,
Compression: goreq.Deflate(),
}.Do()
```
##### Using compressed responses:
If servers replies a correct and matching `Content-Encoding` header (gzip requires `Content-Encoding: gzip` and deflate `Content-Encoding: deflate`) goreq transparently decompresses the response so the previous example should always work:
```go
type Item struct {
Id int
Name string
}
res, err := goreq.Request{
Method: "POST",
Uri: "http://www.google.com",
Body: item,
Compression: goreq.Gzip(),
}.Do()
var item Item
res.Body.FromJsonTo(&item)
```
If no `Content-Encoding` header is replied by the server GoReq will return the crude response.
## Proxy
If you need to use a proxy for your requests GoReq supports the standard `http_proxy` env variable as well as manually setting the proxy for each request
```go
res, err := goreq.Request{
Method: "GET",
Proxy: "http://myproxy:myproxyport",
Uri: "http://www.google.com",
}.Do()
```
### Proxy basic auth is also supported
```go
res, err := goreq.Request{
Method: "GET",
Proxy: "http://user:pass@myproxy:myproxyport",
Uri: "http://www.google.com",
}.Do()
```
## Debug
If you need to debug your http requests, it can print the http request detail.
```go
res, err := goreq.Request{
Method: "GET",
Uri: "http://www.google.com",
Compression: goreq.Gzip(),
ShowDebug: true,
}.Do()
fmt.Println(res, err)
```
and it will print the log:
```
GET / HTTP/1.1
Host: www.google.com
Accept:
Accept-Encoding: gzip
Content-Encoding: gzip
Content-Type:
```
### Getting raw Request & Response
To get the Request:
```go
req := goreq.Request{
Host: "foobar.com",
}
//req.Request will return a new instance of an http.Request so you can safely use it for something else
request, _ := req.NewRequest()
```
To get the Response:
```go
res, err := goreq.Request{
Method: "GET",
Uri: "http://www.google.com",
Compression: goreq.Gzip(),
ShowDebug: true,
}.Do()
// res.Response will contain the original http.Response structure
fmt.Println(res.Response, err)
```
TODO:
-----
We do have a couple of [issues](https://github.com/franela/goreq/issues) pending we'll be addressing soon. But feel free to
contribute and send us PRs (with tests please :smile:).

@ -0,0 +1,491 @@
package goreq
import (
"bufio"
"bytes"
"compress/gzip"
"compress/zlib"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"reflect"
"strings"
"time"
)
type itimeout interface {
Timeout() bool
}
type Request struct {
headers []headerTuple
cookies []*http.Cookie
Method string
Uri string
Body interface{}
QueryString interface{}
Timeout time.Duration
ContentType string
Accept string
Host string
UserAgent string
Insecure bool
MaxRedirects int
RedirectHeaders bool
Proxy string
Compression *compression
BasicAuthUsername string
BasicAuthPassword string
CookieJar http.CookieJar
ShowDebug bool
OnBeforeRequest func(goreq *Request, httpreq *http.Request)
}
type compression struct {
writer func(buffer io.Writer) (io.WriteCloser, error)
reader func(buffer io.Reader) (io.ReadCloser, error)
ContentEncoding string
}
type Response struct {
*http.Response
Uri string
Body *Body
req *http.Request
}
func (r Response) CancelRequest() {
cancelRequest(DefaultTransport, r.req)
}
func cancelRequest(transport interface{}, r *http.Request) {
if tp, ok := transport.(transportRequestCanceler); ok {
tp.CancelRequest(r)
}
}
type headerTuple struct {
name string
value string
}
type Body struct {
reader io.ReadCloser
compressedReader io.ReadCloser
}
type Error struct {
timeout bool
Err error
}
type transportRequestCanceler interface {
CancelRequest(*http.Request)
}
func (e *Error) Timeout() bool {
return e.timeout
}
func (e *Error) Error() string {
return e.Err.Error()
}
func (b *Body) Read(p []byte) (int, error) {
if b.compressedReader != nil {
return b.compressedReader.Read(p)
}
return b.reader.Read(p)
}
func (b *Body) Close() error {
err := b.reader.Close()
if b.compressedReader != nil {
return b.compressedReader.Close()
}
return err
}
func (b *Body) FromJsonTo(o interface{}) error {
return json.NewDecoder(b).Decode(o)
}
func (b *Body) ToString() (string, error) {
body, err := ioutil.ReadAll(b)
if err != nil {
return "", err
}
return string(body), nil
}
func Gzip() *compression {
reader := func(buffer io.Reader) (io.ReadCloser, error) {
return gzip.NewReader(buffer)
}
writer := func(buffer io.Writer) (io.WriteCloser, error) {
return gzip.NewWriter(buffer), nil
}
return &compression{writer: writer, reader: reader, ContentEncoding: "gzip"}
}
func Deflate() *compression {
reader := func(buffer io.Reader) (io.ReadCloser, error) {
return zlib.NewReader(buffer)
}
writer := func(buffer io.Writer) (io.WriteCloser, error) {
return zlib.NewWriter(buffer), nil
}
return &compression{writer: writer, reader: reader, ContentEncoding: "deflate"}
}
func Zlib() *compression {
return Deflate()
}
func paramParse(query interface{}) (string, error) {
switch query.(type) {
case url.Values:
return query.(url.Values).Encode(), nil
case *url.Values:
return query.(*url.Values).Encode(), nil
default:
var v = &url.Values{}
err := paramParseStruct(v, query)
return v.Encode(), err
}
}
func paramParseStruct(v *url.Values, query interface{}) error {
var (
s = reflect.ValueOf(query)
t = reflect.TypeOf(query)
)
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface {
s = s.Elem()
t = s.Type()
}
if t.Kind() != reflect.Struct {
return errors.New("Can not parse QueryString.")
}
for i := 0; i < t.NumField(); i++ {
var name string
field := s.Field(i)
typeField := t.Field(i)
if !field.CanInterface() {
continue
}
urlTag := typeField.Tag.Get("url")
if urlTag == "-" {
continue
}
name, opts := parseTag(urlTag)
var omitEmpty, squash bool
omitEmpty = opts.Contains("omitempty")
squash = opts.Contains("squash")
if squash {
err := paramParseStruct(v, field.Interface())
if err != nil {
return err
}
continue
}
if urlTag == "" {
name = strings.ToLower(typeField.Name)
}
if val := fmt.Sprintf("%v", field.Interface()); !(omitEmpty && len(val) == 0) {
v.Add(name, val)
}
}
return nil
}
func prepareRequestBody(b interface{}) (io.Reader, error) {
switch b.(type) {
case string:
// treat is as text
return strings.NewReader(b.(string)), nil
case io.Reader:
// treat is as text
return b.(io.Reader), nil
case []byte:
//treat as byte array
return bytes.NewReader(b.([]byte)), nil
case nil:
return nil, nil
default:
// try to jsonify it
j, err := json.Marshal(b)
if err == nil {
return bytes.NewReader(j), nil
}
return nil, err
}
}
var DefaultDialer = &net.Dialer{Timeout: 1000 * time.Millisecond}
var DefaultTransport http.RoundTripper = &http.Transport{Dial: DefaultDialer.Dial, Proxy: http.ProxyFromEnvironment}
var DefaultClient = &http.Client{Transport: DefaultTransport}
var proxyTransport http.RoundTripper
var proxyClient *http.Client
func SetConnectTimeout(duration time.Duration) {
DefaultDialer.Timeout = duration
}
func (r *Request) AddHeader(name string, value string) {
if r.headers == nil {
r.headers = []headerTuple{}
}
r.headers = append(r.headers, headerTuple{name: name, value: value})
}
func (r Request) WithHeader(name string, value string) Request {
r.AddHeader(name, value)
return r
}
func (r *Request) AddCookie(c *http.Cookie) {
r.cookies = append(r.cookies, c)
}
func (r Request) WithCookie(c *http.Cookie) Request {
r.AddCookie(c)
return r
}
func (r Request) Do() (*Response, error) {
var client = DefaultClient
var transport = DefaultTransport
var resUri string
var redirectFailed bool
r.Method = valueOrDefault(r.Method, "GET")
// use a client with a cookie jar if necessary. We create a new client not
// to modify the default one.
if r.CookieJar != nil {
client = &http.Client{
Transport: transport,
Jar: r.CookieJar,
}
}
if r.Proxy != "" {
proxyUrl, err := url.Parse(r.Proxy)
if err != nil {
// proxy address is in a wrong format
return nil, &Error{Err: err}
}
//If jar is specified new client needs to be built
if proxyTransport == nil || client.Jar != nil {
proxyTransport = &http.Transport{Dial: DefaultDialer.Dial, Proxy: http.ProxyURL(proxyUrl)}
proxyClient = &http.Client{Transport: proxyTransport, Jar: client.Jar}
} else if proxyTransport, ok := proxyTransport.(*http.Transport); ok {
proxyTransport.Proxy = http.ProxyURL(proxyUrl)
}
transport = proxyTransport
client = proxyClient
}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) > r.MaxRedirects {
redirectFailed = true
return errors.New("Error redirecting. MaxRedirects reached")
}
resUri = req.URL.String()
//By default Golang will not redirect request headers
// https://code.google.com/p/go/issues/detail?id=4800&q=request%20header
if r.RedirectHeaders {
for key, val := range via[0].Header {
req.Header[key] = val
}
}
return nil
}
if transport, ok := transport.(*http.Transport); ok {
if r.Insecure {
if transport.TLSClientConfig != nil {
transport.TLSClientConfig.InsecureSkipVerify = true
} else {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
} else if transport.TLSClientConfig != nil {
// the default TLS client (when transport.TLSClientConfig==nil) is
// already set to verify, so do nothing in that case
transport.TLSClientConfig.InsecureSkipVerify = false
}
}
req, err := r.NewRequest()
if err != nil {
// we couldn't parse the URL.
return nil, &Error{Err: err}
}
timeout := false
if r.Timeout > 0 {
client.Timeout = r.Timeout
}
if r.ShowDebug {
dump, err := httputil.DumpRequest(req, true)
if err != nil {
log.Println(err)
}
log.Println(string(dump))
}
if r.OnBeforeRequest != nil {
r.OnBeforeRequest(&r, req)
}
res, err := client.Do(req)
if err != nil {
if !timeout {
if t, ok := err.(itimeout); ok {
timeout = t.Timeout()
}
if ue, ok := err.(*url.Error); ok {
if t, ok := ue.Err.(itimeout); ok {
timeout = t.Timeout()
}
}
}
var response *Response
//If redirect fails we still want to return response data
if redirectFailed {
if res != nil {
response = &Response{res, resUri, &Body{reader: res.Body}, req}
} else {
response = &Response{res, resUri, nil, req}
}
}
//If redirect fails and we haven't set a redirect count we shouldn't return an error
if redirectFailed && r.MaxRedirects == 0 {
return response, nil
}
return response, &Error{timeout: timeout, Err: err}
}
if r.Compression != nil && strings.Contains(res.Header.Get("Content-Encoding"), r.Compression.ContentEncoding) {
compressedReader, err := r.Compression.reader(res.Body)
if err != nil {
return nil, &Error{Err: err}
}
return &Response{res, resUri, &Body{reader: res.Body, compressedReader: compressedReader}, req}, nil
}
return &Response{res, resUri, &Body{reader: res.Body}, req}, nil
}
func (r Request) addHeaders(headersMap http.Header) {
if len(r.UserAgent) > 0 {
headersMap.Add("User-Agent", r.UserAgent)
}
if r.Accept != "" {
headersMap.Add("Accept", r.Accept)
}
if r.ContentType != "" {
headersMap.Add("Content-Type", r.ContentType)
}
}
func (r Request) NewRequest() (*http.Request, error) {
b, e := prepareRequestBody(r.Body)
if e != nil {
// there was a problem marshaling the body
return nil, &Error{Err: e}
}
if r.QueryString != nil {
param, e := paramParse(r.QueryString)
if e != nil {
return nil, &Error{Err: e}
}
r.Uri = r.Uri + "?" + param
}
var bodyReader io.Reader
if b != nil && r.Compression != nil {
buffer := bytes.NewBuffer([]byte{})
readBuffer := bufio.NewReader(b)
writer, err := r.Compression.writer(buffer)
if err != nil {
return nil, &Error{Err: err}
}
_, e = readBuffer.WriteTo(writer)
writer.Close()
if e != nil {
return nil, &Error{Err: e}
}
bodyReader = buffer
} else {
bodyReader = b
}
req, err := http.NewRequest(r.Method, r.Uri, bodyReader)
if err != nil {
return nil, err
}
// add headers to the request
req.Host = r.Host
r.addHeaders(req.Header)
if r.Compression != nil {
req.Header.Add("Content-Encoding", r.Compression.ContentEncoding)
req.Header.Add("Accept-Encoding", r.Compression.ContentEncoding)
}
if r.headers != nil {
for _, header := range r.headers {
req.Header.Add(header.name, header.value)
}
}
//use basic auth if required
if r.BasicAuthUsername != "" {
req.SetBasicAuth(r.BasicAuthUsername, r.BasicAuthPassword)
}
for _, c := range r.cookies {
req.AddCookie(c)
}
return req, nil
}
// Return value if nonempty, def otherwise.
func valueOrDefault(value, def string) string {
if value != "" {
return value
}
return def
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,64 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found here: https://github.com/golang/go/blob/master/LICENSE
package goreq
import (
"strings"
"unicode"
)
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}
func isValidTag(s string) bool {
if s == "" {
return false
}
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
default:
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
return false
}
}
}
return true
}

@ -21,14 +21,4 @@ _testmain.go
*.exe
*.test
*.prof
dist/
output/
gosuv
bindata_assetfs.go
assets_vfsdata.go
.vscode/
vendor/*
*.un~
*~

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014, 2015 Jason E. Aten, Ph.D.
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.

@ -0,0 +1,34 @@
rbuf: a circular ring buffer in Golang
====
type FixedSizeRingBuf struct:
* is a fixed-size circular ring buffer. Yes, just what is says.
This structure is only for bytes, as it was written to
optimize I/O, but could be easily adapted to any other type.
* We keep a pair of ping/pong buffers so that we can linearize
the circular buffer into a contiguous slice if need be.
For efficiency, a FixedSizeRingBuf may be vastly preferred to
a bytes.Buffer. The ReadWithoutAdvance(), Advance(), and Adopt()
methods are all non-standard methods written for speed.
For an I/O heavy application, I have replaced bytes.Buffer with
FixedSizeRingBuf and seen memory consumption go from 8GB to 25MB.
Yes, that is a 300x reduction in memory footprint. Everything ran
faster too.
Note that Bytes(), while inescapable at times, is expensive: avoid
it if possible. If all you need is len(Bytes()), then it is better
to use the FixedSizeRingBuf.Readable member directly.
Bytes() is expensive because it may copy the back and then
the front of a wrapped buffer A[Use] into A[1-Use] in order to
get a contiguous, unwrapped, slice. If possible use ContigLen()
first to get the size that can be read without copying, Read() that
amount, and then Read() a second time -- to avoid the copy.
copyright (c) 2014, Jason E. Aten
license: MIT

@ -0,0 +1,481 @@
package rbuf
// AtomicFixedSizeRingBuf: Synchronized version of FixedSizeRingBuf,
// safe for concurrent access.
//
// copyright (c) 2014, Jason E. Aten
// license: MIT
//
// Some text from the Golang standard library doc is adapted and
// reproduced in fragments below to document the expected behaviors
// of the interface functions Read()/Write()/ReadFrom()/WriteTo() that
// are implemented here. Those descriptions (see
// http://golang.org/pkg/io/#Reader for example) are
// copyright 2010 The Go Authors.
import (
"fmt"
"io"
"sync"
)
// AtomicFixedSizeRingBuf: see FixedSizeRingBuf for the full
// details; this is the same, just safe for current access
// (and thus paying the price of synchronization on each call
// as well.)
//
type AtomicFixedSizeRingBuf struct {
A [2][]byte // a pair of ping/pong buffers. Only one is active.
Use int // which A buffer is in active use, 0 or 1
N int // MaxViewInBytes, the size of A[0] and A[1] in bytes.
Beg int // start of data in A[Use]
readable int // number of bytes available to read in A[Use]
tex sync.Mutex
}
// Readable() returns the number of bytes available for reading.
func (b *AtomicFixedSizeRingBuf) Readable() int {
b.tex.Lock()
defer b.tex.Unlock()
return b.readable
}
// get the length of the largest read that we can provide to a contiguous slice
// without an extra linearizing copy of all bytes internally.
func (b *AtomicFixedSizeRingBuf) ContigLen() int {
b.tex.Lock()
defer b.tex.Unlock()
extent := b.Beg + b.readable
firstContigLen := intMin2(extent, b.N) - b.Beg
return firstContigLen
}
// constructor. NewAtomicFixedSizeRingBuf will allocate internally
// two buffers of size maxViewInBytes.
func NewAtomicFixedSizeRingBuf(maxViewInBytes int) *AtomicFixedSizeRingBuf {
n := maxViewInBytes
r := &AtomicFixedSizeRingBuf{
Use: 0, // 0 or 1, whichever is actually in use at the moment.
// If we are asked for Bytes() and we wrap, linearize into the other.
N: n,
Beg: 0,
readable: 0,
}
r.A[0] = make([]byte, n, n)
r.A[1] = make([]byte, n, n)
return r
}
// Bytes() returns a slice of the contents of the unread portion of the buffer.
//
// To avoid copying, see the companion BytesTwo() call.
//
// Unlike the standard library Bytes() method (on bytes.Buffer for example),
// the result of the AtomicFixedSizeRingBuf::Bytes(true) is a completely new
// returned slice, so modifying that slice will have no impact on the contents
// of the internal ring.
//
// Bytes(false) acts like the standard library bytes.Buffer::Bytes() call,
// in that it returns a slice which is backed by the buffer itself (so
// no copy is involved).
//
// The largest slice Bytes ever returns is bounded above by the maxViewInBytes
// value used when calling NewAtomicFixedSizeRingBuf().
//
// Possible side-effect: may modify b.Use, the buffer in use.
//
func (b *AtomicFixedSizeRingBuf) Bytes(makeCopy bool) []byte {
b.tex.Lock()
defer b.tex.Unlock()
extent := b.Beg + b.readable
if extent <= b.N {
// we fit contiguously in this buffer without wrapping to the other
return b.A[b.Use][b.Beg:(b.Beg + b.readable)]
}
// wrap into the other buffer
src := b.Use
dest := 1 - b.Use
n := copy(b.A[dest], b.A[src][b.Beg:])
n += copy(b.A[dest][n:], b.A[src][0:(extent%b.N)])
b.Use = dest
b.Beg = 0
if makeCopy {
ret := make([]byte, n)
copy(ret, b.A[b.Use][:n])
return ret
}
return b.A[b.Use][:n]
}
// TwoBuffers: the return value of BytesTwo(). TwoBuffers
// holds two slices to the contents of the readable
// area of the internal buffer. The slices contents are logically
// ordered First then Second, but the Second will actually
// be physically before the First. Either or both of
// First and Second may be empty slices.
type TwoBuffers struct {
First []byte // the first part of the contents
Second []byte // the second part of the contents
}
// BytesTwo returns all readable bytes, but in two separate slices,
// to avoid copying. The two slices are from the same buffer, but
// are not contiguous. Either or both may be empty slices.
func (b *AtomicFixedSizeRingBuf) BytesTwo() TwoBuffers {
b.tex.Lock()
defer b.tex.Unlock()
return b.unatomic_BytesTwo()
}
func (b *AtomicFixedSizeRingBuf) unatomic_BytesTwo() TwoBuffers {
extent := b.Beg + b.readable
if extent <= b.N {
// we fit contiguously in this buffer without wrapping to the other.
// Let second stay an empty slice.
return TwoBuffers{First: b.A[b.Use][b.Beg:(b.Beg + b.readable)], Second: []byte{}}
}
return TwoBuffers{First: b.A[b.Use][b.Beg:(b.Beg + b.readable)], Second: b.A[b.Use][0:(extent % b.N)]}
}
// Purpose of BytesTwo() and AdvanceBytesTwo(): avoid extra copying of data.
//
// AdvanceBytesTwo() takes a TwoBuffers as input, this must have been
// from a previous call to BytesTwo(); no intervening calls to Bytes()
// or Adopt() are allowed (or any other future routine or client data
// access that changes the internal data location or contents) can have
// been made.
//
// After sanity checks, AdvanceBytesTwo() advances the internal buffer, effectively
// calling Advance( len(tb.First) + len(tb.Second)).
//
// If intervening-calls that changed the buffers (other than appending
// data to the buffer) are detected, we will panic as a safety/sanity/
// aid-to-debugging measure.
//
func (b *AtomicFixedSizeRingBuf) AdvanceBytesTwo(tb TwoBuffers) {
b.tex.Lock()
defer b.tex.Unlock()
tblen := len(tb.First) + len(tb.Second)
if tblen == 0 {
return // nothing to do
}
// sanity check: insure we have re-located in the meantime
if tblen > b.readable {
panic(fmt.Sprintf("tblen was %d, and this was greater than b.readerable = %d. Usage error detected and data loss may have occurred (available data appears to have shrunken out from under us!).", tblen, b.readable))
}
tbnow := b.unatomic_BytesTwo()
if len(tb.First) > 0 {
if tb.First[0] != tbnow.First[0] {
panic(fmt.Sprintf("slice contents of First have changed out from under us!: '%s' vs '%s'", string(tb.First), string(tbnow.First)))
}
}
if len(tb.Second) > 0 {
if len(tb.First) > len(tbnow.First) {
panic(fmt.Sprintf("slice contents of Second have changed out from under us! tbnow.First length(%d) is less than tb.First(%d.", len(tbnow.First), len(tb.First)))
}
if len(tbnow.Second) == 0 {
panic(fmt.Sprintf("slice contents of Second have changed out from under us! tbnow.Second is empty, but tb.Second was not"))
}
if tb.Second[0] != tbnow.Second[0] {
panic(fmt.Sprintf("slice contents of Second have changed out from under us!: '%s' vs '%s'", string(tb.Second), string(tbnow.Second)))
}
}
b.unatomic_advance(tblen)
}
// Read():
//
// From bytes.Buffer.Read(): Read reads the next len(p) bytes
// from the buffer or until the buffer is drained. The return
// value n is the number of bytes read. If the buffer has no data
// to return, err is io.EOF (unless len(p) is zero); otherwise it is nil.
//
// from the description of the Reader interface,
// http://golang.org/pkg/io/#Reader
//
/*
Reader is the interface that wraps the basic Read method.
Read reads up to len(p) bytes into p. It returns the number
of bytes read (0 <= n <= len(p)) and any error encountered.
Even if Read returns n < len(p), it may use all of p as scratch
space during the call. If some data is available but not
len(p) bytes, Read conventionally returns what is available
instead of waiting for more.
When Read encounters an error or end-of-file condition after
successfully reading n > 0 bytes, it returns the number of bytes
read. It may return the (non-nil) error from the same call or
return the error (and n == 0) from a subsequent call. An instance
of this general case is that a Reader returning a non-zero number
of bytes at the end of the input stream may return
either err == EOF or err == nil. The next Read should
return 0, EOF regardless.
Callers should always process the n > 0 bytes returned before
considering the error err. Doing so correctly handles I/O errors
that happen after reading some bytes and also both of the
allowed EOF behaviors.
Implementations of Read are discouraged from returning a zero
byte count with a nil error, and callers should treat that
situation as a no-op.
*/
//
func (b *AtomicFixedSizeRingBuf) Read(p []byte) (n int, err error) {
return b.ReadAndMaybeAdvance(p, true)
}
// ReadWithoutAdvance(): if you want to Read the data and leave
// it in the buffer, so as to peek ahead for example.
func (b *AtomicFixedSizeRingBuf) ReadWithoutAdvance(p []byte) (n int, err error) {
return b.ReadAndMaybeAdvance(p, false)
}
func (b *AtomicFixedSizeRingBuf) ReadAndMaybeAdvance(p []byte, doAdvance bool) (n int, err error) {
b.tex.Lock()
defer b.tex.Unlock()
if len(p) == 0 {
return 0, nil
}
if b.readable == 0 {
return 0, io.EOF
}
extent := b.Beg + b.readable
if extent <= b.N {
n += copy(p, b.A[b.Use][b.Beg:extent])
} else {
n += copy(p, b.A[b.Use][b.Beg:b.N])
if n < len(p) {
n += copy(p[n:], b.A[b.Use][0:(extent%b.N)])
}
}
if doAdvance {
b.unatomic_advance(n)
}
return
}
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
//
// Write doesn't modify b.User, so once a []byte is pinned with
// a call to Bytes(), it should remain valid even with additional
// calls to Write() that come after the Bytes() call.
//
func (b *AtomicFixedSizeRingBuf) Write(p []byte) (n int, err error) {
b.tex.Lock()
defer b.tex.Unlock()
for {
if len(p) == 0 {
// nothing (left) to copy in; notice we shorten our
// local copy p (below) as we read from it.
return
}
writeCapacity := b.N - b.readable
if writeCapacity <= 0 {
// we are all full up already.
return n, io.ErrShortWrite
}
if len(p) > writeCapacity {
err = io.ErrShortWrite
// leave err set and
// keep going, write what we can.
}
writeStart := (b.Beg + b.readable) % b.N
upperLim := intMin2(writeStart+writeCapacity, b.N)
k := copy(b.A[b.Use][writeStart:upperLim], p)
n += k
b.readable += k
p = p[k:]
// we can fill from b.A[b.Use][0:something] from
// p's remainder, so loop
}
}
// WriteTo and ReadFrom avoid intermediate allocation and copies.
// WriteTo avoids intermediate allocation and copies.
// WriteTo writes data to w until there's no more data to write
// or when an error occurs. The return value n is the number of
// bytes written. Any error encountered during the write is also returned.
func (b *AtomicFixedSizeRingBuf) WriteTo(w io.Writer) (n int64, err error) {
b.tex.Lock()
defer b.tex.Unlock()
if b.readable == 0 {
return 0, io.EOF
}
extent := b.Beg + b.readable
firstWriteLen := intMin2(extent, b.N) - b.Beg
secondWriteLen := b.readable - firstWriteLen
if firstWriteLen > 0 {
m, e := w.Write(b.A[b.Use][b.Beg:(b.Beg + firstWriteLen)])
n += int64(m)
b.unatomic_advance(m)
if e != nil {
return n, e
}
// all bytes should have been written, by definition of
// Write method in io.Writer
if m != firstWriteLen {
return n, io.ErrShortWrite
}
}
if secondWriteLen > 0 {
m, e := w.Write(b.A[b.Use][0:secondWriteLen])
n += int64(m)
b.unatomic_advance(m)
if e != nil {
return n, e
}
// all bytes should have been written, by definition of
// Write method in io.Writer
if m != secondWriteLen {
return n, io.ErrShortWrite
}
}
return n, nil
}
// ReadFrom avoids intermediate allocation and copies.
// ReadFrom() reads data from r until EOF or error. The return value n
// is the number of bytes read. Any error except io.EOF encountered
// during the read is also returned.
func (b *AtomicFixedSizeRingBuf) ReadFrom(r io.Reader) (n int64, err error) {
b.tex.Lock()
defer b.tex.Unlock()
for {
writeCapacity := b.N - b.readable
if writeCapacity <= 0 {
// we are all full
return n, nil
}
writeStart := (b.Beg + b.readable) % b.N
upperLim := intMin2(writeStart+writeCapacity, b.N)
m, e := r.Read(b.A[b.Use][writeStart:upperLim])
n += int64(m)
b.readable += m
if e == io.EOF {
return n, nil
}
if e != nil {
return n, e
}
}
}
// Reset quickly forgets any data stored in the ring buffer. The
// data is still there, but the ring buffer will ignore it and
// overwrite those buffers as new data comes in.
func (b *AtomicFixedSizeRingBuf) Reset() {
b.tex.Lock()
defer b.tex.Unlock()
b.Beg = 0
b.readable = 0
b.Use = 0
}
// Advance(): non-standard, but better than Next(),
// because we don't have to unwrap our buffer and pay the cpu time
// for the copy that unwrapping may need.
// Useful in conjuction/after ReadWithoutAdvance() above.
func (b *AtomicFixedSizeRingBuf) Advance(n int) {
b.tex.Lock()
defer b.tex.Unlock()
b.unatomic_advance(n)
}
// unatomic_advance(): private implementation of Advance() without
// the locks. See Advance() above for description.
// Necessary so that other methods that already hold
// locks can advance, and there are no recursive mutexes
// in Go.
func (b *AtomicFixedSizeRingBuf) unatomic_advance(n int) {
if n <= 0 {
return
}
if n > b.readable {
n = b.readable
}
b.readable -= n
b.Beg = (b.Beg + n) % b.N
}
// Adopt(): non-standard.
//
// For efficiency's sake, (possibly) take ownership of
// already allocated slice offered in me.
//
// If me is large we will adopt it, and we will potentially then
// write to the me buffer.
// If we already have a bigger buffer, copy me into the existing
// buffer instead.
//
// Side-effect: may change b.Use, among other internal state changes.
//
func (b *AtomicFixedSizeRingBuf) Adopt(me []byte) {
b.tex.Lock()
defer b.tex.Unlock()
n := len(me)
if n > b.N {
b.A[0] = me
b.A[1] = make([]byte, n, n)
b.N = n
b.Use = 0
b.Beg = 0
b.readable = n
} else {
// we already have a larger buffer, reuse it.
copy(b.A[0], me)
b.Use = 0
b.Beg = 0
b.readable = n
}
}
// keep the atomic_rbuf.go standalone and usable without
// the rbuf.go file, by simply duplicating intMin from rbuf.go
//
func intMin2(a, b int) int {
if a < b {
return a
} else {
return b
}
}

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

Loading…
Cancel
Save