waveterm/pkg/wcore/wcore.go
Mike Sawka 01a26d59e6
Persistent Terminal Sessions (+ improvements and bug fixes) (#2806)
Lots of updates across all parts of the system to get this working. Big
changes to routing, streaming, connection management, etc.

* Persistent sessions behind a metadata flag for now
* New backlog queue in the router to prevent hanging
* Fix connection Close() issues that caused hangs when network was down
* Fix issue with random routeids (need to be generated fresh each time
the JWT is used and not fixed) so you can run multiple-wsh commands at
once
* Fix issue with domain sockets changing names across wave restarts
(added a symlink mechanism to resolve new names)
* ClientId caching in main server
* Quick reorder queue for input to prevent out of order delivery across
multiple hops
* Fix out-of-order event delivery in router (remove unnecessary go
routine creation)
* Environment testing and fix environment variables for remote jobs (get
from connserver, add to remote job starts)
* Add new ConnServerInit() remote method to call before marking
connection up
* TODO -- remote file transfer needs to be fixed to not create OOM
issues when transferring large files or directories
2026-01-28 13:30:48 -08:00

229 lines
6.5 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// wave core application coordinator
package wcore
import (
"context"
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"log"
"strings"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/wavejwt"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wcloud"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
// the wcore package coordinates actions across the storage layer
// orchestrating the wave object store, the wave pubsub system, and the wave rpc system
// Ensures that the initial data is present in the store, creates an initial window if needed
func EnsureInitialData() (bool, error) {
// does not need to run in a transaction since it is called on startup
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
firstLaunch := false
if err == wstore.ErrNotFound {
client, err = CreateClient(ctx)
if err != nil {
return false, fmt.Errorf("error creating client: %w", err)
}
firstLaunch = true
}
if client.TempOID == "" {
log.Println("client.TempOID is empty")
client.TempOID = uuid.NewString()
err = wstore.DBUpdate(ctx, client)
if err != nil {
return firstLaunch, fmt.Errorf("error updating client: %w", err)
}
}
if client.InstallId == "" {
log.Println("client.InstallId is empty")
client.InstallId = uuid.NewString()
err = wstore.DBUpdate(ctx, client)
if err != nil {
return firstLaunch, fmt.Errorf("error updating client: %w", err)
}
}
log.Printf("clientid: %s\n", client.OID)
wstore.SetClientId(client.OID)
if len(client.WindowIds) == 1 {
log.Println("client has one window")
CheckAndFixWindow(ctx, client.WindowIds[0])
return firstLaunch, nil
}
if len(client.WindowIds) > 0 {
log.Println("client has windows")
return firstLaunch, nil
}
wsId := ""
if firstLaunch {
log.Println("client has no windows and first launch, creating starter workspace")
starterWs, err := CreateWorkspace(ctx, "Starter workspace", "custom@wave-logo-solid", "#58C142", false, true)
if err != nil {
return firstLaunch, fmt.Errorf("error creating starter workspace: %w", err)
}
wsId = starterWs.OID
}
_, err = CreateWindow(ctx, nil, wsId)
if err != nil {
return firstLaunch, fmt.Errorf("error creating window: %w", err)
}
return firstLaunch, nil
}
func CreateClient(ctx context.Context) (*waveobj.Client, error) {
client := &waveobj.Client{
OID: uuid.NewString(),
WindowIds: []string{},
}
err := wstore.DBInsert(ctx, client)
if err != nil {
return nil, fmt.Errorf("error inserting client: %w", err)
}
return client, nil
}
func GetClientData(ctx context.Context) (*waveobj.Client, error) {
clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client data: %w", err)
}
return clientData, nil
}
func SendWaveObjUpdate(oref waveobj.ORef) {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
// send a waveobj:update event
waveObj, err := wstore.DBGetORef(ctx, oref)
if err != nil {
log.Printf("error getting object for update event: %v", err)
return
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{oref.String()},
Data: waveobj.WaveObjUpdate{
UpdateType: waveobj.UpdateType_Update,
OType: waveObj.GetOType(),
OID: waveobj.GetOID(waveObj),
Obj: waveObj,
},
})
}
func ResolveBlockIdFromPrefix(ctx context.Context, tabId string, blockIdPrefix string) (string, error) {
if len(blockIdPrefix) != 8 {
return "", fmt.Errorf("widget_id must be 8 characters")
}
tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
if err != nil {
return "", fmt.Errorf("error getting tab: %w", err)
}
for _, blockId := range tab.BlockIds {
if strings.HasPrefix(blockId, blockIdPrefix) {
return blockId, nil
}
}
return "", fmt.Errorf("widget_id not found: %q", blockIdPrefix)
}
func GoSendNoTelemetryUpdate(telemetryEnabled bool) {
go func() {
defer func() {
panichandler.PanicHandler("GoSendNoTelemetryUpdate", recover())
}()
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
clientId := wstore.GetClientId()
err := wcloud.SendNoTelemetryUpdate(ctx, clientId, !telemetryEnabled)
if err != nil {
log.Printf("[error] sending no-telemetry update: %v\n", err)
return
}
}()
}
func InitMainServer() error {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
mainServer, err := wstore.DBGetSingleton[*waveobj.MainServer](ctx)
if err == wstore.ErrNotFound {
mainServer = &waveobj.MainServer{
OID: uuid.NewString(),
}
err = wstore.DBInsert(ctx, mainServer)
if err != nil {
return fmt.Errorf("error inserting mainserver: %w", err)
}
} else if err != nil {
return fmt.Errorf("error getting mainserver: %w", err)
}
needsUpdate := false
if mainServer.JwtPrivateKey == "" || mainServer.JwtPublicKey == "" {
keyPair, err := wavejwt.GenerateKeyPair()
if err != nil {
return fmt.Errorf("error generating jwt keypair: %w", err)
}
mainServer.JwtPrivateKey = base64.StdEncoding.EncodeToString(keyPair.PrivateKey)
mainServer.JwtPublicKey = base64.StdEncoding.EncodeToString(keyPair.PublicKey)
needsUpdate = true
}
if needsUpdate {
err = wstore.DBUpdate(ctx, mainServer)
if err != nil {
return fmt.Errorf("error updating mainserver: %w", err)
}
}
privateKeyBytes, err := base64.StdEncoding.DecodeString(mainServer.JwtPrivateKey)
if err != nil {
return fmt.Errorf("error decoding jwt private key: %w", err)
}
publicKeyBytes, err := base64.StdEncoding.DecodeString(mainServer.JwtPublicKey)
if err != nil {
return fmt.Errorf("error decoding jwt public key: %w", err)
}
err = wavejwt.SetPrivateKey(privateKeyBytes)
if err != nil {
return fmt.Errorf("error setting jwt private key: %w", err)
}
err = wavejwt.SetPublicKey(publicKeyBytes)
if err != nil {
return fmt.Errorf("error setting jwt public key: %w", err)
}
pubKeyDer, err := x509.MarshalPKIXPublicKey(ed25519.PublicKey(publicKeyBytes))
if err != nil {
log.Printf("warning: could not marshal public key for logging: %v", err)
} else {
pubKeyPem := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyDer,
})
log.Printf("JWT Public Key:\n%s", string(pubKeyPem))
}
return nil
}