mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-11-28 05:00:26 +08:00
this fixes a rare case where a workspace tries to load a tab that doesn't exist. this allows the UI to load even despite that condition. in addition it fixes the "delete tab" path so, even if the tab doesn't exist, it will get cleaned up correctly out of the workspace.
244 lines
8.3 KiB
Go
244 lines
8.3 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package wcore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
|
"github.com/wavetermdev/waveterm/pkg/filestore"
|
|
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
|
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
|
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
"github.com/wavetermdev/waveterm/pkg/wstore"
|
|
)
|
|
|
|
func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
|
if blockDef == nil {
|
|
return nil, fmt.Errorf("blockDef is nil")
|
|
}
|
|
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
|
return nil, fmt.Errorf("no view provided for new block")
|
|
}
|
|
blockData, err := createSubBlockObj(ctx, blockId, blockDef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating sub block: %w", err)
|
|
}
|
|
return blockData, nil
|
|
}
|
|
|
|
func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
|
|
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentBlockId)
|
|
if parentBlock == nil {
|
|
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
|
|
}
|
|
blockId := uuid.NewString()
|
|
blockData := &waveobj.Block{
|
|
OID: blockId,
|
|
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
|
RuntimeOpts: nil,
|
|
Meta: blockDef.Meta,
|
|
}
|
|
wstore.DBInsert(tx.Context(), blockData)
|
|
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
|
|
wstore.DBUpdate(tx.Context(), parentBlock)
|
|
return blockData, nil
|
|
})
|
|
}
|
|
|
|
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (rtnBlock *waveobj.Block, rtnErr error) {
|
|
return CreateBlockWithTelemetry(ctx, tabId, blockDef, rtOpts, true)
|
|
}
|
|
|
|
func CreateBlockWithTelemetry(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts, recordTelemetry bool) (rtnBlock *waveobj.Block, rtnErr error) {
|
|
var blockCreated bool
|
|
var newBlockOID string
|
|
defer func() {
|
|
if rtnErr == nil {
|
|
return
|
|
}
|
|
// if there was an error, and we created the block, clean it up since the function failed
|
|
if blockCreated && newBlockOID != "" {
|
|
deleteBlockObj(ctx, newBlockOID)
|
|
filestore.WFS.DeleteZone(ctx, newBlockOID)
|
|
}
|
|
}()
|
|
if blockDef == nil {
|
|
return nil, fmt.Errorf("blockDef is nil")
|
|
}
|
|
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
|
return nil, fmt.Errorf("no view provided for new block")
|
|
}
|
|
blockData, err := createBlockObj(ctx, tabId, blockDef, rtOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating block: %w", err)
|
|
}
|
|
blockCreated = true
|
|
newBlockOID = blockData.OID
|
|
// upload the files if present
|
|
if len(blockDef.Files) > 0 {
|
|
for fileName, fileDef := range blockDef.Files {
|
|
err := filestore.WFS.MakeFile(ctx, newBlockOID, fileName, fileDef.Meta, wshrpc.FileOpts{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error making blockfile %q: %w", fileName, err)
|
|
}
|
|
err = filestore.WFS.WriteFile(ctx, newBlockOID, fileName, []byte(fileDef.Content))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error writing blockfile %q: %w", fileName, err)
|
|
}
|
|
}
|
|
}
|
|
if recordTelemetry {
|
|
blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "")
|
|
go recordBlockCreationTelemetry(blockView)
|
|
}
|
|
return blockData, nil
|
|
}
|
|
|
|
func recordBlockCreationTelemetry(blockView string) {
|
|
defer func() {
|
|
panichandler.PanicHandler("CreateBlock:telemetry", recover())
|
|
}()
|
|
if blockView == "" {
|
|
return
|
|
}
|
|
tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancelFn()
|
|
telemetry.UpdateActivity(tctx, wshrpc.ActivityUpdate{
|
|
Renderers: map[string]int{blockView: 1},
|
|
})
|
|
telemetry.RecordTEvent(tctx, &telemetrydata.TEvent{
|
|
Event: "action:createblock",
|
|
Props: telemetrydata.TEventProps{
|
|
BlockView: blockView,
|
|
},
|
|
})
|
|
}
|
|
|
|
func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
|
|
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), tabId)
|
|
if tab == nil {
|
|
return nil, fmt.Errorf("tab not found: %q", tabId)
|
|
}
|
|
blockId := uuid.NewString()
|
|
blockData := &waveobj.Block{
|
|
OID: blockId,
|
|
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
|
RuntimeOpts: rtOpts,
|
|
Meta: blockDef.Meta,
|
|
}
|
|
wstore.DBInsert(tx.Context(), blockData)
|
|
tab.BlockIds = append(tab.BlockIds, blockId)
|
|
wstore.DBUpdate(tx.Context(), tab)
|
|
return blockData, nil
|
|
})
|
|
}
|
|
|
|
// Must delete all blocks individually first.
|
|
// Also deletes LayoutState.
|
|
// recursive: if true, will recursively close parent tab, window, workspace, if they are empty.
|
|
// Returns new active tab id, error.
|
|
func DeleteBlock(ctx context.Context, blockId string, recursive bool) error {
|
|
block, err := wstore.DBGet[*waveobj.Block](ctx, blockId)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting block: %w", err)
|
|
}
|
|
if block == nil {
|
|
return nil
|
|
}
|
|
if len(block.SubBlockIds) > 0 {
|
|
for _, subBlockId := range block.SubBlockIds {
|
|
err := DeleteBlock(ctx, subBlockId, recursive)
|
|
if err != nil {
|
|
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
|
|
}
|
|
}
|
|
}
|
|
parentBlockCount, err := deleteBlockObj(ctx, blockId)
|
|
if err != nil {
|
|
return fmt.Errorf("error deleting block: %w", err)
|
|
}
|
|
log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
|
|
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
|
|
|
if recursive && parentORef.OType == waveobj.OType_Tab && parentBlockCount == 0 {
|
|
// if parent tab has no blocks, delete the tab
|
|
log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID)
|
|
parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID)
|
|
if err != nil {
|
|
return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err)
|
|
}
|
|
newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true)
|
|
if err != nil {
|
|
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
|
|
}
|
|
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
|
|
}
|
|
go blockcontroller.StopBlockController(blockId)
|
|
sendBlockCloseEvent(blockId)
|
|
return nil
|
|
}
|
|
|
|
// returns the updated block count for the parent object
|
|
func deleteBlockObj(ctx context.Context, blockId string) (int, error) {
|
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {
|
|
block, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("error getting block: %w", err)
|
|
}
|
|
if block == nil {
|
|
return -1, fmt.Errorf("block not found: %q", blockId)
|
|
}
|
|
if len(block.SubBlockIds) > 0 {
|
|
return -1, fmt.Errorf("block has subblocks, must delete subblocks first")
|
|
}
|
|
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
|
parentBlockCount := -1
|
|
if parentORef != nil {
|
|
if parentORef.OType == waveobj.OType_Tab {
|
|
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
|
|
if tab != nil {
|
|
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
|
|
wstore.DBUpdate(tx.Context(), tab)
|
|
parentBlockCount = len(tab.BlockIds)
|
|
}
|
|
} else if parentORef.OType == waveobj.OType_Block {
|
|
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
|
|
if parentBlock != nil {
|
|
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
|
|
wstore.DBUpdate(tx.Context(), parentBlock)
|
|
parentBlockCount = len(parentBlock.SubBlockIds)
|
|
}
|
|
}
|
|
}
|
|
wstore.DBDelete(tx.Context(), waveobj.OType_Block, blockId)
|
|
|
|
// Clean up block runtime info
|
|
blockORef := waveobj.MakeORef(waveobj.OType_Block, blockId)
|
|
wstore.DeleteRTInfo(blockORef)
|
|
|
|
return parentBlockCount, nil
|
|
})
|
|
}
|
|
|
|
func sendBlockCloseEvent(blockId string) {
|
|
waveEvent := wps.WaveEvent{
|
|
Event: wps.Event_BlockClose,
|
|
Scopes: []string{
|
|
waveobj.MakeORef(waveobj.OType_Block, blockId).String(),
|
|
},
|
|
Data: blockId,
|
|
}
|
|
wps.Broker.Publish(waveEvent)
|
|
}
|