waveterm/pkg/wavebase/wavebase.go
Evan Simkowitz 33f05c6e0c
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).

For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.

As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 09:26:58 -07:00

246 lines
5.9 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"
"github.com/wavetermdev/waveterm/pkg/util/panic"
)
// set by main-server.go
var WaveVersion = "0.0.0"
var BuildTime = "0"
const WaveConfigHomeEnvVar = "WAVETERM_CONFIG_HOME"
const WaveDataHomeEnvVar = "WAVETERM_DATA_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(GetWaveDataDir(), DomainSocketBaseName)
}
func GetWaveDataDir() string {
retVal, found := os.LookupEnv(WaveDataHomeEnvVar)
if !found {
panic.Panic(WaveDataHomeEnvVar + " not set")
}
return retVal
}
func GetWaveConfigDir() string {
retVal, found := os.LookupEnv(WaveConfigHomeEnvVar)
if !found {
panic.Panic(WaveConfigHomeEnvVar + " not set")
}
return retVal
}
func EnsureWaveDataDir() error {
return CacheEnsureDir(GetWaveDataDir(), "wavehome", 0700, "wave home directory")
}
func EnsureWaveDBDir() error {
return CacheEnsureDir(filepath.Join(GetWaveDataDir(), WaveDBDir), "wavedb", 0700, "wave db directory")
}
func EnsureWaveConfigDir() error {
return CacheEnsureDir(GetWaveConfigDir(), "waveconfig", 0700, "wave config directory")
}
func EnsureWavePresetsDir() error {
return CacheEnsureDir(filepath.Join(GetWaveConfigDir(), "presets"), "wavepresets", 0700, "wave presets 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
}