Giter Site home page Giter Site logo

overseer's Introduction

overseer

GoDoc Tests

overseer is a package for creating monitorable, gracefully restarting, self-upgrading binaries in Go (golang). The main goal of this project is to facilitate the creation of self-upgrading binaries which play nice with standard process managers, secondly it should expose a small and simple API with reasonable defaults.

overseer diagram

Commonly, graceful restarts are performed by the active process (dark blue) closing its listeners and passing these matching listening socket files (green) over to a newly started process. This restart causes any foreground process monitoring to incorrectly detect a program crash. overseer attempts to solve this by using a small process to perform this socket file exchange and proxying signals and exit code from the active process.

Features

  • Simple
  • Works with process managers (systemd, upstart, supervisor, etc)
  • Graceful, zero-down time restarts
  • Easy self-upgrading binaries

Install

go get github.com/jpillora/overseer

Quick example

This program works with process managers, supports graceful, zero-down time restarts and self-upgrades its own binary.

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/jpillora/overseer"
	"github.com/jpillora/overseer/fetcher"
)

//create another main() to run the overseer process
//and then convert your old main() into a 'prog(state)'
func main() {
	overseer.Run(overseer.Config{
		Program: prog,
		Address: ":3000",
		Fetcher: &fetcher.HTTP{
			URL:      "http://localhost:4000/binaries/myapp",
			Interval: 1 * time.Second,
		},
	})
}

//prog(state) runs in a child process
func prog(state overseer.State) {
	log.Printf("app (%s) listening...", state.ID)
	http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "app (%s) says hello\n", state.ID)
	}))
	http.Serve(state.Listener, nil)
}

How it works:

  • overseer uses the main process to check for and install upgrades and a child process to run Program.
  • The main process retrieves the files of the listeners described by Address/es.
  • The child process is provided with these files which is converted into a Listener/s for the Program to consume.
  • All child process pipes are connected back to the main process.
  • All signals received on the main process are forwarded through to the child process.
  • Fetcher runs in a goroutine and checks for updates at preconfigured interval. When Fetcher returns a valid binary stream (io.Reader), the master process saves it to a temporary location, verifies it, replaces the current binary and initiates a graceful restart.
  • The fetcher.HTTP accepts a URL, it polls this URL with HEAD requests and until it detects a change. On change, we GET the URL and stream it back out to overseer. See also fetcher.S3.
  • Once a binary is received, it is run with a simple echo token to confirm it is a overseer binary.
  • Except for scheduled restarts, the active child process exiting will cause the main process to exit with the same code. So, overseer is not a process manager.

See Configuration options here and the runtime State available to your program here.

More examples

See the example/ directory and run example.sh, you should see the following output:

$ cd example/
$ sh example.sh
BUILT APP (1)
RUNNING APP
app#1 (c7940a5bfc3f0e8633d3bf775f54bb59f50b338e) listening...
app#1 (c7940a5bfc3f0e8633d3bf775f54bb59f50b338e) says hello
app#1 (c7940a5bfc3f0e8633d3bf775f54bb59f50b338e) says hello
BUILT APP (2)
app#2 (3dacb8bc673c1b4d38f8fb4fad5b017671aa8a67) listening...
app#2 (3dacb8bc673c1b4d38f8fb4fad5b017671aa8a67) says hello
app#2 (3dacb8bc673c1b4d38f8fb4fad5b017671aa8a67) says hello
app#1 (c7940a5bfc3f0e8633d3bf775f54bb59f50b338e) says hello
app#1 (c7940a5bfc3f0e8633d3bf775f54bb59f50b338e) exiting...
BUILT APP (3)
app#3 (b7614e7ff42eed8bb334ed35237743b0e4041678) listening...
app#3 (b7614e7ff42eed8bb334ed35237743b0e4041678) says hello
app#3 (b7614e7ff42eed8bb334ed35237743b0e4041678) says hello
app#2 (3dacb8bc673c1b4d38f8fb4fad5b017671aa8a67) says hello
app#2 (3dacb8bc673c1b4d38f8fb4fad5b017671aa8a67) exiting...
app#3 (b7614e7ff42eed8bb334ed35237743b0e4041678) says hello

Note: app#1 stays running until the last request is closed.

Only use graceful restarts

func main() {
	overseer.Run(overseer.Config{
		Program: prog,
		Address: ":3000",
	})
}

Send main a SIGUSR2 (Config.RestartSignal) to manually trigger a restart

Only use auto-upgrades, no restarts

func main() {
	overseer.Run(overseer.Config{
		Program: prog,
		NoRestart: true,
		Fetcher: &fetcher.HTTP{
			URL:      "http://localhost:4000/binaries/myapp",
			Interval: 1 * time.Second,
		},
	})
}

Your binary will be upgraded though it will require manual restart from the user, suitable for creating self-upgrading command-line applications.

Multi-platform binaries using a dynamic fetch URL

func main() {
	overseer.Run(overseer.Config{
		Program: prog,
		Fetcher: &fetcher.HTTP{
			URL: "http://localhost:4000/binaries/app-"+runtime.GOOS+"-"+runtime.GOARCH,
			//e.g.http://localhost:4000/binaries/app-linux-amd64
		},
	})
}

Known issues

  • The master process's overseer.Config cannot be changed via an upgrade, the master process must be restarted.
    • Therefore, Addresses can only be changed by restarting the main process.
  • Currently shells out to mv for moving files because mv handles cross-partition moves unlike os.Rename.
  • Package init() functions will run twice on start, once in the main process and once in the child process.

