chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
// Options for web.
|
||||
type Options struct {
|
||||
Handler http.Handler
|
||||
|
||||
Logger logger.Logger
|
||||
|
||||
Service micro.Service
|
||||
|
||||
Registry registry.Registry
|
||||
|
||||
// Alternative Options
|
||||
Context context.Context
|
||||
|
||||
Action func(*cli.Context)
|
||||
Metadata map[string]string
|
||||
TLSConfig *tls.Config
|
||||
|
||||
Server *http.Server
|
||||
|
||||
// RegisterCheck runs a check function before registering the service
|
||||
RegisterCheck func(context.Context) error
|
||||
|
||||
Version string
|
||||
|
||||
// Static directory
|
||||
StaticDir string
|
||||
|
||||
Advertise string
|
||||
|
||||
Address string
|
||||
Name string
|
||||
Id string
|
||||
Flags []cli.Flag
|
||||
|
||||
BeforeStart []func() error
|
||||
BeforeStop []func() error
|
||||
AfterStart []func() error
|
||||
AfterStop []func() error
|
||||
|
||||
RegisterInterval time.Duration
|
||||
|
||||
RegisterTTL time.Duration
|
||||
|
||||
Secure bool
|
||||
|
||||
Signal bool
|
||||
}
|
||||
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{
|
||||
Name: DefaultName,
|
||||
Version: DefaultVersion,
|
||||
Id: DefaultId,
|
||||
Address: DefaultAddress,
|
||||
RegisterTTL: DefaultRegisterTTL,
|
||||
RegisterInterval: DefaultRegisterInterval,
|
||||
StaticDir: DefaultStaticDir,
|
||||
Service: micro.NewService(DefaultName),
|
||||
Context: context.TODO(),
|
||||
Signal: true,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.RegisterCheck == nil {
|
||||
opt.RegisterCheck = DefaultRegisterCheck
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name of Web.
|
||||
func Name(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
// Icon specifies an icon url to load in the UI.
|
||||
func Icon(ico string) Option {
|
||||
return func(o *Options) {
|
||||
if o.Metadata == nil {
|
||||
o.Metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
o.Metadata["icon"] = ico
|
||||
}
|
||||
}
|
||||
|
||||
// Id for Unique server id.
|
||||
func Id(id string) Option {
|
||||
return func(o *Options) {
|
||||
o.Id = id
|
||||
}
|
||||
}
|
||||
|
||||
// Version of the service.
|
||||
func Version(v string) Option {
|
||||
return func(o *Options) {
|
||||
o.Version = v
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata associated with the service.
|
||||
func Metadata(md map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
// Address to bind to - host:port.
|
||||
func Address(a string) Option {
|
||||
return func(o *Options) {
|
||||
o.Address = a
|
||||
}
|
||||
}
|
||||
|
||||
// Advertise The address to advertise for discovery - host:port.
|
||||
func Advertise(a string) Option {
|
||||
return func(o *Options) {
|
||||
o.Advertise = a
|
||||
}
|
||||
}
|
||||
|
||||
// Context specifies a context for the service.
|
||||
// Can be used to signal shutdown of the service.
|
||||
// Can be used for extra option values.
|
||||
func Context(ctx context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// Registry used for discovery.
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterTTL Register the service with a TTL.
|
||||
func RegisterTTL(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterTTL = t
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInterval Register the service with at interval.
|
||||
func RegisterInterval(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterInterval = t
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for custom handler.
|
||||
func Handler(h http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
// Server for custom Server.
|
||||
func Server(srv *http.Server) Option {
|
||||
return func(o *Options) {
|
||||
o.Server = srv
|
||||
}
|
||||
}
|
||||
|
||||
// MicroService sets the micro.Service used internally.
|
||||
func MicroService(s micro.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Service = s
|
||||
}
|
||||
}
|
||||
|
||||
// Flags sets the command flags.
|
||||
func Flags(flags ...cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, flags...)
|
||||
}
|
||||
}
|
||||
|
||||
// Action sets the command action.
|
||||
func Action(a func(*cli.Context)) Option {
|
||||
return func(o *Options) {
|
||||
o.Action = a
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeStart is executed before the server starts.
|
||||
func BeforeStart(fn func() error) Option {
|
||||
return func(o *Options) {
|
||||
o.BeforeStart = append(o.BeforeStart, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeStop is executed before the server stops.
|
||||
func BeforeStop(fn func() error) Option {
|
||||
return func(o *Options) {
|
||||
o.BeforeStop = append(o.BeforeStop, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// AfterStart is executed after server start.
|
||||
func AfterStart(fn func() error) Option {
|
||||
return func(o *Options) {
|
||||
o.AfterStart = append(o.AfterStart, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// AfterStop is executed after server stop.
|
||||
func AfterStop(fn func() error) Option {
|
||||
return func(o *Options) {
|
||||
o.AfterStop = append(o.AfterStop, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Secure Use secure communication.
|
||||
// If TLSConfig is not specified we use InsecureSkipVerify and generate a self signed cert.
|
||||
func Secure(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Secure = b
|
||||
}
|
||||
}
|
||||
|
||||
// TLSConfig to be used for the transport.
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
|
||||
// StaticDir sets the static file directory. This defaults to ./html.
|
||||
func StaticDir(d string) Option {
|
||||
return func(o *Options) {
|
||||
o.StaticDir = d
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCheck run func before registry service.
|
||||
func RegisterCheck(fn func(context.Context) error) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterCheck = fn
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSignal toggles automatic installation of the signal handler that
|
||||
// traps TERM, INT, and QUIT. Users of this feature to disable the signal
|
||||
// handler, should control liveness of the service through the context.
|
||||
func HandleSignal(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Signal = b
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underline logger.
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v6"
|
||||
maddr "go-micro.dev/v6/internal/util/addr"
|
||||
"go-micro.dev/v6/internal/util/backoff"
|
||||
mhttp "go-micro.dev/v6/internal/util/http"
|
||||
mnet "go-micro.dev/v6/internal/util/net"
|
||||
signalutil "go-micro.dev/v6/internal/util/signal"
|
||||
mls "go-micro.dev/v6/internal/util/tls"
|
||||
log "go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
mux *http.ServeMux
|
||||
srv *registry.Service
|
||||
|
||||
exit chan chan error
|
||||
ex chan bool
|
||||
opts Options
|
||||
|
||||
sync.RWMutex
|
||||
running bool
|
||||
static bool
|
||||
}
|
||||
|
||||
func newService(opts ...Option) Service {
|
||||
options := newOptions(opts...)
|
||||
s := &service{
|
||||
opts: options,
|
||||
mux: http.NewServeMux(),
|
||||
static: true,
|
||||
ex: make(chan bool),
|
||||
}
|
||||
s.srv = s.genSrv()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *service) genSrv() *registry.Service {
|
||||
var (
|
||||
host string
|
||||
port string
|
||||
err error
|
||||
)
|
||||
|
||||
logger := s.opts.Logger
|
||||
|
||||
// default host:port
|
||||
if len(s.opts.Address) > 0 {
|
||||
host, port, err = net.SplitHostPort(s.opts.Address)
|
||||
if err != nil {
|
||||
logger.Log(log.FatalLevel, err)
|
||||
}
|
||||
}
|
||||
|
||||
// check the advertise address first
|
||||
// if it exists then use it, otherwise
|
||||
// use the address
|
||||
if len(s.opts.Advertise) > 0 {
|
||||
host, port, err = net.SplitHostPort(s.opts.Advertise)
|
||||
if err != nil {
|
||||
logger.Log(log.FatalLevel, err)
|
||||
}
|
||||
}
|
||||
|
||||
addr, err := maddr.Extract(host)
|
||||
if err != nil {
|
||||
logger.Log(log.FatalLevel, err)
|
||||
}
|
||||
|
||||
if strings.Count(addr, ":") > 0 {
|
||||
addr = "[" + addr + "]"
|
||||
}
|
||||
|
||||
return ®istry.Service{
|
||||
Name: s.opts.Name,
|
||||
Version: s.opts.Version,
|
||||
Nodes: []*registry.Node{{
|
||||
Id: s.opts.Id,
|
||||
Address: net.JoinHostPort(addr, port),
|
||||
Metadata: s.opts.Metadata,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) run() {
|
||||
s.RLock()
|
||||
if s.opts.RegisterInterval <= time.Duration(0) {
|
||||
s.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
t := time.NewTicker(s.opts.RegisterInterval)
|
||||
s.RUnlock()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
_ = s.register()
|
||||
case <-s.ex:
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) register() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if s.srv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger := s.opts.Logger
|
||||
|
||||
// default to service registry
|
||||
r := s.opts.Service.Client().Options().Registry
|
||||
// switch to option if specified
|
||||
if s.opts.Registry != nil {
|
||||
r = s.opts.Registry
|
||||
}
|
||||
|
||||
// service node need modify, node address maybe changed
|
||||
srv := s.genSrv()
|
||||
srv.Endpoints = s.srv.Endpoints
|
||||
s.srv = srv
|
||||
|
||||
// use RegisterCheck func before register
|
||||
if err := s.opts.RegisterCheck(s.opts.Context); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s register check error: %s", s.opts.Name, s.opts.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var regErr error
|
||||
|
||||
// try three times if necessary
|
||||
for i := 0; i < 3; i++ {
|
||||
// attempt to register
|
||||
if err := r.Register(s.srv, registry.RegisterTTL(s.opts.RegisterTTL)); err != nil {
|
||||
// set the error
|
||||
regErr = err
|
||||
// backoff then retry
|
||||
time.Sleep(backoff.Do(i + 1))
|
||||
|
||||
continue
|
||||
}
|
||||
// success so nil error
|
||||
regErr = nil
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return regErr
|
||||
}
|
||||
|
||||
func (s *service) deregister() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if s.srv == nil {
|
||||
return nil
|
||||
}
|
||||
// default to service registry
|
||||
r := s.opts.Service.Client().Options().Registry
|
||||
// switch to option if specified
|
||||
if s.opts.Registry != nil {
|
||||
r = s.opts.Registry
|
||||
}
|
||||
|
||||
return r.Deregister(s.srv)
|
||||
}
|
||||
|
||||
func (s *service) start() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if s.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, fn := range s.opts.BeforeStart {
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := s.listen("tcp", s.opts.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := s.opts.Logger
|
||||
|
||||
s.opts.Address = listener.Addr().String()
|
||||
srv := s.genSrv()
|
||||
srv.Endpoints = s.srv.Endpoints
|
||||
s.srv = srv
|
||||
|
||||
var handler http.Handler
|
||||
|
||||
if s.opts.Handler != nil {
|
||||
handler = s.opts.Handler
|
||||
} else {
|
||||
handler = s.mux
|
||||
var r sync.Once
|
||||
|
||||
// register the html dir
|
||||
r.Do(func() {
|
||||
// static dir
|
||||
static := s.opts.StaticDir
|
||||
if s.opts.StaticDir[0] != '/' {
|
||||
dir, _ := os.Getwd()
|
||||
static = filepath.Join(dir, static)
|
||||
}
|
||||
|
||||
// set static if no / handler is registered
|
||||
if s.static {
|
||||
_, err := os.Stat(static)
|
||||
if err == nil {
|
||||
logger.Logf(log.InfoLevel, "Enabling static file serving from %s", static)
|
||||
s.mux.Handle("/", http.FileServer(http.Dir(static)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var httpSrv *http.Server
|
||||
if s.opts.Server != nil {
|
||||
httpSrv = s.opts.Server
|
||||
} else {
|
||||
httpSrv = &http.Server{}
|
||||
}
|
||||
|
||||
httpSrv.Handler = handler
|
||||
|
||||
go func() { _ = httpSrv.Serve(listener) }()
|
||||
|
||||
for _, fn := range s.opts.AfterStart {
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.exit = make(chan chan error, 1)
|
||||
s.running = true
|
||||
|
||||
go func() {
|
||||
ch := <-s.exit
|
||||
ch <- listener.Close()
|
||||
}()
|
||||
|
||||
logger.Logf(log.InfoLevel, "Listening on %v", listener.Addr().String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) stop() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if !s.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, fn := range s.opts.BeforeStop {
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ch := make(chan error, 1)
|
||||
s.exit <- ch
|
||||
s.running = false
|
||||
|
||||
s.opts.Logger.Log(log.InfoLevel, "Stopping")
|
||||
|
||||
for _, fn := range s.opts.AfterStop {
|
||||
if err := fn(); err != nil {
|
||||
if chErr := <-ch; chErr != nil {
|
||||
return chErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (s *service) Client() *http.Client {
|
||||
rt := mhttp.NewRoundTripper(
|
||||
mhttp.WithRegistry(s.opts.Registry),
|
||||
)
|
||||
return &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Handle(pattern string, handler http.Handler) {
|
||||
var seen bool
|
||||
s.RLock()
|
||||
for _, ep := range s.srv.Endpoints {
|
||||
if ep.Name == pattern {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
s.RUnlock()
|
||||
|
||||
// if its unseen then add an endpoint
|
||||
if !seen {
|
||||
s.Lock()
|
||||
s.srv.Endpoints = append(s.srv.Endpoints, ®istry.Endpoint{
|
||||
Name: pattern,
|
||||
})
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// disable static serving
|
||||
if pattern == "/" {
|
||||
s.Lock()
|
||||
s.static = false
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// register the handler
|
||||
s.mux.Handle(pattern, handler)
|
||||
}
|
||||
|
||||
func (s *service) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
|
||||
var seen bool
|
||||
|
||||
s.RLock()
|
||||
for _, ep := range s.srv.Endpoints {
|
||||
if ep.Name == pattern {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
s.RUnlock()
|
||||
|
||||
if !seen {
|
||||
s.Lock()
|
||||
s.srv.Endpoints = append(s.srv.Endpoints, ®istry.Endpoint{
|
||||
Name: pattern,
|
||||
})
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// disable static serving
|
||||
if pattern == "/" {
|
||||
s.Lock()
|
||||
s.static = false
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
s.mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
func (s *service) Init(opts ...Option) error {
|
||||
s.Lock()
|
||||
|
||||
for _, o := range opts {
|
||||
o(&s.opts)
|
||||
}
|
||||
|
||||
serviceOpts := []micro.Option{}
|
||||
|
||||
if len(s.opts.Flags) > 0 {
|
||||
serviceOpts = append(serviceOpts, micro.Flags(s.opts.Flags...))
|
||||
}
|
||||
|
||||
if s.opts.Registry != nil {
|
||||
serviceOpts = append(serviceOpts, micro.Registry(s.opts.Registry))
|
||||
}
|
||||
|
||||
s.Unlock()
|
||||
|
||||
serviceOpts = append(serviceOpts, micro.Action(func(ctx *cli.Context) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if ttl := ctx.Int("register_ttl"); ttl > 0 {
|
||||
s.opts.RegisterTTL = time.Duration(ttl) * time.Second
|
||||
}
|
||||
|
||||
if interval := ctx.Int("register_interval"); interval > 0 {
|
||||
s.opts.RegisterInterval = time.Duration(interval) * time.Second
|
||||
}
|
||||
|
||||
if name := ctx.String("server_name"); len(name) > 0 {
|
||||
s.opts.Name = name
|
||||
}
|
||||
|
||||
if ver := ctx.String("server_version"); len(ver) > 0 {
|
||||
s.opts.Version = ver
|
||||
}
|
||||
|
||||
if id := ctx.String("server_id"); len(id) > 0 {
|
||||
s.opts.Id = id
|
||||
}
|
||||
|
||||
if addr := ctx.String("server_address"); len(addr) > 0 {
|
||||
s.opts.Address = addr
|
||||
}
|
||||
|
||||
if adv := ctx.String("server_advertise"); len(adv) > 0 {
|
||||
s.opts.Advertise = adv
|
||||
}
|
||||
|
||||
if s.opts.Action != nil {
|
||||
s.opts.Action(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
s.RLock()
|
||||
// pass in own name and version
|
||||
if s.opts.Service.Name() == "" {
|
||||
serviceOpts = append(serviceOpts, micro.Name(s.opts.Name))
|
||||
}
|
||||
|
||||
serviceOpts = append(serviceOpts, micro.Version(s.opts.Version))
|
||||
|
||||
s.RUnlock()
|
||||
|
||||
s.opts.Service.Init(serviceOpts...)
|
||||
|
||||
s.Lock()
|
||||
srv := s.genSrv()
|
||||
srv.Endpoints = s.srv.Endpoints
|
||||
s.srv = srv
|
||||
s.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) Start() error {
|
||||
if err := s.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.register(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start reg loop
|
||||
go s.run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) Stop() error {
|
||||
// exit reg loop
|
||||
close(s.ex)
|
||||
|
||||
if err := s.deregister(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.stop()
|
||||
}
|
||||
|
||||
func (s *service) Run() error {
|
||||
if err := s.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := s.opts.Logger
|
||||
// start the profiler
|
||||
if s.opts.Service.Options().Profile != nil {
|
||||
// to view mutex contention
|
||||
runtime.SetMutexProfileFraction(5)
|
||||
// to view blocking profile
|
||||
runtime.SetBlockProfileRate(1)
|
||||
|
||||
if err := s.opts.Service.Options().Profile.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := s.opts.Service.Options().Profile.Stop(); err != nil {
|
||||
logger.Log(log.ErrorLevel, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := s.register(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start reg loop
|
||||
go s.run()
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
if s.opts.Signal {
|
||||
signal.Notify(ch, signalutil.Shutdown()...)
|
||||
}
|
||||
|
||||
select {
|
||||
// wait on kill signal
|
||||
case sig := <-ch:
|
||||
logger.Logf(log.InfoLevel, "Received signal %s", sig)
|
||||
// wait on context cancel
|
||||
case <-s.opts.Context.Done():
|
||||
logger.Log(log.InfoLevel, "Received context shutdown")
|
||||
}
|
||||
|
||||
// exit reg loop
|
||||
close(s.ex)
|
||||
|
||||
if err := s.deregister(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.stop()
|
||||
}
|
||||
|
||||
// Options returns the options for the given service.
|
||||
func (s *service) Options() Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *service) listen(network, addr string) (net.Listener, error) {
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
// TODO: support use of listen options
|
||||
if s.opts.Secure || s.opts.TLSConfig != nil {
|
||||
config := s.opts.TLSConfig
|
||||
|
||||
fn := func(addr string) (net.Listener, error) {
|
||||
if config == nil {
|
||||
hosts := []string{addr}
|
||||
|
||||
// check if its a valid host:port
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
if len(host) == 0 {
|
||||
hosts = maddr.IPs()
|
||||
} else {
|
||||
hosts = []string{host}
|
||||
}
|
||||
}
|
||||
|
||||
// generate a certificate
|
||||
cert, err := mls.Certificate(hosts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
}
|
||||
|
||||
return tls.Listen(network, addr, config)
|
||||
}
|
||||
|
||||
listener, err = mnet.Listen(addr, fn)
|
||||
} else {
|
||||
fn := func(addr string) (net.Listener, error) {
|
||||
return net.Listen(network, addr)
|
||||
}
|
||||
|
||||
listener, err = mnet.Listen(addr, fn)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listener, nil
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
var (
|
||||
beforeStartCalled bool
|
||||
afterStartCalled bool
|
||||
beforeStopCalled bool
|
||||
afterStopCalled bool
|
||||
str = `<html><body><h1>Hello World</h1></body></html>`
|
||||
fn = func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, str) }
|
||||
reg = registry.NewMemoryRegistry()
|
||||
)
|
||||
|
||||
beforeStart := func() error {
|
||||
beforeStartCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
afterStart := func() error {
|
||||
afterStartCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
beforeStop := func() error {
|
||||
beforeStopCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
afterStop := func() error {
|
||||
afterStopCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
service := NewService(
|
||||
Name("go.micro.web.test"),
|
||||
Registry(reg),
|
||||
BeforeStart(beforeStart),
|
||||
AfterStart(afterStart),
|
||||
BeforeStop(beforeStop),
|
||||
AfterStop(afterStop),
|
||||
)
|
||||
|
||||
service.HandleFunc("/", fn)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- service.Run()
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
var s []*registry.Service
|
||||
|
||||
eventually(func() bool {
|
||||
var err error
|
||||
s, err = reg.GetService("go.micro.web.test")
|
||||
return err == nil
|
||||
}, t.Fatal)
|
||||
|
||||
if have, want := len(s), 1; have != want {
|
||||
t.Fatalf("Expected %d but got %d services", want, have)
|
||||
}
|
||||
|
||||
rsp, err := http.Get(fmt.Sprintf("http://%s", s[0].Nodes[0].Address))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != str {
|
||||
t.Errorf("Expected %s got %s", str, string(b))
|
||||
}
|
||||
|
||||
callbackTests := []struct {
|
||||
subject string
|
||||
have interface{}
|
||||
}{
|
||||
{"beforeStartCalled", beforeStartCalled},
|
||||
{"afterStartCalled", afterStartCalled},
|
||||
}
|
||||
|
||||
for _, tt := range callbackTests {
|
||||
if tt.have != true {
|
||||
t.Errorf("unexpected %s: want true, have false", tt.subject)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("service.Run():%v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Logf("service.Run() survived a client request without an error")
|
||||
}
|
||||
}
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGTERM)
|
||||
p, _ := os.FindProcess(os.Getpid())
|
||||
p.Signal(syscall.SIGTERM)
|
||||
|
||||
<-ch
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("service.Run():%v", err)
|
||||
} else {
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Log("service.Run() nil return on syscall.SIGTERM")
|
||||
}
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Logf("service.Run() survived a client request without an error")
|
||||
}
|
||||
}
|
||||
|
||||
eventually(func() bool {
|
||||
_, err := reg.GetService("go.micro.web.test")
|
||||
return err == registry.ErrNotFound
|
||||
}, t.Error)
|
||||
|
||||
callbackTests = []struct {
|
||||
subject string
|
||||
have interface{}
|
||||
}{
|
||||
{"beforeStopCalled", beforeStopCalled},
|
||||
{"afterStopCalled", afterStopCalled},
|
||||
}
|
||||
|
||||
for _, tt := range callbackTests {
|
||||
if tt.have != true {
|
||||
t.Errorf("unexpected %s: want true, have false", tt.subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
var (
|
||||
name = "service-name"
|
||||
id = "service-id"
|
||||
version = "service-version"
|
||||
address = "service-addr:8080"
|
||||
advertise = "service-adv:8080"
|
||||
reg = registry.NewMemoryRegistry()
|
||||
registerTTL = 123 * time.Second
|
||||
registerInterval = 456 * time.Second
|
||||
handler = http.NewServeMux()
|
||||
metadata = map[string]string{"key": "val"}
|
||||
secure = true
|
||||
)
|
||||
|
||||
service := NewService(
|
||||
Name(name),
|
||||
Id(id),
|
||||
Version(version),
|
||||
Address(address),
|
||||
Advertise(advertise),
|
||||
Registry(reg),
|
||||
RegisterTTL(registerTTL),
|
||||
RegisterInterval(registerInterval),
|
||||
Handler(handler),
|
||||
Metadata(metadata),
|
||||
Secure(secure),
|
||||
)
|
||||
|
||||
opts := service.Options()
|
||||
|
||||
tests := []struct {
|
||||
subject string
|
||||
want interface{}
|
||||
have interface{}
|
||||
}{
|
||||
{"name", name, opts.Name},
|
||||
{"version", version, opts.Version},
|
||||
{"id", id, opts.Id},
|
||||
{"address", address, opts.Address},
|
||||
{"advertise", advertise, opts.Advertise},
|
||||
{"registry", reg, opts.Registry},
|
||||
{"registerTTL", registerTTL, opts.RegisterTTL},
|
||||
{"registerInterval", registerInterval, opts.RegisterInterval},
|
||||
{"handler", handler, opts.Handler},
|
||||
{"metadata", metadata["key"], opts.Metadata["key"]},
|
||||
{"secure", secure, opts.Secure},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
if tc.want != tc.have {
|
||||
t.Errorf("unexpected %s: want %v, have %v", tc.subject, tc.want, tc.have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func eventually(pass func() bool, fail func(...interface{})) {
|
||||
tick := time.NewTicker(10 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
|
||||
timeout := time.After(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
fail("timed out")
|
||||
return
|
||||
case <-tick.C:
|
||||
if pass() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLS(t *testing.T) {
|
||||
var (
|
||||
str = `<html><body><h1>Hello World</h1></body></html>`
|
||||
fn = func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, str) }
|
||||
secure = true
|
||||
reg = registry.NewMemoryRegistry()
|
||||
)
|
||||
|
||||
service := NewService(
|
||||
Name("go.micro.web.test"),
|
||||
Secure(secure),
|
||||
Registry(reg),
|
||||
)
|
||||
|
||||
service.HandleFunc("/", fn)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- service.Run()
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
var s []*registry.Service
|
||||
|
||||
eventually(func() bool {
|
||||
var err error
|
||||
s, err = reg.GetService("go.micro.web.test")
|
||||
return err == nil
|
||||
}, t.Fatal)
|
||||
|
||||
if have, want := len(s), 1; have != want {
|
||||
t.Fatalf("Expected %d but got %d services", want, have)
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
rsp, err := client.Get(fmt.Sprintf("https://%s", s[0].Nodes[0].Address))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != str {
|
||||
t.Errorf("Expected %s got %s", str, string(b))
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("service.Run():%v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) == 0 {
|
||||
t.Logf("service.Run() survived a client request without an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
// Package web provides a web service for go-micro
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/events"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
// SSEClient represents a connected SSE client
|
||||
type SSEClient struct {
|
||||
id string
|
||||
send chan []byte
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// SSEBroadcaster manages SSE connections and broadcasts events to connected clients
|
||||
type SSEBroadcaster struct {
|
||||
clients map[*SSEClient]struct{}
|
||||
register chan *SSEClient
|
||||
unregister chan *SSEClient
|
||||
broadcast chan []byte
|
||||
stream events.Stream
|
||||
topics []string
|
||||
logger log.Logger
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// SSEEvent represents an event to be sent to clients
|
||||
type SSEEvent struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// SSEOption is a function that configures the SSEBroadcaster
|
||||
type SSEOption func(*SSEBroadcaster)
|
||||
|
||||
// WithStream sets the events stream for the broadcaster
|
||||
func WithStream(stream events.Stream) SSEOption {
|
||||
return func(b *SSEBroadcaster) {
|
||||
b.stream = stream
|
||||
}
|
||||
}
|
||||
|
||||
// WithTopics sets the topics to subscribe to
|
||||
func WithTopics(topics ...string) SSEOption {
|
||||
return func(b *SSEBroadcaster) {
|
||||
b.topics = topics
|
||||
}
|
||||
}
|
||||
|
||||
// WithSSELogger sets the logger for the broadcaster
|
||||
func WithSSELogger(logger log.Logger) SSEOption {
|
||||
return func(b *SSEBroadcaster) {
|
||||
b.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// NewSSEBroadcaster creates a new SSE broadcaster
|
||||
func NewSSEBroadcaster(opts ...SSEOption) *SSEBroadcaster {
|
||||
b := &SSEBroadcaster{
|
||||
clients: make(map[*SSEClient]struct{}),
|
||||
register: make(chan *SSEClient),
|
||||
unregister: make(chan *SSEClient),
|
||||
broadcast: make(chan []byte, 256),
|
||||
logger: log.DefaultLogger,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Start begins the broadcaster's event loop and subscribes to configured topics
|
||||
func (b *SSEBroadcaster) Start() error {
|
||||
b.mu.Lock()
|
||||
if b.running {
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.running = true
|
||||
b.mu.Unlock()
|
||||
|
||||
// Start the main event loop
|
||||
go b.run()
|
||||
|
||||
// Subscribe to topics if stream is configured
|
||||
if b.stream != nil && len(b.topics) > 0 {
|
||||
for _, topic := range b.topics {
|
||||
if err := b.subscribeToTopic(topic); err != nil {
|
||||
b.logger.Logf(log.ErrorLevel, "Failed to subscribe to topic %s: %v", topic, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the broadcaster
|
||||
func (b *SSEBroadcaster) Stop() {
|
||||
b.mu.Lock()
|
||||
if !b.running {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
b.running = false
|
||||
b.mu.Unlock()
|
||||
|
||||
close(b.stopCh)
|
||||
}
|
||||
|
||||
func (b *SSEBroadcaster) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-b.register:
|
||||
b.mu.Lock()
|
||||
b.clients[client] = struct{}{}
|
||||
b.mu.Unlock()
|
||||
b.logger.Logf(log.DebugLevel, "SSE client connected: %s", client.id)
|
||||
|
||||
case client := <-b.unregister:
|
||||
b.mu.Lock()
|
||||
if _, ok := b.clients[client]; ok {
|
||||
delete(b.clients, client)
|
||||
close(client.send)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.logger.Logf(log.DebugLevel, "SSE client disconnected: %s", client.id)
|
||||
|
||||
case message := <-b.broadcast:
|
||||
b.mu.RLock()
|
||||
for client := range b.clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
// Client buffer full, skip
|
||||
}
|
||||
}
|
||||
b.mu.RUnlock()
|
||||
|
||||
case <-b.stopCh:
|
||||
b.mu.Lock()
|
||||
for client := range b.clients {
|
||||
close(client.send)
|
||||
delete(b.clients, client)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SSEBroadcaster) subscribeToTopic(topic string) error {
|
||||
eventChan, err := b.stream.Consume(topic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-eventChan:
|
||||
b.Broadcast(event.Payload)
|
||||
case <-b.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
b.logger.Logf(log.InfoLevel, "SSE broadcaster subscribed to topic: %s", topic)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Broadcast sends a message to all connected clients
|
||||
func (b *SSEBroadcaster) Broadcast(data []byte) {
|
||||
select {
|
||||
case b.broadcast <- data:
|
||||
default:
|
||||
b.logger.Log(log.WarnLevel, "SSE broadcast channel full, dropping message")
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastEvent sends a structured event to all connected clients
|
||||
func (b *SSEBroadcaster) BroadcastEvent(eventType string, data interface{}) error {
|
||||
event := SSEEvent{
|
||||
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Event: eventType,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Broadcast(jsonData)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastHTML sends raw HTML to clients (for htmx/datastar integration)
|
||||
func (b *SSEBroadcaster) BroadcastHTML(eventType string, html string) {
|
||||
// Format as SSE with event type for htmx sse-swap
|
||||
message := fmt.Sprintf("event: %s\ndata: %s\n\n", eventType, html)
|
||||
b.Broadcast([]byte(message))
|
||||
}
|
||||
|
||||
// ClientCount returns the number of connected clients
|
||||
func (b *SSEBroadcaster) ClientCount() int {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return len(b.clients)
|
||||
}
|
||||
|
||||
// Handler returns an http.HandlerFunc for SSE connections
|
||||
func (b *SSEBroadcaster) Handler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the client supports SSE
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "SSE not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set SSE headers
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||
|
||||
// Create client
|
||||
client := &SSEClient{
|
||||
id: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
send: make(chan []byte, 64),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Register client
|
||||
b.register <- client
|
||||
|
||||
// Ensure cleanup on disconnect
|
||||
defer func() {
|
||||
b.unregister <- client
|
||||
}()
|
||||
|
||||
// Send initial connection event
|
||||
fmt.Fprintf(w, "event: connected\ndata: {\"id\":\"%s\"}\n\n", client.id)
|
||||
flusher.Flush()
|
||||
|
||||
// Keep-alive ticker
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-client.send:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Check if message is already SSE formatted (contains "event:" or "data:")
|
||||
if len(message) > 0 && (message[0] == 'e' || message[0] == 'd') {
|
||||
w.Write(message)
|
||||
} else {
|
||||
fmt.Fprintf(w, "data: %s\n\n", message)
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
case <-ticker.C:
|
||||
// Send keep-alive comment
|
||||
fmt.Fprintf(w, ": keepalive\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GinHandler returns a handler compatible with Gin framework
|
||||
func (b *SSEBroadcaster) GinHandler() interface{} {
|
||||
return b.Handler()
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSSEBroadcaster_Basic(t *testing.T) {
|
||||
// Create broadcaster
|
||||
b := NewSSEBroadcaster()
|
||||
if err := b.Start(); err != nil {
|
||||
t.Fatalf("Failed to start broadcaster: %v", err)
|
||||
}
|
||||
defer b.Stop()
|
||||
|
||||
// Create test server
|
||||
server := httptest.NewServer(b.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Connect client
|
||||
resp, err := http.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check headers
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "text/event-stream" {
|
||||
t.Errorf("Expected Content-Type text/event-stream, got %s", ct)
|
||||
}
|
||||
|
||||
// Read initial connection event
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(line, "event: connected") {
|
||||
t.Errorf("Expected connected event, got: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSEBroadcaster_BroadcastEvent(t *testing.T) {
|
||||
b := NewSSEBroadcaster()
|
||||
if err := b.Start(); err != nil {
|
||||
t.Fatalf("Failed to start broadcaster: %v", err)
|
||||
}
|
||||
defer b.Stop()
|
||||
|
||||
server := httptest.NewServer(b.Handler())
|
||||
defer server.Close()
|
||||
|
||||
// Connect client
|
||||
resp, err := http.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Wait for client to register
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Broadcast an event
|
||||
testData := map[string]string{"message": "hello"}
|
||||
if err := b.BroadcastEvent("test", testData); err != nil {
|
||||
t.Fatalf("Failed to broadcast: %v", err)
|
||||
}
|
||||
|
||||
// Read and verify
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
|
||||
// Skip connection event
|
||||
for i := 0; i < 3; i++ {
|
||||
reader.ReadString('\n')
|
||||
}
|
||||
|
||||
// Read broadcast event
|
||||
line, _ := reader.ReadString('\n')
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
t.Errorf("Expected data line, got: %s", line)
|
||||
}
|
||||
|
||||
// Parse the data
|
||||
dataStr := strings.TrimPrefix(line, "data: ")
|
||||
dataStr = strings.TrimSpace(dataStr)
|
||||
|
||||
var event SSEEvent
|
||||
if err := json.Unmarshal([]byte(dataStr), &event); err != nil {
|
||||
t.Fatalf("Failed to parse event: %v", err)
|
||||
}
|
||||
|
||||
if event.Event != "test" {
|
||||
t.Errorf("Expected event type 'test', got '%s'", event.Event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSEBroadcaster_ClientCount(t *testing.T) {
|
||||
b := NewSSEBroadcaster()
|
||||
if err := b.Start(); err != nil {
|
||||
t.Fatalf("Failed to start broadcaster: %v", err)
|
||||
}
|
||||
defer b.Stop()
|
||||
|
||||
server := httptest.NewServer(b.Handler())
|
||||
defer server.Close()
|
||||
|
||||
if count := b.ClientCount(); count != 0 {
|
||||
t.Errorf("Expected 0 clients, got %d", count)
|
||||
}
|
||||
|
||||
// Connect a client
|
||||
resp, err := http.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
|
||||
// Wait for registration
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if count := b.ClientCount(); count != 1 {
|
||||
t.Errorf("Expected 1 client, got %d", count)
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
|
||||
// Wait for unregistration
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if count := b.ClientCount(); count != 0 {
|
||||
t.Errorf("Expected 0 clients after disconnect, got %d", count)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Package web provides web based micro services
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Service is a web service with service discovery built in.
|
||||
type Service interface {
|
||||
Client() *http.Client
|
||||
Init(opts ...Option) error
|
||||
Options() Options
|
||||
Handle(pattern string, handler http.Handler)
|
||||
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
|
||||
Start() error
|
||||
Stop() error
|
||||
Run() error
|
||||
}
|
||||
|
||||
// Option for web.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Web basic Defaults.
|
||||
var (
|
||||
// For serving.
|
||||
DefaultName = "go-web"
|
||||
DefaultVersion = "latest"
|
||||
DefaultId = uuid.New().String()
|
||||
DefaultAddress = ":0"
|
||||
|
||||
// for registration.
|
||||
DefaultRegisterTTL = time.Second * 90
|
||||
DefaultRegisterInterval = time.Second * 30
|
||||
|
||||
// static directory.
|
||||
DefaultStaticDir = "html"
|
||||
DefaultRegisterCheck = func(context.Context) error { return nil }
|
||||
)
|
||||
|
||||
// NewService returns a new web.Service.
|
||||
func NewService(opts ...Option) Service {
|
||||
return newService(opts...)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/web"
|
||||
)
|
||||
|
||||
func TestWeb(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
testFunc()
|
||||
}
|
||||
}
|
||||
|
||||
func testFunc() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*250)
|
||||
defer cancel()
|
||||
|
||||
service := micro.NewService("test", micro.Context(ctx),
|
||||
micro.HandleSignal(false),
|
||||
micro.Flags(
|
||||
&cli.StringFlag{
|
||||
Name: "test.timeout",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "test.v",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "test.run",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "test.testlogfile",
|
||||
},
|
||||
),
|
||||
)
|
||||
w := web.NewService(
|
||||
web.MicroService(service),
|
||||
web.Context(ctx),
|
||||
web.HandleSignal(false),
|
||||
)
|
||||
// s.Init()
|
||||
// w.Init()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := service.Run()
|
||||
if err != nil {
|
||||
logger.Logf(logger.ErrorLevel, "micro run error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := w.Run()
|
||||
if err != nil {
|
||||
logger.Logf(logger.ErrorLevel, "web run error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user