waveterm/cmd/server/main-server.go
Evan Simkowitz 7e49e14977
Fix initialdata bug (#1365)
There was a bug in main-server where we were returning Window from
EnsureInitialData and then overwriting the layout on the active tab. The
return values from this function are unclear. Some of the initial data
was being set in the wcore.EnsureInitialData and some was being set in
main, so I've moved all the logic into EnsureInitialData and now it just
returns error.
2024-12-02 11:44:08 -08:00

322 lines
9.0 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"sync"
"syscall"
"time"
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wcloud"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wcore"
"github.com/wavetermdev/waveterm/pkg/web"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshserver"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wsl"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
// these are set at build time
var WaveVersion = "0.0.0"
var BuildTime = "0"
const InitialTelemetryWait = 10 * time.Second
const TelemetryTick = 2 * time.Minute
const TelemetryInterval = 4 * time.Hour
var shutdownOnce sync.Once
func doShutdown(reason string) {
shutdownOnce.Do(func() {
log.Printf("shutting down: %s\n", reason)
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
go blockcontroller.StopAllBlockControllers()
shutdownActivityUpdate()
sendTelemetryWrapper()
// TODO deal with flush in progress
clearTempFiles()
filestore.WFS.FlushCache(ctx)
watcher := wconfig.GetWatcher()
if watcher != nil {
watcher.Close()
}
time.Sleep(500 * time.Millisecond)
log.Printf("shutdown complete\n")
os.Exit(0)
})
}
func installShutdownSignalHandlers() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
go func() {
for sig := range sigCh {
doShutdown(fmt.Sprintf("got signal %v", sig))
break
}
}()
}
// watch stdin, kill server if stdin is closed
func stdinReadWatch() {
buf := make([]byte, 1024)
for {
_, err := os.Stdin.Read(buf)
if err != nil {
doShutdown(fmt.Sprintf("stdin closed/error (%v)", err))
break
}
}
}
func configWatcher() {
watcher := wconfig.GetWatcher()
if watcher != nil {
watcher.Start()
}
}
func telemetryLoop() {
var nextSend int64
time.Sleep(InitialTelemetryWait)
for {
if time.Now().Unix() > nextSend {
nextSend = time.Now().Add(TelemetryInterval).Unix()
sendTelemetryWrapper()
}
time.Sleep(TelemetryTick)
}
}
func panicTelemetryHandler() {
activity := wshrpc.ActivityUpdate{NumPanics: 1}
err := telemetry.UpdateActivity(context.Background(), activity)
if err != nil {
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
}
}
func sendTelemetryWrapper() {
defer panichandler.PanicHandler("sendTelemetryWrapper")
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
beforeSendActivityUpdate(ctx)
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
log.Printf("[error] getting client data for telemetry: %v\n", err)
return
}
err = wcloud.SendTelemetry(ctx, client.OID)
if err != nil {
log.Printf("[error] sending telemetry: %v\n", err)
}
}
func beforeSendActivityUpdate(ctx context.Context) {
activity := wshrpc.ActivityUpdate{}
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
activity.NumWSLConn = wsl.GetNumWSLHasConnected()
err := telemetry.UpdateActivity(ctx, activity)
if err != nil {
log.Printf("error updating before activity: %v\n", err)
}
}
func startupActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
activity := wshrpc.ActivityUpdate{Startup: 1}
err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
if err != nil {
log.Printf("error updating startup activity: %v\n", err)
}
}
func shutdownActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFn()
activity := wshrpc.ActivityUpdate{Shutdown: 1}
err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
if err != nil {
log.Printf("error updating shutdown activity: %v\n", err)
}
}
func createMainWshClient() {
rpc := wshserver.GetMainRpcClient()
wshutil.DefaultRouter.RegisterRoute(wshutil.DefaultRoute, rpc, true)
wps.Broker.SetClient(wshutil.DefaultRouter)
localConnWsh := wshutil.MakeWshRpc(nil, nil, wshrpc.RpcContext{Conn: wshrpc.LocalConnName}, &wshremote.ServerImpl{})
go wshremote.RunSysInfoLoop(localConnWsh, wshrpc.LocalConnName)
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeConnectionRouteId(wshrpc.LocalConnName), localConnWsh, true)
}
func grabAndRemoveEnvVars() error {
err := authkey.SetAuthKeyFromEnv()
if err != nil {
return fmt.Errorf("setting auth key: %v", err)
}
err = wavebase.CacheAndRemoveEnvVars()
if err != nil {
return err
}
err = wcloud.CacheAndRemoveEnvVars()
if err != nil {
return err
}
return nil
}
func clearTempFiles() error {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return fmt.Errorf("error getting client: %v", err)
}
filestore.WFS.DeleteZone(ctx, client.TempOID)
return nil
}
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.SetPrefix("[wavesrv] ")
wavebase.WaveVersion = WaveVersion
wavebase.BuildTime = BuildTime
err := grabAndRemoveEnvVars()
if err != nil {
log.Printf("[error] %v\n", err)
return
}
err = service.ValidateServiceMap()
if err != nil {
log.Printf("error validating service map: %v\n", err)
return
}
err = wavebase.EnsureWaveDataDir()
if err != nil {
log.Printf("error ensuring wave home dir: %v\n", err)
return
}
err = wavebase.EnsureWaveDBDir()
if err != nil {
log.Printf("error ensuring wave db dir: %v\n", err)
return
}
err = wavebase.EnsureWaveConfigDir()
if err != nil {
log.Printf("error ensuring wave config dir: %v\n", err)
return
}
// TODO: rather than ensure this dir exists, we should let the editor recursively create parent dirs on save
err = wavebase.EnsureWavePresetsDir()
if err != nil {
log.Printf("error ensuring wave presets dir: %v\n", err)
return
}
waveLock, err := wavebase.AcquireWaveLock()
if err != nil {
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)
return
}
defer func() {
err = waveLock.Close()
if err != nil {
log.Printf("error releasing wave lock: %v\n", err)
}
}()
log.Printf("wave version: %s (%s)\n", WaveVersion, BuildTime)
log.Printf("wave data dir: %s\n", wavebase.GetWaveDataDir())
log.Printf("wave config dir: %s\n", wavebase.GetWaveConfigDir())
err = filestore.InitFilestore()
if err != nil {
log.Printf("error initializing filestore: %v\n", err)
return
}
err = wstore.InitWStore()
if err != nil {
log.Printf("error initializing wstore: %v\n", err)
return
}
panichandler.PanicTelemetryHandler = panicTelemetryHandler
go func() {
defer panichandler.PanicHandler("InitCustomShellStartupFiles")
err := shellutil.InitCustomShellStartupFiles()
if err != nil {
log.Printf("error initializing wsh and shell-integration files: %v\n", err)
}
}()
err = wcore.EnsureInitialData()
if err != nil {
log.Printf("error ensuring initial data: %v\n", err)
return
}
err = clearTempFiles()
if err != nil {
log.Printf("error clearing temp files: %v\n", err)
return
}
createMainWshClient()
installShutdownSignalHandlers()
startupActivityUpdate()
go stdinReadWatch()
go telemetryLoop()
configWatcher()
webListener, err := web.MakeTCPListener("web")
if err != nil {
log.Printf("error creating web listener: %v\n", err)
return
}
wsListener, err := web.MakeTCPListener("websocket")
if err != nil {
log.Printf("error creating websocket listener: %v\n", err)
return
}
go web.RunWebSocketServer(wsListener)
unixListener, err := web.MakeUnixListener()
if err != nil {
log.Printf("error creating unix listener: %v\n", err)
return
}
go func() {
if BuildTime == "" {
BuildTime = "0"
}
// use fmt instead of log here to make sure it goes directly to stderr
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART ws:%s web:%s version:%s buildtime:%s\n", wsListener.Addr(), webListener.Addr(), WaveVersion, BuildTime)
}()
go wshutil.RunWshRpcOverListener(unixListener)
web.RunWebServer(webListener) // blocking
runtime.KeepAlive(waveLock)
}