mirror of
https://github.com/sorenisanerd/gotty.git
synced 2024-11-22 12:24:25 +00:00
Credit braintree/manners
This commit is contained in:
parent
470621f39e
commit
b37f7973bd
13
Godeps/Godeps.json
generated
13
Godeps/Godeps.json
generated
@ -2,6 +2,11 @@
|
|||||||
"ImportPath": "github.com/yudai/gotty",
|
"ImportPath": "github.com/yudai/gotty",
|
||||||
"GoVersion": "go1.5",
|
"GoVersion": "go1.5",
|
||||||
"Deps": [
|
"Deps": [
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/braintree/manners",
|
||||||
|
"Comment": "0.4.0-6-g9e2a271",
|
||||||
|
"Rev": "9e2a2714de21eb092ead2ef56d8c7a60d7928819"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/codegangsta/cli",
|
"ImportPath": "github.com/codegangsta/cli",
|
||||||
"Comment": "1.2.0-139-g142e6cd",
|
"Comment": "1.2.0-139-g142e6cd",
|
||||||
@ -11,14 +16,6 @@
|
|||||||
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
||||||
"Rev": "d5cac425555ca5cf00694df246e04f05e6a55150"
|
"Rev": "d5cac425555ca5cf00694df246e04f05e6a55150"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"ImportPath": "github.com/gorilla/context",
|
|
||||||
"Rev": "1c83b3eabd45b6d76072b66b746c20815fb2872d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/gorilla/mux",
|
|
||||||
"Rev": "ee1815431e497d3850809578c93ab6705f1a19f7"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/gorilla/websocket",
|
"ImportPath": "github.com/gorilla/websocket",
|
||||||
"Rev": "b6ab76f1fe9803ee1d59e7e5b2a797c1fe897ce5"
|
"Rev": "b6ab76f1fe9803ee1d59e7e5b2a797c1fe897ce5"
|
||||||
|
19
Godeps/_workspace/src/github.com/braintree/manners/LICENSE
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/braintree/manners/LICENSE
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2014 Braintree, a division of PayPal, Inc.
|
||||||
|
|
||||||
|
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.
|
36
Godeps/_workspace/src/github.com/braintree/manners/README.md
generated
vendored
Normal file
36
Godeps/_workspace/src/github.com/braintree/manners/README.md
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Manners
|
||||||
|
|
||||||
|
A *polite* webserver for Go.
|
||||||
|
|
||||||
|
Manners allows you to shut your Go webserver down gracefully, without dropping any requests. It can act as a drop-in replacement for the standard library's http.ListenAndServe function:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
handler := MyHTTPHandler()
|
||||||
|
manners.ListenAndServe(":7000", handler)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, when you want to shut the server down:
|
||||||
|
|
||||||
|
```go
|
||||||
|
manners.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
(Note that this does not block until all the requests are finished. Rather, the call to manners.ListenAndServe will stop blocking when all the requests are finished.)
|
||||||
|
|
||||||
|
Manners ensures that all requests are served by incrementing a WaitGroup when a request comes in and decrementing it when the request finishes.
|
||||||
|
|
||||||
|
If your request handler spawns Goroutines that are not guaranteed to finish with the request, you can ensure they are also completed with the `StartRoutine` and `FinishRoutine` functions on the server.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
Manners does not correctly shut down long-lived keepalive connections when issued a shutdown command. Clients on an idle keepalive connection may see a connection reset error rather than a close. See https://github.com/braintree/manners/issues/13 for details.
|
||||||
|
|
||||||
|
### Compatability
|
||||||
|
|
||||||
|
Manners 0.3.0 and above uses standard library functionality introduced in Go 1.3.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
`go get github.com/braintree/manners`
|
118
Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go
generated
vendored
Normal file
118
Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package manners
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newServer() *GracefulServer {
|
||||||
|
return NewWithServer(new(http.Server))
|
||||||
|
}
|
||||||
|
|
||||||
|
// a simple step-controllable http client
|
||||||
|
type client struct {
|
||||||
|
tls bool
|
||||||
|
addr net.Addr
|
||||||
|
connected chan error
|
||||||
|
sendrequest chan bool
|
||||||
|
idle chan error
|
||||||
|
idlerelease chan bool
|
||||||
|
closed chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Run() {
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
conn, err := net.Dial(c.addr.Network(), c.addr.String())
|
||||||
|
if err != nil {
|
||||||
|
c.connected <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.tls {
|
||||||
|
conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
||||||
|
}
|
||||||
|
c.connected <- nil
|
||||||
|
for <-c.sendrequest {
|
||||||
|
_, err = conn.Write([]byte("GET / HTTP/1.1\nHost: localhost:8000\n\n"))
|
||||||
|
if err != nil {
|
||||||
|
c.idle <- err
|
||||||
|
}
|
||||||
|
// Read response; no content
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
for scanner.Scan() {
|
||||||
|
// our null handler doesn't send a body, so we know the request is
|
||||||
|
// done when we reach the blank line after the headers
|
||||||
|
if scanner.Text() == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.idle <- scanner.Err()
|
||||||
|
<-c.idlerelease
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
ioutil.ReadAll(conn)
|
||||||
|
c.closed <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient(addr net.Addr, tls bool) *client {
|
||||||
|
return &client{
|
||||||
|
addr: addr,
|
||||||
|
tls: tls,
|
||||||
|
connected: make(chan error),
|
||||||
|
sendrequest: make(chan bool),
|
||||||
|
idle: make(chan error),
|
||||||
|
idlerelease: make(chan bool),
|
||||||
|
closed: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a handler that returns 200 ok with no body
|
||||||
|
var nullHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
func startGenericServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState, runner func() error) (l net.Listener, errc chan error) {
|
||||||
|
server.Addr = "localhost:0"
|
||||||
|
server.Handler = nullHandler
|
||||||
|
if statechanged != nil {
|
||||||
|
// Wrap the ConnState handler with something that will notify
|
||||||
|
// the statechanged channel when a state change happens
|
||||||
|
server.ConnState = func(conn net.Conn, newState http.ConnState) {
|
||||||
|
statechanged <- newState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.up = make(chan net.Listener)
|
||||||
|
exitchan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
exitchan <- runner()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for server socket to be bound
|
||||||
|
select {
|
||||||
|
case l = <-server.up:
|
||||||
|
// all good
|
||||||
|
|
||||||
|
case err := <-exitchan:
|
||||||
|
// all bad
|
||||||
|
t.Fatal("Server failed to start", err)
|
||||||
|
}
|
||||||
|
return l, exitchan
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState) (
|
||||||
|
l net.Listener, errc chan error) {
|
||||||
|
return startGenericServer(t, server, statechanged, server.ListenAndServe)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTLSServer(t *testing.T, server *GracefulServer, certFile, keyFile string, statechanged chan http.ConnState) (l net.Listener, errc chan error) {
|
||||||
|
runner := func() error {
|
||||||
|
return server.ListenAndServeTLS(certFile, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return startGenericServer(t, server, statechanged, runner)
|
||||||
|
}
|
7
Godeps/_workspace/src/github.com/braintree/manners/interfaces.go
generated
vendored
Normal file
7
Godeps/_workspace/src/github.com/braintree/manners/interfaces.go
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package manners
|
||||||
|
|
||||||
|
type waitGroup interface {
|
||||||
|
Add(int)
|
||||||
|
Done()
|
||||||
|
Wait()
|
||||||
|
}
|
228
Godeps/_workspace/src/github.com/braintree/manners/server.go
generated
vendored
Normal file
228
Godeps/_workspace/src/github.com/braintree/manners/server.go
generated
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
Package manners provides a wrapper for a standard net/http server that
|
||||||
|
ensures all active HTTP client have completed their current request
|
||||||
|
before the server shuts down.
|
||||||
|
|
||||||
|
It can be used a drop-in replacement for the standard http package,
|
||||||
|
or can wrap a pre-configured Server.
|
||||||
|
|
||||||
|
eg.
|
||||||
|
|
||||||
|
http.Handle("/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("Hello\n"))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Fatal(manners.ListenAndServe(":8080", nil))
|
||||||
|
|
||||||
|
or for a customized server:
|
||||||
|
|
||||||
|
s := manners.NewWithServer(&http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: myHandler,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
})
|
||||||
|
log.Fatal(s.ListenAndServe())
|
||||||
|
|
||||||
|
The server will shut down cleanly when the Close() method is called:
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sigchan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigchan, os.Interrupt, os.Kill)
|
||||||
|
<-sigchan
|
||||||
|
log.Info("Shutting down...")
|
||||||
|
manners.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
http.Handle("/hello", myHandler)
|
||||||
|
log.Fatal(manners.ListenAndServe(":8080", nil))
|
||||||
|
*/
|
||||||
|
package manners
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A GracefulServer maintains a WaitGroup that counts how many in-flight
|
||||||
|
// requests the server is handling. When it receives a shutdown signal,
|
||||||
|
// it stops accepting new requests but does not actually shut down until
|
||||||
|
// all in-flight requests terminate.
|
||||||
|
//
|
||||||
|
// GracefulServer embeds the underlying net/http.Server making its non-override
|
||||||
|
// methods and properties avaiable.
|
||||||
|
//
|
||||||
|
// It must be initialized by calling NewWithServer.
|
||||||
|
type GracefulServer struct {
|
||||||
|
*http.Server
|
||||||
|
|
||||||
|
shutdown chan bool
|
||||||
|
wg waitGroup
|
||||||
|
|
||||||
|
lcsmu sync.RWMutex
|
||||||
|
lastConnState map[net.Conn]http.ConnState
|
||||||
|
|
||||||
|
up chan net.Listener // Only used by test code.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithServer wraps an existing http.Server object and returns a
|
||||||
|
// GracefulServer that supports all of the original Server operations.
|
||||||
|
func NewWithServer(s *http.Server) *GracefulServer {
|
||||||
|
return &GracefulServer{
|
||||||
|
Server: s,
|
||||||
|
shutdown: make(chan bool),
|
||||||
|
wg: new(sync.WaitGroup),
|
||||||
|
lastConnState: make(map[net.Conn]http.ConnState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the server from accepting new requets and begins shutting down.
|
||||||
|
// It returns true if it's the first time Close is called.
|
||||||
|
func (s *GracefulServer) Close() bool {
|
||||||
|
return <-s.shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe provides a graceful equivalent of net/http.Serve.ListenAndServe.
|
||||||
|
func (s *GracefulServer) ListenAndServe() error {
|
||||||
|
addr := s.Addr
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":http"
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Serve(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServeTLS provides a graceful equivalent of net/http.Serve.ListenAndServeTLS.
|
||||||
|
func (s *GracefulServer) ListenAndServeTLS(certFile, keyFile string) error {
|
||||||
|
// direct lift from net/http/server.go
|
||||||
|
addr := s.Addr
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":https"
|
||||||
|
}
|
||||||
|
config := &tls.Config{}
|
||||||
|
if s.TLSConfig != nil {
|
||||||
|
*config = *s.TLSConfig
|
||||||
|
}
|
||||||
|
if config.NextProtos == nil {
|
||||||
|
config.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
config.Certificates = make([]tls.Certificate, 1)
|
||||||
|
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Serve(tls.NewListener(ln, config))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve provides a graceful equivalent net/http.Server.Serve.
|
||||||
|
func (s *GracefulServer) Serve(listener net.Listener) error {
|
||||||
|
var closing int32
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.shutdown <- true
|
||||||
|
close(s.shutdown)
|
||||||
|
atomic.StoreInt32(&closing, 1)
|
||||||
|
s.Server.SetKeepAlivesEnabled(false)
|
||||||
|
listener.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
originalConnState := s.Server.ConnState
|
||||||
|
|
||||||
|
// s.ConnState is invoked by the net/http.Server every time a connectiion
|
||||||
|
// changes state. It keeps track of each connection's state over time,
|
||||||
|
// enabling manners to handle persisted connections correctly.
|
||||||
|
s.ConnState = func(conn net.Conn, newState http.ConnState) {
|
||||||
|
s.lcsmu.RLock()
|
||||||
|
lastConnState := s.lastConnState[conn]
|
||||||
|
s.lcsmu.RUnlock()
|
||||||
|
|
||||||
|
switch newState {
|
||||||
|
|
||||||
|
// New connection -> StateNew
|
||||||
|
case http.StateNew:
|
||||||
|
s.StartRoutine()
|
||||||
|
|
||||||
|
// (StateNew, StateIdle) -> StateActive
|
||||||
|
case http.StateActive:
|
||||||
|
// The connection transitioned from idle back to active
|
||||||
|
if lastConnState == http.StateIdle {
|
||||||
|
s.StartRoutine()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateActive -> StateIdle
|
||||||
|
// Immediately close newly idle connections; if not they may make
|
||||||
|
// one more request before SetKeepAliveEnabled(false) takes effect.
|
||||||
|
case http.StateIdle:
|
||||||
|
if atomic.LoadInt32(&closing) == 1 {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
s.FinishRoutine()
|
||||||
|
|
||||||
|
// (StateNew, StateActive, StateIdle) -> (StateClosed, StateHiJacked)
|
||||||
|
// If the connection was idle we do not need to decrement the counter.
|
||||||
|
case http.StateClosed, http.StateHijacked:
|
||||||
|
if lastConnState != http.StateIdle {
|
||||||
|
s.FinishRoutine()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lcsmu.Lock()
|
||||||
|
if newState == http.StateClosed || newState == http.StateHijacked {
|
||||||
|
delete(s.lastConnState, conn)
|
||||||
|
} else {
|
||||||
|
s.lastConnState[conn] = newState
|
||||||
|
}
|
||||||
|
s.lcsmu.Unlock()
|
||||||
|
|
||||||
|
if originalConnState != nil {
|
||||||
|
originalConnState(conn, newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hook to allow the server to notify others when it is ready to receive
|
||||||
|
// requests; only used by tests.
|
||||||
|
if s.up != nil {
|
||||||
|
s.up <- listener
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Server.Serve(listener)
|
||||||
|
|
||||||
|
// This block is reached when the server has received a shut down command
|
||||||
|
// or a real error happened.
|
||||||
|
if err == nil || atomic.LoadInt32(&closing) == 1 {
|
||||||
|
s.wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartRoutine increments the server's WaitGroup. Use this if a web request
|
||||||
|
// starts more goroutines and these goroutines are not guaranteed to finish
|
||||||
|
// before the request.
|
||||||
|
func (s *GracefulServer) StartRoutine() {
|
||||||
|
s.wg.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinishRoutine decrements the server's WaitGroup. Use this to complement
|
||||||
|
// StartRoutine().
|
||||||
|
func (s *GracefulServer) FinishRoutine() {
|
||||||
|
s.wg.Done()
|
||||||
|
}
|
243
Godeps/_workspace/src/github.com/braintree/manners/server_test.go
generated
vendored
Normal file
243
Godeps/_workspace/src/github.com/braintree/manners/server_test.go
generated
vendored
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package manners
|
||||||
|
|
||||||
|
import (
|
||||||
|
helpers "github.com/braintree/manners/test_helpers"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests that the server allows in-flight requests to complete
|
||||||
|
// before shutting down.
|
||||||
|
func TestGracefulness(t *testing.T) {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
server.wg = wg
|
||||||
|
statechanged := make(chan http.ConnState)
|
||||||
|
listener, exitchan := startServer(t, server, statechanged)
|
||||||
|
|
||||||
|
client := newClient(listener.Addr(), false)
|
||||||
|
client.Run()
|
||||||
|
|
||||||
|
// wait for client to connect, but don't let it send the request yet
|
||||||
|
if err := <-client.connected; err != nil {
|
||||||
|
t.Fatal("Client failed to connect to server", err)
|
||||||
|
}
|
||||||
|
// avoid a race between the client connection and the server accept
|
||||||
|
if state := <-statechanged; state != http.StateNew {
|
||||||
|
t.Fatal("Unexpected state", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
|
||||||
|
waiting := <-wg.WaitCalled
|
||||||
|
if waiting < 1 {
|
||||||
|
t.Errorf("Expected the waitgroup to equal 1 at shutdown; actually %d", waiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow the client to finish sending the request and make sure the server exits after
|
||||||
|
// (client will be in connected but idle state at that point)
|
||||||
|
client.sendrequest <- true
|
||||||
|
close(client.sendrequest)
|
||||||
|
if err := <-exitchan; err != nil {
|
||||||
|
t.Error("Unexpected error during shutdown", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the server begins to shut down when told to and does not accept
|
||||||
|
// new requests once shutdown has begun
|
||||||
|
func TestShutdown(t *testing.T) {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
server.wg = wg
|
||||||
|
statechanged := make(chan http.ConnState)
|
||||||
|
listener, exitchan := startServer(t, server, statechanged)
|
||||||
|
|
||||||
|
client1 := newClient(listener.Addr(), false)
|
||||||
|
client1.Run()
|
||||||
|
|
||||||
|
// wait for client1 to connect
|
||||||
|
if err := <-client1.connected; err != nil {
|
||||||
|
t.Fatal("Client failed to connect to server", err)
|
||||||
|
}
|
||||||
|
// avoid a race between the client connection and the server accept
|
||||||
|
if state := <-statechanged; state != http.StateNew {
|
||||||
|
t.Fatal("Unexpected state", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the shutdown; once it hits waitgroup.Wait()
|
||||||
|
// the listener should of been closed, though client1 is still connected
|
||||||
|
if server.Close() != true {
|
||||||
|
t.Fatal("first call to Close returned false")
|
||||||
|
}
|
||||||
|
if server.Close() != false {
|
||||||
|
t.Fatal("second call to Close returned true")
|
||||||
|
}
|
||||||
|
|
||||||
|
waiting := <-wg.WaitCalled
|
||||||
|
if waiting != 1 {
|
||||||
|
t.Errorf("Waitcount should be one, got %d", waiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// should get connection refused at this point
|
||||||
|
client2 := newClient(listener.Addr(), false)
|
||||||
|
client2.Run()
|
||||||
|
|
||||||
|
if err := <-client2.connected; err == nil {
|
||||||
|
t.Fatal("client2 connected when it should of received connection refused")
|
||||||
|
}
|
||||||
|
|
||||||
|
// let client1 finish so the server can exit
|
||||||
|
close(client1.sendrequest) // don't bother sending an actual request
|
||||||
|
|
||||||
|
<-exitchan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a connection is closed upon reaching an idle state if and only if the server
|
||||||
|
// is shutting down.
|
||||||
|
func TestCloseOnIdle(t *testing.T) {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
server.wg = wg
|
||||||
|
fl := helpers.NewListener()
|
||||||
|
runner := func() error {
|
||||||
|
return server.Serve(fl)
|
||||||
|
}
|
||||||
|
|
||||||
|
startGenericServer(t, server, nil, runner)
|
||||||
|
|
||||||
|
// Change to idle state while server is not closing; Close should not be called
|
||||||
|
conn := &helpers.Conn{}
|
||||||
|
server.ConnState(conn, http.StateIdle)
|
||||||
|
if conn.CloseCalled {
|
||||||
|
t.Error("Close was called unexpected")
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
|
||||||
|
// wait until the server calls Close() on the listener
|
||||||
|
// by that point the atomic closing variable will have been updated, avoiding a race.
|
||||||
|
<-fl.CloseCalled
|
||||||
|
|
||||||
|
conn = &helpers.Conn{}
|
||||||
|
server.ConnState(conn, http.StateIdle)
|
||||||
|
if !conn.CloseCalled {
|
||||||
|
t.Error("Close was not called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForState(t *testing.T, waiter chan http.ConnState, state http.ConnState, errmsg string) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ns := <-waiter:
|
||||||
|
if ns == state {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal(errmsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a request moving from active->idle->active using an actual
|
||||||
|
// network connection still results in a corect shutdown
|
||||||
|
func TestStateTransitionActiveIdleActive(t *testing.T) {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
statechanged := make(chan http.ConnState)
|
||||||
|
server.wg = wg
|
||||||
|
listener, exitchan := startServer(t, server, statechanged)
|
||||||
|
|
||||||
|
client := newClient(listener.Addr(), false)
|
||||||
|
client.Run()
|
||||||
|
|
||||||
|
// wait for client to connect, but don't let it send the request
|
||||||
|
if err := <-client.connected; err != nil {
|
||||||
|
t.Fatal("Client failed to connect to server", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
client.sendrequest <- true
|
||||||
|
waitForState(t, statechanged, http.StateActive, "Client failed to reach active state")
|
||||||
|
<-client.idle
|
||||||
|
client.idlerelease <- true
|
||||||
|
waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// client is now in an idle state
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
waiting := <-wg.WaitCalled
|
||||||
|
if waiting != 0 {
|
||||||
|
t.Errorf("Waitcount should be zero, got %d", waiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-exitchan; err != nil {
|
||||||
|
t.Error("Unexpected error during shutdown", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test state transitions from new->active->-idle->closed using an actual
|
||||||
|
// network connection and make sure the waitgroup count is correct at the end.
|
||||||
|
func TestStateTransitionActiveIdleClosed(t *testing.T) {
|
||||||
|
var (
|
||||||
|
listener net.Listener
|
||||||
|
exitchan chan error
|
||||||
|
)
|
||||||
|
|
||||||
|
keyFile, err1 := helpers.NewTempFile(helpers.Key)
|
||||||
|
certFile, err2 := helpers.NewTempFile(helpers.Cert)
|
||||||
|
defer keyFile.Unlink()
|
||||||
|
defer certFile.Unlink()
|
||||||
|
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
t.Fatal("Failed to create temporary files", err1, err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, withTLS := range []bool{false, true} {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
statechanged := make(chan http.ConnState)
|
||||||
|
server.wg = wg
|
||||||
|
if withTLS {
|
||||||
|
listener, exitchan = startTLSServer(t, server, certFile.Name(), keyFile.Name(), statechanged)
|
||||||
|
} else {
|
||||||
|
listener, exitchan = startServer(t, server, statechanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newClient(listener.Addr(), withTLS)
|
||||||
|
client.Run()
|
||||||
|
|
||||||
|
// wait for client to connect, but don't let it send the request
|
||||||
|
if err := <-client.connected; err != nil {
|
||||||
|
t.Fatal("Client failed to connect to server", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.sendrequest <- true
|
||||||
|
waitForState(t, statechanged, http.StateActive, "Client failed to reach active state")
|
||||||
|
|
||||||
|
err := <-client.idle
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tls=%t unexpected error from client %s", withTLS, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.idlerelease <- true
|
||||||
|
waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state")
|
||||||
|
|
||||||
|
// client is now in an idle state
|
||||||
|
close(client.sendrequest)
|
||||||
|
<-client.closed
|
||||||
|
waitForState(t, statechanged, http.StateClosed, "Client failed to reach closed state")
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
waiting := <-wg.WaitCalled
|
||||||
|
if waiting != 0 {
|
||||||
|
t.Errorf("Waitcount should be zero, got %d", waiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-exitchan; err != nil {
|
||||||
|
t.Error("Unexpected error during shutdown", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
Godeps/_workspace/src/github.com/braintree/manners/static.go
generated
vendored
Normal file
35
Godeps/_workspace/src/github.com/braintree/manners/static.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package manners
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultServer *GracefulServer
|
||||||
|
|
||||||
|
// ListenAndServe provides a graceful version of the function provided by the
|
||||||
|
// net/http package. Call Close() to stop the server.
|
||||||
|
func ListenAndServe(addr string, handler http.Handler) error {
|
||||||
|
defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler})
|
||||||
|
return defaultServer.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServeTLS provides a graceful version of the function provided by the
|
||||||
|
// net/http package. Call Close() to stop the server.
|
||||||
|
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error {
|
||||||
|
defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler})
|
||||||
|
return defaultServer.ListenAndServeTLS(certFile, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve provides a graceful version of the function provided by the net/http
|
||||||
|
// package. Call Close() to stop the server.
|
||||||
|
func Serve(l net.Listener, handler http.Handler) error {
|
||||||
|
defaultServer = NewWithServer(&http.Server{Handler: handler})
|
||||||
|
return defaultServer.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuts down the default server used by ListenAndServe, ListenAndServeTLS and
|
||||||
|
// Serve. It returns true if it's the first time Close is called.
|
||||||
|
func Close() bool {
|
||||||
|
return defaultServer.Close()
|
||||||
|
}
|
29
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go
generated
vendored
Normal file
29
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package test_helpers
|
||||||
|
|
||||||
|
// A PEM-encoded TLS cert with SAN IPs "127.0.0.1" and "[::1]", expiring at the
|
||||||
|
// last second of 2049 (the end of ASN.1 time).
|
||||||
|
|
||||||
|
// generated from src/pkg/crypto/tls:
|
||||||
|
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||||
|
var (
|
||||||
|
Cert = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
|
||||||
|
bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj
|
||||||
|
bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa
|
||||||
|
IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA
|
||||||
|
AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
|
||||||
|
EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk
|
||||||
|
Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA==
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
Key = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0
|
||||||
|
0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV
|
||||||
|
NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d
|
||||||
|
AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW
|
||||||
|
MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD
|
||||||
|
EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA
|
||||||
|
1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE=
|
||||||
|
-----END RSA PRIVATE KEY-----`)
|
||||||
|
)
|
13
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go
generated
vendored
Normal file
13
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package test_helpers
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
CloseCalled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
c.CloseCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
34
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go
generated
vendored
Normal file
34
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package test_helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Listener struct {
|
||||||
|
AcceptRelease chan bool
|
||||||
|
CloseCalled chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListener() *Listener {
|
||||||
|
return &Listener{
|
||||||
|
make(chan bool, 1),
|
||||||
|
make(chan bool, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Addr() net.Addr {
|
||||||
|
addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Close() error {
|
||||||
|
l.CloseCalled <- true
|
||||||
|
l.AcceptRelease <- true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Accept() (net.Conn, error) {
|
||||||
|
<-l.AcceptRelease
|
||||||
|
return nil, errors.New("connection closed")
|
||||||
|
}
|
27
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go
generated
vendored
Normal file
27
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package test_helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TempFile struct {
|
||||||
|
*os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTempFile(content []byte) (*TempFile, error) {
|
||||||
|
f, err := ioutil.TempFile("", "graceful-test")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Write(content)
|
||||||
|
return &TempFile{f}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *TempFile) Unlink() {
|
||||||
|
if tf.File != nil {
|
||||||
|
os.Remove(tf.Name())
|
||||||
|
tf.File = nil
|
||||||
|
}
|
||||||
|
}
|
33
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go
generated
vendored
Normal file
33
Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package test_helpers
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type WaitGroup struct {
|
||||||
|
sync.Mutex
|
||||||
|
Count int
|
||||||
|
WaitCalled chan int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWaitGroup() *WaitGroup {
|
||||||
|
return &WaitGroup{
|
||||||
|
WaitCalled: make(chan int, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wg *WaitGroup) Add(delta int) {
|
||||||
|
wg.Lock()
|
||||||
|
wg.Count++
|
||||||
|
wg.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wg *WaitGroup) Done() {
|
||||||
|
wg.Lock()
|
||||||
|
wg.Count--
|
||||||
|
wg.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wg *WaitGroup) Wait() {
|
||||||
|
wg.Lock()
|
||||||
|
wg.WaitCalled <- wg.Count
|
||||||
|
wg.Unlock()
|
||||||
|
}
|
54
Godeps/_workspace/src/github.com/braintree/manners/transition_test.go
generated
vendored
Normal file
54
Godeps/_workspace/src/github.com/braintree/manners/transition_test.go
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package manners
|
||||||
|
|
||||||
|
import (
|
||||||
|
helpers "github.com/braintree/manners/test_helpers"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStateTransitions(t *testing.T) {
|
||||||
|
tests := []transitionTest{
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive}, 1},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateClosed}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateClosed}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateHijacked}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive}, 1},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateClosed}, 0},
|
||||||
|
transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle, http.StateClosed}, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
testStateTransition(t, test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type transitionTest struct {
|
||||||
|
states []http.ConnState
|
||||||
|
expectedWgCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStateTransition(t *testing.T, test transitionTest) {
|
||||||
|
server := newServer()
|
||||||
|
wg := helpers.NewWaitGroup()
|
||||||
|
server.wg = wg
|
||||||
|
startServer(t, server, nil)
|
||||||
|
|
||||||
|
conn := &helpers.Conn{}
|
||||||
|
for _, newState := range test.states {
|
||||||
|
server.ConnState(conn, newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
waiting := <-wg.WaitCalled
|
||||||
|
if waiting != test.expectedWgCount {
|
||||||
|
names := make([]string, len(test.states))
|
||||||
|
for i, s := range test.states {
|
||||||
|
names[i] = s.String()
|
||||||
|
}
|
||||||
|
transitions := strings.Join(names, " -> ")
|
||||||
|
t.Errorf("%s - Waitcount should be %d, got %d", transitions, test.expectedWgCount, waiting)
|
||||||
|
}
|
||||||
|
}
|
23
LICENSE
23
LICENSE
@ -220,3 +220,26 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
# braintree/manners
|
||||||
|
|
||||||
|
Copyright (c) 2014 Braintree, a division of PayPal, Inc.
|
||||||
|
|
||||||
|
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.
|
Loading…
Reference in New Issue
Block a user