More documentation

Third-party Fetchers

Contributing

See CONTRIBUTING.md

overseer's People

Contributors

audiolion avatar bnelz avatar chicknsoup avatar fesiqueira avatar fmpwizard avatar glorieux avatar joshforbes avatar jpillora avatar lookfirst avatar mutexlock avatar tgulacsi avatar wrfly avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

overseer's Issues

use of closed network connection

Copied the code on the homepage, but now I print out the error from http.Serve().

./test
2019/06/06 15:42:07 [overseer master] run
2019/06/06 15:42:07 [overseer master] starting /tmp/main/test
2019/06/06 15:42:07 [overseer slave#1] run
2019/06/06 15:42:07 [overseer slave#1] start program
2019/06/06 15:42:07 app (f81a86c0d7a5120aa306900493dacb542143ca66) listening...
2019/06/06 15:42:45 [overseer master] graceful restart triggered
2019/06/06 15:42:45 [overseer slave#1] graceful shutdown requested
2019/06/06 15:42:45 accept tcp [::]:3000: use of closed network connection
2019/06/06 15:42:45 [overseer master] signaled, sockets ready
2019/06/06 15:42:45 [overseer master] starting /tmp/main/test
2019/06/06 15:42:45 [overseer master] restart success
2019/06/06 15:42:45 [overseer slave#2] run
2019/06/06 15:42:45 [overseer slave#2] start program
2019/06/06 15:42:45 app (f81a86c0d7a5120aa306900493dacb542143ca66) listening...

The problem is that when I do a kill -s SIGUSR2 PID, I sometimes get an error along the lines of:

accept tcp [::]:3000: use of closed network connection

Ideas?

[overseer master] proxy signal (broken pipe)

sometimes happen, how to do with this ?

[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)
[overseer master] proxy signal (broken pipe)

Support binary diffs, versions

I've stumbled upon https://github.com/sanbornm/go-selfupdate which solves the binary diff problem nicely, but lacks overseer's great graceful restart capabilities.

Maybe a marriage made in heaven?

a) copy code from go-selfupdate
b) just use go-selfupdate cli to generate the versions, and add a proper fetcher for such config?

I'm willing to help, but please decide which way to go, what kind and level of integration is desired!

graceful restart can not achieve zero-down

My project is built on the Beego, Now I want to graceful restart my project with Overseer. When I send kill -SIGUSR2 PID. I found that the current process was closed first, and then fork a new process. This will lead to a period of no work process in the middle, resulting in service denial of access. Unable to achieve zero-down

My project code

func main() {
overseer.Run(overseer.Config{
Program: prog,
Debug: true,
})
}

func prog(state overseer.State) {
beego.BConfig.RecoverPanic = true
beego.BConfig.Listen.Graceful = false
beego.Run()
}

Overseer log

2020/12/29 16:36:31 [overseer master] graceful restart triggered
2020/12/29 16:36:31 [overseer master] restart success
2020/12/29 16:36:31 [overseer slave#2] graceful shutdown requested
2020/12/29 16:36:31 [overseer slave#2] timeout. forceful shutdown
2020/12/29 16:36:31 [overseer master] prog exited with 1
2020/12/29 16:36:31 [overseer master] starting /Users/Demo

2020/12/29 16:36:34 [overseer slave#3] run
2020/12/29 16:36:34 [overseer slave#3] start program

Looking forward to your reply, Thank you!

Issue when path is symlink

At work, our deployment goes something like this:

copy go binary to

/srv/tools//binary-here

then create/update an symlink so that we have:
/srv/tools/current => /srv/tools/<last-build-id>

This lets us quickly revert to a previous version if things go wrong and we didn't detect any issues during testing.

Because overseer uses github.com/kardianos/osext to detect the "real" path to the binary that is running and needs to be restarted, sending a restart signal doesn't restart the new binary, but it restarts the "old" binary, the one with the specific build-id, instead of the one pointed by current

Possible solutions:

  1. I could update our deployment to make a "copy" of the binary to .../current/, and duplicate it at /srv/tools/, but doesn't seem too clean
  2. I see that you have this env variable envBinPath = "OVERSEER_BIN_PATH" , how about checking if this is set, and if is is, then use that instead of calling :
func (mp *master) checkBinary() error {
	//get path to binary and confirm its writable
	binPath, err := osext.Executable()
	if err != nil {
		return fmt.Errorf("failed to find binary path (%s)", err)
	}

so, it would be something like

mp.binPath = os.GetEnv(envBinPath)

I would be happy to send a PR if you find this idea useful, or maybe there is already a work around this.

Timeout. Forceful shutdown.

Hi. I'm trying use your package with no success.
Debian 8

If I starting my server, and don't do any http request, and then run Kkill cmd I get:

[overseer slave#1] run
[overseer slave#1] start program
some output from my program
sudo kill -SIGUSR2 PID
[overseer slave#1] graceful shutdown requested
[overseer master] proxy signal (user defined signal 1)
[overseer master] prog exited with 0

But, if I start the server, and do at least one http request, and then run kill cmd, the program exiting after timeout:

[overseer slave#1] run
[overseer slave#1] start program
some output from my program

wget http://locahost:PORT/api/
sudo kill -SIGUSR2 pid

[overseer slave#1] graceful shutdown requested
[overseer master] proxy signal (user defined signal 1)
[overseer slave#1] timeout. forceful shutdown
[overseer master] prog exited with 1

Ant ideas?

My main is:

func main() {
    overseer.Run(overseer.Config{
        Program: prog,
        Address: ":" + config.Port,
        Debug: true,
    })
}

My packages:

import (
    "net/http"
    "github.com/gorilla/mux"
    "fmt"
    "github.com/*/*/components"
    "runtime"
    "time"
    log "github.com/Sirupsen/logrus"
    "github.com/nytimes/gziphandler"
    "github.com/justinas/alice"
    "github.com/*/*/routers"
    "syscall"
    "github.com/jpillora/overseer"
 )

Support github token for private repositories/avoid rate-limit

I would really like to use this cool tool for some projects which are hosted within a private github repository. To do so there should be a chance to add a github-token to access them.

Even though this could fix problems related to rate-limits.

The configuration could look like this:

overseer.Run(overseer.Config{
		Program: prog,
		Fetcher: &fetcher.Github{
			User:     "jaedle",
			Repo:     "overseer",
			Interval: time.Minute,
			Token:    "super-secret-token",
		},
	})

Are you welcoming pull requests for this feature?

Call sync after mv?

Thanks for a really awesome tool.

I'm running this on > 1000 little Pi servers that have various quality of flash storage and may reboot right in the middle of doing work.

Occasionally, I'll see either a corrupt file after an overseer upgrade or I'll see the file has zero bytes.

I'm thinking that calling sync after the mv will help with the corruption issues?

Sanity check failed due to message output

Hi,

I've been testing the fetcher on my program and I always get a Sanity check fail in fetch(). I've added additional debug messaging to show tokenIn and tokenOut, and this is returned:
2016/09/20 08:02:18 [overseer master] sanity check failed:d57f1402568b9d42, 2016/09/20 08:02:18 INFO:EcomRate.bdb-ConfigLoad - Retry - 5

It seems that the first message that is returned on stdOUT from my code is a database load INFO message, instead of the token. This is most likely occuring due to my import load order and my defined init.

For a quick fix I'm going to comment out the tokenIn/tokenOut check. But for a longer term fix I think Overseer needs to have a init function defined that outputs the token.

REQUEST: Report an issue

Would like to see an option for users to report issues about shows/movies like Petio currently has. I prefer using overseer for my request management but like that feature in Petio.

[Feature request] Configure github to fetch after interval instead of right away

Awesome library. Thanks for figuring out all the esoteric unix restart logic! I've learned some good golang tricks from reading your code as well.

The github Fetch() method fetches right away as soon as overseer starts up, but this doesn't work for me because I'm building an agent that goes onto literally thousands of small linux boxes running on a network.

If I start/reboot all the machines at the same time, then I'll DDOS not only github, but also my own network if there is an upgrade to be had.

All I'm asking for is the ability to configure this behavior. Ideally, just fetch after the first interval (which I randomize during configure time to prevent the DDOS).

As a short term solution, I've just hacked my own fetcher from your code, but that isn't obviously the right long term solution.

"failed to run temp binary" on Windows

Hello 👋

Thanks for the great library. It was simple to get it implemented in our code base. We have it working successfully on Mac but I'm running into a bit of trouble on Windows. We're using the HTTP fetcher which is downloading the file and placing it in a temp directory successfully (can see it show up in file explorer). But when the fetch process tries to see if the downloaded binary is executable it returns an error and bails out:

failed to run temp binary: exec: "C:\\Users\\Level\\AppData\\Local\\Temp\\overseer-1dc5f76e2b9039ce": file does not exist (C:\Users\Level\AppData\Local\Temp\overseer-1dc5f76e2b9039ce) output ""

The file certainly does exist but for some reason Go thinks it doesn't. The error is coming from this line:

mp.warnf("failed to run temp binary: %s (%s) output \"%s\"", err, tmpBinPath, tokenOut)

image

Does anyone have any experience or advice related to this error? Thanks!

Question: about the example, why app#3 never exiting?

BUILT APP (1)
RUNNING APP
app#1 (1357a5d6a252170f2cf9e60b6edce84e28ebe3bf) listening...
app#1 (1357a5d6a252170f2cf9e60b6edce84e28ebe3bf) says hello
app#1 (1357a5d6a252170f2cf9e60b6edce84e28ebe3bf) says hello
BUILT APP (2)
app#2 (5b2873c193587d494c5e11052856cd587924ce1a) listening...
app#2 (5b2873c193587d494c5e11052856cd587924ce1a) says hello
app#2 (5b2873c193587d494c5e11052856cd587924ce1a) says hello
app#1 (1357a5d6a252170f2cf9e60b6edce84e28ebe3bf) says hello
app#1 (1357a5d6a252170f2cf9e60b6edce84e28ebe3bf) exiting...
BUILT APP (3)
app#3 (364734c72d8885a7f695fbae8ef154b2e2afa2c5) listening...
app#3 (364734c72d8885a7f695fbae8ef154b2e2afa2c5) says hello
app#3 (364734c72d8885a7f695fbae8ef154b2e2afa2c5) says hello
app#2 (5b2873c193587d494c5e11052856cd587924ce1a) says hello
app#2 (5b2873c193587d494c5e11052856cd587924ce1a) exiting...
app#3 (364734c72d8885a7f695fbae8ef154b2e2afa2c5) says hello

Error with windows build

I'm trying to build a windows version (.exe) for my project on linux using CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ GOOS=windows GOARCH=amd64 go build -ldflags -H=windowsgui -o ./builds/jacp-windows-raw.exe but it shows this error:

../../go/pkg/mod/github.com/jpillora/[email protected]/proc_slave_windows.go:12:2: missing go.sum entry for module providing package github.com/StackExchange/wmi (imported by github.com/jpillora/overseer); to add:

@jpillora

restarting

Is there a way to code a golang app so that it can control its restarting itself ?

I have a bit of a russian doll situation where i need the golang code to do various things, rerestart with a different flag, do things, restart again with a different flag. You get the idea and its like a russian doll metaphor.

Super great package !! thanks for doing this.

I can test on arm v7 and v8 64 bit also soon.

overseer blocks new requests without Fetcher

When I use Fetcher and link to a path, after I place new file, overseer successfully load it and all new requests goes to this new child and old requests are awaiting for complete.

This is an expected behavior. I liked this method very much. However, If I remove Fetcher and manually send SIGUSR2 to the main process, new requests are hang until existing's are finish.

It basically blocks new incoming requests. Why isn't working without Fetcher?

I'm using systemd to start main app.

Update: It seems even with Fetcher there is a slight downtime. I think my config somehow wrong. However, I also used very basic example from example/

[Bug] Document and expose the overseer sanity check for cases where it needs to be run manually

I'm using spf13/cobra with an "update" subcommand, and the sanity check fails as it calls the binary without arguments, which means the help page is printed, instead of the token.

I can add the "missing" arg before execute, Iff I know I need it - when OVERSEER_BIN_UPD env var is set. So this constant should be exported, and the needed special behaviour of the binary documented.

How can i use it with gin ?

I can't find any documentation about using gin and overseer together.Below is my code, how can I use them together?

router := gin.Default()
router.Use(tlsHandler())
router.RunTLS(":8080", ".pem", ".key")

Using a go routine does not keep the connection alive

Using a go routine does not keep the connection alive.

  1. compile program.
  2. telnet to port 3000
  3. kill -s SIGUSR2 PID

If you do this same program without the go routine wrapper, the connection is not dropped.

package main

import (
        "fmt"
        "log"
        "net/http"

        "github.com/jpillora/overseer"
)

//create another main() to run the overseer process
//and then convert your old main() into a 'prog(state)'
func main() {
        overseer.Run(overseer.Config{
                Debug: true,
                Program: prog,
                Address: ":3000",
        })
}

//prog(state) runs in a child process
func prog(state overseer.State) {
        log.Printf("app (%s) listening...", state.ID)
        http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "app (%s) says hello\n", state.ID)
        }))
        go func() {
                if err := http.Serve(state.Listener, nil); err != nil {
                     log.Println(err)
                }
        }()
        <-state.GracefulShutdown
}

Windows triggerRestart() not working

Similar to watchParrent(), we cannot send signals between windows processes, so triggerRestart() obviously does not work.

To fix this, we have to find a way to send/receive messages between master/slave processes.

  • gRPC over tcp
  • RESTful
  • Windows API (like postmessage/sendmessage)

What do you think? @jpillora

Windows port binding

I'm trying to run overseer-based application on windows server 2016 and getting

[overseer] disabled. run failed: Failed to retreive fd for: 127.0.0.1:92 (file tcp 127.0.0.1:92: not supported by windows)
...
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x639c3d]

No matter what port I use (I've tried several from different ranges) - the error is the same.
On mac os the same application works just fine.

Any hints on this? That was quite unexpected.

Windows

Edit: Sorry for the terrible title, I thought I updated it before posting...

Getting a permission denied error on Windows after updated binary downloads. The original application does not crash, which is nice... Works perfectly on Mac. I've tried everything I can think of:

  • Cannot run the demo app because windows does not support files over tcp
  • Run application as administrator
  • Tried multiple folder locations
  • Tested multiple difference machines

Windows error message:

 [overseer master] failed to overwrite binary: [cmd /c move /y C:\Users\{user}\source\go\src\syncer\syncer.exe C:\Users\{user}\source\go\src\syncer\syncer-old.exe]: "Access is denied.\r\n        0 file(s) moved.": exit status 1

Files created:
image

Overseer.Config

overseer.Config{
		Program:          prog,
		Required:         true,
		TerminateTimeout: time.Second * 30,
		Fetcher: &fetcher.HTTP{
			URL:      fetchUrl,
			Interval: time.Second * 10,
		}

Thanks for any input!

Operational inquiries

Hello, I am interested in using this library but I have a few questions about how it works.

Fetcher: &fetcher.HTTP{ URL: "http://localhost:4000/binaries/myapp", Interval: 1 * time.Second, }

  • The program that does every x time intervals?
  • How do you know you have an update?
  • When download a new binary, does it erase the old one?
  • Some example with systemd?

I hope someone can answer my questions.
Kind regards

Sanity Check Failed Echo Framework

im trying to integrate with Echo framework https://echo.labstack.com/

func main() {
	overseer.Run(overseer.Config{
		Program: server,
		Address: viper.GetString("server.address"),
		Fetcher: &fetcher.File{
			Path: "/home/putrafajarh/code/heimdall/heimdall",
		},
		Debug: true,
	})
}

func server(state overseer.State) {
	dbHost := viper.GetString(`database.host`)
	dbPort := viper.GetString(`database.port`)
	dbUser := viper.GetString(`database.user`)
	dbPass := viper.GetString(`database.pass`)
	dbName := viper.GetString(`database.name`)
	connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
	val := url.Values{}
	val.Add("parseTime", "1")
	val.Add("loc", "Asia/Jakarta")
	dsn := fmt.Sprintf("%s?%s", connection, val.Encode())
	dbConn, err := sql.Open(`mysql`, dsn)

	if err != nil {
		log.Fatal(err)
	}
	err = dbConn.Ping()
	if err != nil {
		log.Fatal(err)
	}

	defer func() {
		err := dbConn.Close()
		if err != nil {
			log.Fatal(err)
		}
	}()

	e := echo.New()
	e.Listener = state.Listener
	middL := _articleHttpDeliveryMiddleware.InitMiddleware()
	e.Use(middL.CORS)
	authorRepo := _authorRepo.NewMysqlAuthorRepository(dbConn)
	ar := _articleRepo.NewMysqlArticleRepository(dbConn)

	timeoutContext := time.Duration(viper.GetInt("context.timeout")) * time.Second
	au := _articleUcase.NewArticleUsecase(ar, authorRepo, timeoutContext)
	_articleHttpDelivery.NewArticleHandler(e, au)

	// s := &http.Server{
	// 	Addr: viper.GetString("server.address"),
	// }

	fmt.Printf("app#%s (%s) listening...\n", VersionID, state.ID)
	log.Fatal(e.Start(viper.GetString("server.address")))
	fmt.Printf("app#%s (%s) exiting...\n", VersionID, state.ID)
}

i got this log, and code changes doesnt work when i try to hit using postman

2020/12/11 07:32:32 [overseer master] proxy signal (urgent I/O condition)
2020/12/11 07:32:33 [overseer master] streaming update...
2020/12/11 07:32:33 [overseer master] proxy signal (urgent I/O condition)
2020/12/11 07:32:33 [overseer master] proxy signal (urgent I/O condition)
2020/12/11 07:32:33 [overseer master] proxy signal (urgent I/O condition)
2020/12/11 07:32:33 [overseer master] sanity check failed
2020/12/11 07:32:33 [overseer master] checking for updates...
2020/12/11 07:32:34 [overseer master] no updates

Feature request: support udp and unix protocols

func (mp *master) retreiveFileDescriptors() error {
	mp.slaveExtraFiles = make([]*os.File, len(mp.Config.Addresses))
	for i, addr := range mp.Config.Addresses {
		a, err := net.ResolveTCPAddr("tcp", addr)
                //a,err :=net.ResolveUnixAddr("unix",addr)
               //net.ResolveUnixAddr("udp",addr)
		if err != nil {
			return fmt.Errorf("Invalid address %s (%s)", addr, err)
		}
		l, err := net.ListenTCP("tcp", a)
		if err != nil {
			return err
		}
		f, err := l.File()
		if err != nil {
			return fmt.Errorf("Failed to retreive fd for: %s (%s)", addr, err)
		}
		if err := l.Close(); err != nil {
			return fmt.Errorf("Failed to close listener for: %s (%s)", addr, err)
		}
		mp.slaveExtraFiles[i] = f
	}
	return nil
}

like this?


edit from @jpillora: to support this and stay backwards compatible, I think we add this to the Address string:

Currently

  • :5000 is TCP 0.0.0.0:5000,
    We could also support
  • tcp::5000 which would also be TCP 0.0.0.0:5000,
  • tcp:0.0.0.0:5000 which would also be TCP 0.0.0.0:5000,
  • udp::5000 which would be UDP 0.0.0.0:5000
  • unix:/my/absolute/path/to/my/file.sock which would be a unix socket

[Feature] Ability to reliably test overseer

I was trying to create some unit tests to my app that is using overseer. The test need to change the os.Args so it can test different scenarios. The problem is that overseer starts a new sub-process (as it should) and the Go test framework fails to parse the modified arguments, as it doesn't recognize them. Check the following simplified example:

program.go

package main

import (
    "os"
    "strings"
    "time"

    "github.com/jpillora/overseer"
    "github.com/jpillora/overseer/fetcher"
)

var value = ""

func main() {
    overseer.Run(overseer.Config{
        Program: prog,
        Address: ":3000",
        Fetcher: &fetcher.HTTP{
            URL:      "http://localhost:4000/binaries/myapp",
            Interval: 1 * time.Second,
        },
    })
}

func prog(state overseer.State) {
    println("Running program")

    if len(os.Args) == 2 {
        if argParts := strings.Split(os.Args[1], "="); len(argParts) == 2 && argParts[0] == "--value" {
            value = argParts[1]
        }
    }
}

program_test.go

package main

import (
    "os"
    "testing"
)

func Test_main(t *testing.T) {
    os.Args = os.Args[:1]
    os.Args = append(os.Args, "--value=2")
    main()

    if value != "2" {
        t.Error("expected value 2, got %s", value)
    }
}

output

flag provided but not defined: -value
Usage of /tmp/go-build073924981/labs/program/_test/program.test:
  -test.bench string
        regular expression to select benchmarks to run
  -test.benchmem
        print memory allocations for benchmarks
  -test.benchtime duration
        approximate run time for each benchmark (default 1s)
  -test.blockprofile string
        write a goroutine blocking profile to the named file after execution
  -test.blockprofilerate int
        if >= 0, calls runtime.SetBlockProfileRate() (default 1)
  -test.count n
        run tests and benchmarks n times (default 1)
  -test.coverprofile string
        write a coverage profile to the named file after execution
  -test.cpu string
        comma-separated list of number of CPUs to use for each test
  -test.cpuprofile string
        write a cpu profile to the named file during execution
  -test.memprofile string
        write a memory profile to the named file after execution
  -test.memprofilerate int
        if >=0, sets runtime.MemProfileRate
  -test.outputdir string
        directory in which to write profiles
  -test.parallel int
        maximum test parallelism (default 8)
  -test.run string
        regular expression to select tests and examples to run
  -test.short
        run smaller test suite to save time
  -test.timeout duration
        if positive, sets an aggregate time limit for all tests
  -test.trace string
        write an execution trace to the named file after execution
  -test.v
        verbose: print additional output
exit status 2
FAIL    labs/program    0.019s

One solution would be instead of modifying the os.Args, set an environment variable and detect it inside the sub-process execution:

program_test.go

package main

import (
    "os"
    "testing"
)

func Test_main(t *testing.T) {
    if arg := os.Getenv("TEST_ARG"); arg != "" {
        os.Args = os.Args[:1]
        os.Args = append(os.Args, arg)
        main()
        return
    }

    os.Setenv("TEST_ARG", "--value=2")

    main()

    if value != "2" {
        t.Errorf("expected value 2, got %s", value)
    }
}

output

Running program
ok      labs/program    0.019s

Is there any better solution for this situation? If not, this issue can be closed and could be used in the future for others that have the same problem that I had.

usage question for OS startup

this looks really useful.
In my use case i typically need to install 3 binaries onto a machine.
web server, app server and maybe e another tool.
Now each one needs the other, so you have a graph.
with your system, each individual app would pull their updates and upgrade.
But what about the graph knowledge of each ?
There seems to be two choices here:

  1. Forget the graph and let each binary process self update independently
  2. Root one upgrades, and tells the others to shutdown and upgrade

Just thinking this through... and wondering what your thinking about dependency chains like this simple example.

Run example code failed in Win 10

Env

Win 10 (1909)

Description

Build example module, and run, error:

2020/07/15 10:02:52 [overseer master] run
2020/07/15 10:02:53 [overseer] disabled. run failed: cannot move binary back ([cmd /c move /y C:\Users\ping\AppData\Local\Temp\overseer-8a21b19139fe1a2b.exe D:\sourcecode\go\overseer\exa
mple\my_app]: "\xc1\xedһ\xb8\xf6\xb3\xcc\xd0\xf2\xd5\xfd\xd4\xdaʹ\xd3ô\xcb\xceļ\xfe\xa3\xac\xbd\xf8\xb3\xcc\xce\u07b7\xa8\xb7\xc3\xceʡ\xa3\r\n\xd2ƶ\xaf\xc1\xcb         0 \xb8\xf6\xceļ\xf
e\xa1\xa3": exit status 1)
app#1 () listening...
panic: runtime error: invalid memory address or nil pointer dereference
        panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x28 pc=0x65cbd4]

goroutine 1 [running]:
net/http.(*onceCloseListener).close(...)
        C:/Go/src/net/http/server.go:3337
sync.(*Once).doSlow(0xc00010c070, 0xc0000779c8)
        C:/Go/src/sync/once.go:66 +0xf3
sync.(*Once).Do(...)
        C:/Go/src/sync/once.go:57
net/http.(*onceCloseListener).Close(0xc00010c060, 0x0, 0x0)
        C:/Go/src/net/http/server.go:3333 +0x7e
panic(0x6ba600, 0x957040)
        C:/Go/src/runtime/panic.go:967 +0x16b
net/http.(*onceCloseListener).Accept(0xc00010c060, 0xc0000100d0, 0x6b52c0, 0x956fc0, 0x701740)
        <autogenerated>:1 +0x39
net/http.(*Server).Serve(0xc00012c000, 0x0, 0x0, 0x0, 0x0)
        C:/Go/src/net/http/server.go:2901 +0x264
net/http.Serve(...)
        C:/Go/src/net/http/server.go:2468
main.prog(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        D:/sourcecode/go/overseer/example/main.go:26 +0x210
github.com/jpillora/overseer.Run(0x0, 0x729820, 0x70f548, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        D:/sourcecode/go/overseer/overseer.go:106 +0x14e
main.main()
        D:/sourcecode/go/overseer/example/main.go:34 +0x16d

then setup a breakpoint in

func move(dst, src string) error {
	os.MkdirAll(filepath.Dir(dst), 0755)
	if err := os.Rename(src, dst); err == nil {
		return nil
	}
	//HACK: we're shelling out to move because windows
	//throws errors when crossing device boundaries.
	// https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/move.mspx?mfr=true

	// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
	R := func(s string) string { return replShellMeta.Replace(syscall.EscapeArg(s)) }
	cmd := exec.Command("cmd", "/c", `move /y "`+R(src)+`" "`+R(dst)+`"`)
	if b, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("%v: %q: %v", cmd.Args, bytes.TrimSpace(b), err)
	}
	return nil
}

and get the cmd args:

move /y "C:\Users\ping\AppData\Local\Temp\___go_build_main_go.exe" "C:\Users\ping\AppData\Local\Temp\overseer-01c8d5338e1b2dde.exe"

run the command in separate CMD window:

D:\>move /y "C:\Users\ping\AppData\Local\Temp\___go_build_main_go.exe" "C:\Users\ping\AppData\Local\Temp\overseer-01c8d5338e1b2dde.exe"
另一个程序正在使用此文件,进程无法访问。
移动了         0 个文件。

mean: "This file is being used by another program and cannot be accessed by the process."

May be this tool has some data race problem

When I build my project using "go build -race" with this tool, after shutdown the process by "Ctrl+C", I found any data race warning, like below :
WARNING: DATA RACE
Read at 0x00c420374de0 by goroutine 15:
github.com/jpillora/overseer.(*master).handleSignal()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:135 +0x115
github.com/jpillora/overseer.(*master).setupSignalling.func1()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:113 +0xa1

Previous write at 0x00c420374de0 by main goroutine:
github.com/jpillora/overseer.(*master).fork()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:350 +0x18f
github.com/jpillora/overseer.(*master).forkLoop()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:339 +0x38
github.com/jpillora/overseer.(*master).run()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:65 +0x172
github.com/jpillora/overseer.runErr()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:162 +0x27f
github.com/jpillora/overseer.Run()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:99 +0x9d
main.main_overseer()
/home/work/gowork/src/go-entry/main/main.go:59 +0x128
main.main()
/home/work/gowork/src/go-entry/main/main.go:49 +0x358

Goroutine 15 (running) created at:
github.com/jpillora/overseer.(*master).setupSignalling()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:115 +0x161
github.com/jpillora/overseer.(*master).run()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:56 +0xfb
github.com/jpillora/overseer.runErr()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:162 +0x27f
github.com/jpillora/overseer.Run()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:99 +0x9d
main.main_overseer()
/home/work/gowork/src/go-entry/main/main.go:59 +0x128
main.main()
/home/work/gowork/src/go-entry/main/main.go:49 +0x358

==================
WARNING: DATA RACE
Read at 0x00c4203a20a0 by goroutine 15:
github.com/jpillora/overseer.(*master).handleSignal()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:135 +0x2e2
github.com/jpillora/overseer.(*master).setupSignalling.func1()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:113 +0xa1

Previous write at 0x00c4203a20a0 by main goroutine:
os/exec.Command()
/home/work/soft/go/src/os/exec/exec.go:133 +0x15b
github.com/jpillora/overseer.(*master).fork()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:347 +0x162
github.com/jpillora/overseer.(*master).forkLoop()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:339 +0x38
github.com/jpillora/overseer.(*master).run()
/home/work/gowork/src/github.com/jpillora/overseer/proc_master.go:65 +0x172
github.com/jpillora/overseer.runErr()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:162 +0x27f
github.com/jpillora/overseer.Run()
/home/work/gowork/src/github.com/jpillora/overseer/overseer.go:99 +0x9d
main.main_overseer()
/home/work/gowork/src/go-entry/main/main.go:59 +0x128
main.main()
/home/work/gowork/src/go-entry/main/main.go:49 +0x358

"access is denied" on Windows "move"

On Windows once the temp binary is downloaded and confirmed to be executable overseer attempts to replace the currently running file with the new one. There is windows specific code added for this case:

overseer/sys_windows.go

Lines 24 to 40 in 7279a36

func move(dst, src string) error {
os.MkdirAll(filepath.Dir(dst), 0755)
if err := os.Rename(src, dst); err == nil {
return nil
}
//HACK: we're shelling out to move because windows
//throws errors when crossing device boundaries.
// https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/move.mspx?mfr=true
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
R := func(s string) string { return replShellMeta.Replace(syscall.EscapeArg(s)) }
cmd := exec.Command("cmd", "/c", `move /y `+R(src)+` `+R(dst))
if b, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("%v: %q: %v", cmd.Args, bytes.TrimSpace(b), err)
}
return nil
}

Which renames the temp file to match the name of the current file and then shells out to cmd to run "move". But for me this is returning:

image

It doesn't really appear to be anything with overseer in particular because if I just run this command directly:

image

Same result.

I see this issue from around the time that windows support was added: #7. But it's unclear to me if this is the same thing because that error message seems to be different. Also, @tgulacsi did work to this sys_windows file after that issue was created. So that makes me think it was working at that point - but not sure.

Does anyone know if this is something specific to my system. Or is it as that issue describes and move just doesn't work right now? Do we need to rename the currently running file before doing the move and rename it back if the move fails?

@tgulacsi if you're available I would love to hear your thoughts.

Lots of output: [overseer master] proxy signal (urgent I/O condition)

It seems it always outputting those lines every few seconds, is there any meaning about this? or is it because my VPS using OpenVZ?

Mar 27 15:13:19 indo.x.com runSvc.sh[5645]: 2020/03/27 15:13:19 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:13:29 indo.x.com runSvc.sh[5645]: 2020/03/27 15:13:29 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:13:31 indo.x.com runSvc.sh[5645]: 2020/03/27 15:13:31 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:14:41 indo.x.com runSvc.sh[5645]: 2020/03/27 15:14:41 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:15:31 indo.x.com runSvc.sh[5645]: 2020/03/27 15:15:31 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:15:49 indo.x.com runSvc.sh[5645]: 2020/03/27 15:15:49 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:15:49 indo.x.com runSvc.sh[5645]: 2020/03/27 15:15:49 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:16:39 indo.x.com runSvc.sh[5645]: 2020/03/27 15:16:39 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:16:39 indo.x.com runSvc.sh[5645]: 2020/03/27 15:16:39 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:17:29 indo.x.com runSvc.sh[5645]: 2020/03/27 15:17:29 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:17:31 indo.x.com runSvc.sh[5645]: 2020/03/27 15:17:31 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:17:31 indo.x.com runSvc.sh[5645]: 2020/03/27 15:17:31 [overseer master] proxy signal (urgent I/O condition)
Mar 27 15:17:31 indo.x.com runSvc.sh[5645]: 2020/03/27 15:17:31 [overseer master] proxy signal (urgent I/O condition)

[Feature] Switch to unix sockets for signalling

Hello and thank you for a great tool :)

I've been looking around the code and trying to figure out what keeps this library/tool from being supported on windows, and as far as I can see the only thing problematic is the signaling, especially the SIGUSR1/2 that don't exist on windows. (Signals really don't exist on windows at all, but some actions are caught and signals emulated by my understanding..)

Since you're already using tcp sockets, wouldn't it be possible to to signaling over tcp between the processes?

It would be nice if you could list other areas where you think windows support would be problematic. I would really like to try making a branch with windows support :)

Regards
Bernt-Johan

add notice for users don't printf in func init()

if use a printf in func init(){}

This may cause the upgrade check to fail!

user will got sanity check failed

eg:
add

func init() {
	fmt.Println("hahhahahhahahahaaha")
}

to github.com/jpillora/overseer/example/main.go

bogon:example qiantao$ ./example.sh 
BUILT APP (1)
RUNNING APP
hahhahahhahahahaaha
2019/03/22 17:22:13 [overseer master] run
2019/03/22 17:22:13 [overseer master] setupSignalling
2019/03/22 17:22:13 [overseer master] checking for updates...
2019/03/22 17:22:13 [overseer master] no updates
2019/03/22 17:22:13 [overseer master] starting /Users/qiantao/work/GoTestSrc/src/github.com/jpillora/overseer/example/my_app
hahhahahhahahahaaha
2019/03/22 17:22:13 [overseer slave#1] run
2019/03/22 17:22:13 [overseer slave#1] slave wait sp.Config.RestartSignal
2019/03/22 17:22:13 [overseer slave#1] start program
app#1 start
app#1 BuildDate=20190322_172211
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) listening...
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 work
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 work
BUILT APP (2)
2019/03/22 17:22:18 [overseer master] streaming update...
2019/03/22 17:22:18 [overseer master] sanity check failed
2019/03/22 17:22:18 [overseer master] checking for updates...
app#1 work
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
2019/03/22 17:22:19 [overseer master] no updates
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 work
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=5000000000)
BUILT APP (3)
app#1 work
2019/03/22 17:22:23 [overseer master] streaming update...
2019/03/22 17:22:23 [overseer master] sanity check failed
2019/03/22 17:22:23 [overseer master] checking for updates...
2019/03/22 17:22:24 [overseer master] no updates
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 work
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=5000000000)
app#1 (45fb899a1abd7bd65c698fbcbc6525bade97932f) says hello (d=0)
app#1 work
2019/03/22 17:22:27 [overseer master] proxy signal (terminated)
2019/03/22 17:22:27 [overseer master] prog exited with -1

[Bug] Windows locks executing files

Hi,

It seems that something is wrong with the way it works under windows. When its trying to move a file:

The process cannot access the file because it is being used by another process

Any ideas how can i fix it?

graceful restart failed

My project is built on the Beego, Now I want to graceful restart my project with Overseer. When I send signal SIGUSR2 demo, has some wrong. The error messages as follows:

**2020/12/29 14:51:43 [overseer master] graceful restart triggered
2020/12/29 14:51:43 [overseer slave#1] graceful shutdown requested

2020/12/29 14:52:13 [overseer slave#1] timeout. forceful shutdown
2020/12/29 14:52:13 [overseer master] graceful timeout, forcing exit
2020/12/29 14:52:13 [overseer master] prog exited with -1
2020/12/29 14:52:13 [overseer master] starting /Users/demo**

My project code as follows:

`func main() {
overseer.Run(overseer.Config{
Program: prog,
Debug: true,
})
}

func prog(state overseer.State) {
beego.BConfig.RecoverPanic = true
beego.BConfig.Listen.Graceful = false
beego.Run()
}
`

Is there has some wrongs?
Thank you!

Can I not set Address And Addresses? My app isn't used to listen any port.

When I don't set both Address and Addresses, it has a mistake:

panic: runtime error: invalid memory address or nil pointer dereference
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x603e20]

goroutine 1 [running]:
net/http.(*onceCloseListener).close(0xc000092cf0)
	/usr/local/go/src/net/http/server.go:3479 +0x20
sync.(*Once).doSlow(0xc000100180?, 0xc0000c38d0?)
	/usr/local/go/src/sync/once.go:68 +0xc2
sync.(*Once).Do(...)
	/usr/local/go/src/sync/once.go:59
net/http.(*onceCloseListener).Close(0xc000092cf0?)
	/usr/local/go/src/net/http/server.go:3475 +0x4a
panic({0x640680, 0x84d140})
	/usr/local/go/src/runtime/panic.go:838 +0x207
net/http.(*onceCloseListener).Accept(0x6ee250?)
	<autogenerated>:1 +0x24
net/http.(*Server).Serve(0xc0001000e0, {0x0, 0x0})
	/usr/local/go/src/net/http/server.go:3039 +0x385
net/http.Serve(...)
	/usr/local/go/src/net/http/server.go:2543
main.prog({0x1, {0xc000020090, 0x28}, {0xc0a568efa87b5794, 0x4aa32, 0x85a2e0}, {0x0, 0x0}, {0x889d30, 0x0, ...}, ...})
	/home/lee/Projects/overseer/example/main.go:26 +0x1ad
github.com/jpillora/overseer.(*slave).run(0xc000100000)
	/home/lee/Projects/overseer/proc_slave.go:77 +0x315
github.com/jpillora/overseer.runErr(0xc0000fe000)
	/home/lee/Projects/overseer/overseer.go:162 +0x12b
github.com/jpillora/overseer.Run({0x0, 0x6a77a0, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x0, 0x0}, 0x0, ...})
	/home/lee/Projects/overseer/overseer.go:99 +0x99
main.main()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.