Files
livekit/cmd/server/main.go

334 lines
8.0 KiB
Go

// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"math/rand"
"net"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"syscall"
"time"
"github.com/urfave/cli/v3"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/tracer/jaeger"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/service"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/version"
)
var baseFlags = []cli.Flag{
&cli.StringSliceFlag{
Name: "bind",
Usage: "IP address to listen on, use flag multiple times to specify multiple addresses",
},
&cli.StringFlag{
Name: "config",
Usage: "path to LiveKit config file",
},
&cli.StringFlag{
Name: "config-body",
Usage: "LiveKit config in YAML, typically passed in as an environment var in a container",
Sources: cli.EnvVars("LIVEKIT_CONFIG"),
},
&cli.StringFlag{
Name: "key-file",
Usage: "path to file that contains API keys/secrets",
},
&cli.StringFlag{
Name: "keys",
Usage: "api keys (key: secret\\n)",
Sources: cli.EnvVars("LIVEKIT_KEYS"),
},
&cli.StringFlag{
Name: "region",
Usage: "region of the current node. Used by regionaware node selector",
Sources: cli.EnvVars("LIVEKIT_REGION"),
},
&cli.StringFlag{
Name: "node-ip",
Usage: "IP address of the current node, used to advertise to clients. Automatically determined by default",
Sources: cli.EnvVars("NODE_IP"),
},
&cli.StringFlag{
Name: "udp-port",
Usage: "UDP port(s) to use for WebRTC traffic",
Sources: cli.EnvVars("UDP_PORT"),
},
&cli.StringFlag{
Name: "redis-host",
Usage: "host (incl. port) to redis server",
Sources: cli.EnvVars("REDIS_HOST"),
},
&cli.StringFlag{
Name: "redis-password",
Usage: "password to redis",
Sources: cli.EnvVars("REDIS_PASSWORD"),
},
&cli.StringFlag{
Name: "turn-cert",
Usage: "tls cert file for TURN server",
Sources: cli.EnvVars("LIVEKIT_TURN_CERT"),
},
&cli.StringFlag{
Name: "turn-key",
Usage: "tls key file for TURN server",
Sources: cli.EnvVars("LIVEKIT_TURN_KEY"),
},
&cli.StringFlag{
Name: "cpuprofile",
Usage: "write CPU profile to `file`",
},
&cli.StringFlag{
Name: "memprofile",
Usage: "write memory profile to `file`",
},
&cli.BoolFlag{
Name: "dev",
Usage: "sets log-level to debug, console formatter, and /debug/pprof. insecure for production",
},
&cli.BoolFlag{
Name: "disable-strict-config",
Usage: "disables strict config parsing",
Hidden: true,
},
}
func init() {
rand.Seed(time.Now().Unix())
}
func main() {
defer func() {
if rtc.Recover(logger.GetLogger()) != nil {
os.Exit(1)
}
}()
generatedFlags, err := config.GenerateCLIFlags(baseFlags, true)
if err != nil {
fmt.Println(err)
}
cmd := &cli.Command{
Name: "livekit-server",
Usage: "High performance WebRTC server",
Description: "run without subcommands to start the server",
Flags: append(baseFlags, generatedFlags...),
Action: startServer,
Commands: []*cli.Command{
{
Name: "generate-keys",
Usage: "generates an API key and secret pair",
Action: generateKeys,
},
{
Name: "ports",
Usage: "print ports that server is configured to use",
Action: printPorts,
},
{
// this subcommand is deprecated, token generation is provided by CLI
Name: "create-join-token",
Hidden: true,
Usage: "create a room join token for development use",
Action: createToken,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "room",
Usage: "name of room to join",
Required: true,
},
&cli.StringFlag{
Name: "identity",
Usage: "identity of participant that holds the token",
Required: true,
},
&cli.BoolFlag{
Name: "recorder",
Usage: "creates a hidden participant that can only subscribe",
Required: false,
},
},
},
{
Name: "list-nodes",
Usage: "list all nodes",
Action: listNodes,
},
{
Name: "help-verbose",
Usage: "prints app help, including all generated configuration flags",
Action: helpVerbose,
},
},
Version: version.Version,
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
fmt.Println(err)
}
}
func getConfig(c *cli.Command) (*config.Config, error) {
confString, err := getConfigString(c.String("config"), c.String("config-body"))
if err != nil {
return nil, err
}
strictMode := true
if c.Bool("disable-strict-config") {
strictMode = false
}
conf, err := config.NewConfig(confString, strictMode, c, baseFlags)
if err != nil {
return nil, err
}
config.InitLoggerFromConfig(&conf.Logging)
if conf.Development {
logger.Infow("starting in development mode")
if len(conf.Keys) == 0 {
logger.Infow("no keys provided, using placeholder keys",
"API Key", "devkey",
"API Secret", "secret",
)
conf.Keys = map[string]string{
"devkey": "secret",
}
shouldMatchRTCIP := false
// when dev mode and using shared keys, we'll bind to localhost by default
if conf.BindAddresses == nil {
conf.BindAddresses = []string{
"127.0.0.1",
"::1",
}
} else {
// if non-loopback addresses are provided, then we'll match RTC IP to bind address
// our IP discovery ignores loopback addresses
for _, addr := range conf.BindAddresses {
ip := net.ParseIP(addr)
if ip != nil && !ip.IsLoopback() && !ip.IsUnspecified() {
shouldMatchRTCIP = true
}
}
}
if shouldMatchRTCIP {
for _, bindAddr := range conf.BindAddresses {
conf.RTC.IPs.Includes = append(conf.RTC.IPs.Includes, bindAddr+"/24")
}
}
}
}
return conf, nil
}
func startServer(ctx context.Context, c *cli.Command) error {
conf, err := getConfig(c)
if err != nil {
return err
}
if url := conf.Trace.JaegerURL; url != "" {
jaeger.Configure(ctx, url, "livekit")
}
// validate API key length
err = conf.ValidateKeys()
if err != nil {
return err
}
if cpuProfile := c.String("cpuprofile"); cpuProfile != "" {
if f, err := os.Create(cpuProfile); err != nil {
return err
} else {
if err := pprof.StartCPUProfile(f); err != nil {
f.Close()
return err
}
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
}
}
if memProfile := c.String("memprofile"); memProfile != "" {
if f, err := os.Create(memProfile); err != nil {
return err
} else {
defer func() {
// run memory profile at termination
runtime.GC()
_ = pprof.WriteHeapProfile(f)
_ = f.Close()
}()
}
}
currentNode, err := routing.NewLocalNode(conf)
if err != nil {
return err
}
if err := prometheus.Init(string(currentNode.NodeID()), currentNode.NodeType()); err != nil {
return err
}
server, err := service.InitializeServer(conf, currentNode)
if err != nil {
return err
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
for i := range 2 {
sig := <-sigChan
force := i > 0
logger.Infow("exit requested, shutting down", "signal", sig, "force", force)
go server.Stop(force)
}
}()
return server.Start()
}
func getConfigString(configFile string, inConfigBody string) (string, error) {
if inConfigBody != "" || configFile == "" {
return inConfigBody, nil
}
outConfigBody, err := os.ReadFile(configFile)
if err != nil {
return "", err
}
return string(outConfigBody), nil
}