mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-03-11 13:23:06 +01:00
Adds new functionality on the backend that will merge any file from the config directory that matches `<partName>.json` or `<partName>/*.json` into the corresponding config part (presets, termthemes, etc.). This lets us separate the AI presets into `presets/ai.json` so that we can add a dropdown in the AI preset selector that will directly open the file so a user can edit it more easily. Right now, this will create a preview block in the layout, but in the future we can look into making this block disconnected from the layout. If you put AI presets in the regular presets.json file, it will still work, since all the presets get merged. Same for any other config part.
232 lines
5.5 KiB
Go
232 lines
5.5 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package wavebase
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// set by main-server.go
|
|
var WaveVersion = "0.0.0"
|
|
var BuildTime = "0"
|
|
|
|
const DefaultWaveHome = "~/.waveterm"
|
|
const DevWaveHome = "~/.waveterm-dev"
|
|
const WaveHomeVarName = "WAVETERM_HOME"
|
|
const WaveDevVarName = "WAVETERM_DEV"
|
|
const WaveLockFile = "wave.lock"
|
|
const DomainSocketBaseName = "wave.sock"
|
|
const WaveDBDir = "db"
|
|
const JwtSecret = "waveterm" // TODO generate and store this
|
|
const ConfigDir = "config"
|
|
|
|
const WaveAppPathVarName = "WAVETERM_APP_PATH"
|
|
const AppPathBinDir = "bin"
|
|
|
|
var baseLock = &sync.Mutex{}
|
|
var ensureDirCache = map[string]bool{}
|
|
|
|
type FDLock interface {
|
|
Close() error
|
|
}
|
|
|
|
func IsDevMode() bool {
|
|
pdev := os.Getenv(WaveDevVarName)
|
|
return pdev != ""
|
|
}
|
|
|
|
func GetWaveAppPath() string {
|
|
return os.Getenv(WaveAppPathVarName)
|
|
}
|
|
|
|
func GetWaveAppBinPath() string {
|
|
return filepath.Join(GetWaveAppPath(), AppPathBinDir)
|
|
}
|
|
|
|
func GetHomeDir() string {
|
|
homeVar, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "/"
|
|
}
|
|
return homeVar
|
|
}
|
|
|
|
func ExpandHomeDir(pathStr string) (string, error) {
|
|
if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") && (!strings.HasPrefix(pathStr, `~\`) || runtime.GOOS != "windows") {
|
|
return filepath.Clean(pathStr), nil
|
|
}
|
|
homeDir := GetHomeDir()
|
|
if pathStr == "~" {
|
|
return homeDir, nil
|
|
}
|
|
expandedPath := filepath.Clean(filepath.Join(homeDir, pathStr[2:]))
|
|
absPath, err := filepath.Abs(filepath.Join(homeDir, expandedPath))
|
|
if err != nil || !strings.HasPrefix(absPath, homeDir) {
|
|
return "", fmt.Errorf("potential path traversal detected for path %s", pathStr)
|
|
}
|
|
return expandedPath, nil
|
|
}
|
|
|
|
func ExpandHomeDirSafe(pathStr string) string {
|
|
path, _ := ExpandHomeDir(pathStr)
|
|
return path
|
|
}
|
|
|
|
func ReplaceHomeDir(pathStr string) string {
|
|
homeDir := GetHomeDir()
|
|
if pathStr == homeDir {
|
|
return "~"
|
|
}
|
|
if strings.HasPrefix(pathStr, homeDir+"/") {
|
|
return "~" + pathStr[len(homeDir):]
|
|
}
|
|
return pathStr
|
|
}
|
|
|
|
func GetDomainSocketName() string {
|
|
return filepath.Join(GetWaveHomeDir(), DomainSocketBaseName)
|
|
}
|
|
|
|
func GetWaveHomeDir() string {
|
|
homeVar := os.Getenv(WaveHomeVarName)
|
|
if homeVar != "" {
|
|
return ExpandHomeDirSafe(homeVar)
|
|
}
|
|
if IsDevMode() {
|
|
return ExpandHomeDirSafe(DevWaveHome)
|
|
}
|
|
return ExpandHomeDirSafe(DefaultWaveHome)
|
|
}
|
|
|
|
func EnsureWaveHomeDir() error {
|
|
return CacheEnsureDir(GetWaveHomeDir(), "wavehome", 0700, "wave home directory")
|
|
}
|
|
|
|
func EnsureWaveDBDir() error {
|
|
return CacheEnsureDir(filepath.Join(GetWaveHomeDir(), WaveDBDir), "wavedb", 0700, "wave db directory")
|
|
}
|
|
|
|
func CacheEnsureDir(dirName string, cacheKey string, perm os.FileMode, dirDesc string) error {
|
|
baseLock.Lock()
|
|
ok := ensureDirCache[cacheKey]
|
|
baseLock.Unlock()
|
|
if ok {
|
|
return nil
|
|
}
|
|
err := TryMkdirs(dirName, perm, dirDesc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
baseLock.Lock()
|
|
ensureDirCache[cacheKey] = true
|
|
baseLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func TryMkdirs(dirName string, perm os.FileMode, dirDesc string) error {
|
|
info, err := os.Stat(dirName)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
err = os.MkdirAll(dirName, perm)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot make %s %q: %w", dirDesc, dirName, err)
|
|
}
|
|
info, err = os.Stat(dirName)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("error trying to stat %s: %w", dirDesc, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("%s %q must be a directory", dirDesc, dirName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var osLangOnce = &sync.Once{}
|
|
var osLang string
|
|
|
|
func determineLang() string {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancelFn()
|
|
if runtime.GOOS == "darwin" {
|
|
out, err := exec.CommandContext(ctx, "defaults", "read", "-g", "AppleLocale").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err)
|
|
return ""
|
|
}
|
|
strOut := string(out)
|
|
truncOut := strings.Split(strOut, "@")[0]
|
|
return strings.TrimSpace(truncOut) + ".UTF-8"
|
|
} else if runtime.GOOS == "win32" {
|
|
out, err := exec.CommandContext(ctx, "Get-Culture", "|", "select", "-exp", "Name").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("error executing 'Get-Culture | select -exp Name': %v\n", err)
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(out)) + ".UTF-8"
|
|
} else {
|
|
// this is specifically to get the wavesrv LANG so waveshell
|
|
// on a remote uses the same LANG
|
|
return os.Getenv("LANG")
|
|
}
|
|
}
|
|
|
|
func DetermineLang() string {
|
|
osLangOnce.Do(func() {
|
|
osLang = determineLang()
|
|
})
|
|
return osLang
|
|
}
|
|
|
|
func DetermineLocale() string {
|
|
truncated := strings.Split(DetermineLang(), ".")[0]
|
|
if truncated == "" {
|
|
return "C"
|
|
}
|
|
return strings.Replace(truncated, "_", "-", -1)
|
|
}
|
|
|
|
func ClientArch() string {
|
|
return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
var releaseRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)`)
|
|
var osReleaseOnce = &sync.Once{}
|
|
var osRelease string
|
|
|
|
func unameKernelRelease() string {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancelFn()
|
|
out, err := exec.CommandContext(ctx, "uname", "-r").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("error executing uname -r: %v\n", err)
|
|
return "-"
|
|
}
|
|
releaseStr := strings.TrimSpace(string(out))
|
|
m := releaseRegex.FindStringSubmatch(releaseStr)
|
|
if m == nil || len(m) < 2 {
|
|
log.Printf("invalid uname -r output: [%s]\n", releaseStr)
|
|
return "-"
|
|
}
|
|
return m[1]
|
|
}
|
|
|
|
func UnameKernelRelease() string {
|
|
osReleaseOnce.Do(func() {
|
|
osRelease = unameKernelRelease()
|
|
})
|
|
return osRelease
|
|
}
|