waveterm/wavesrv/pkg/scbase/scbase.go

464 lines
11 KiB
Go
Raw Normal View History

2023-10-17 06:31:13 +02:00
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2022-07-01 02:02:19 +02:00
package scbase
import (
2023-02-24 00:17:47 +01:00
"context"
"errors"
"fmt"
"io"
"io/fs"
2022-10-31 20:40:45 +01:00
"log"
2022-07-01 02:02:19 +02:00
"os"
2023-02-24 00:17:47 +01:00
"os/exec"
"path/filepath"
2023-02-24 00:17:47 +01:00
"regexp"
"runtime"
2022-09-21 02:37:49 +02:00
"strconv"
2023-02-24 00:17:47 +01:00
"strings"
"sync"
2023-02-24 00:17:47 +01:00
"time"
2023-07-31 02:16:43 +02:00
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"golang.org/x/mod/semver"
"golang.org/x/sys/unix"
2022-07-01 02:02:19 +02:00
)
const HomeVarName = "HOME"
const WaveHomeVarName = "WAVETERM_HOME"
const WaveDevVarName = "WAVETERM_DEV"
const SessionsDirBaseName = "sessions"
2023-03-21 03:20:57 +01:00
const ScreensDirBaseName = "screens"
const WaveLockFile = "waveterm.lock"
const WaveDirName = ".waveterm" // must match emain.ts
const WaveDevDirName = ".waveterm-dev" // must match emain.ts
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveAuthKeyFileName = "waveterm.authkey"
const WaveshellVersion = "v0.7.0" // must match base.WaveshellVersion
// initialized by InitialzeWaveAuthKey (called by main-server)
var WaveAuthKey string
var SessionDirCache = make(map[string]string)
2023-03-21 03:20:57 +01:00
var ScreenDirCache = make(map[string]string)
var BaseLock = &sync.Mutex{}
// these are set by the main-server using build-time variables
2023-02-24 00:17:47 +01:00
var BuildTime = "-"
var WaveVersion = "-"
2022-07-01 02:02:19 +02:00
func IsDevMode() bool {
pdev := os.Getenv(WaveDevVarName)
return pdev != ""
}
// must match js
func GetWaveHomeDir() string {
scHome := os.Getenv(WaveHomeVarName)
2022-07-01 02:02:19 +02:00
if scHome == "" {
homeVar := os.Getenv(HomeVarName)
if homeVar == "" {
homeVar = "/"
}
pdev := os.Getenv(WaveDevVarName)
2022-12-28 22:56:19 +01:00
if pdev != "" {
scHome = filepath.Join(homeVar, WaveDevDirName)
2022-12-28 22:56:19 +01:00
} else {
scHome = filepath.Join(homeVar, WaveDirName)
2022-12-28 22:56:19 +01:00
}
2022-07-01 02:02:19 +02:00
}
return scHome
}
func WaveshellBinaryDir() string {
appPath := os.Getenv(WaveAppPathVarName)
if appPath == "" {
appPath = "."
}
return filepath.Join(appPath, "bin", "mshell")
}
func WaveshellBinaryPath(version string, goos string, goarch string) (string, error) {
if !base.ValidGoArch(goos, goarch) {
return "", fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch)
}
binaryDir := WaveshellBinaryDir()
versionStr := semver.MajorMinor(version)
if versionStr == "" {
return "", fmt.Errorf("invalid waveshell version: %q", version)
}
fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch)
fullFileName := filepath.Join(binaryDir, fileName)
return fullFileName, nil
}
func LocalWaveshellBinaryPath() (string, error) {
return WaveshellBinaryPath(WaveshellVersion, runtime.GOOS, runtime.GOARCH)
}
func WaveshellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) {
waveshellPath, err := WaveshellBinaryPath(version, goos, goarch)
if err != nil {
return nil, err
}
fd, err := os.Open(waveshellPath)
if err != nil {
return nil, fmt.Errorf("cannot open waveshell binary %q: %v", waveshellPath, err)
}
return fd, nil
}
// also sets WaveAuthKey
func createWaveAuthKeyFile(fileName string) error {
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer fd.Close()
keyStr := GenWaveUUID()
_, err = fd.Write([]byte(keyStr))
if err != nil {
return err
}
WaveAuthKey = keyStr
return nil
}
// sets WaveAuthKey
func InitializeWaveAuthKey() error {
homeDir := GetWaveHomeDir()
err := ensureDir(homeDir)
if err != nil {
return fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
}
fileName := filepath.Join(homeDir, WaveAuthKeyFileName)
fd, err := os.Open(fileName)
if err != nil && errors.Is(err, fs.ErrNotExist) {
return createWaveAuthKeyFile(fileName)
}
if err != nil {
return fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
}
defer fd.Close()
buf, err := io.ReadAll(fd)
if err != nil {
return fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
}
keyStr := string(buf)
_, err = uuid.Parse(keyStr)
if err != nil {
return fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
}
WaveAuthKey = keyStr
return nil
}
func AcquireWaveLock() (*os.File, error) {
homeDir := GetWaveHomeDir()
2022-10-31 20:40:45 +01:00
err := ensureDir(homeDir)
if err != nil {
return nil, fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
2022-10-31 20:40:45 +01:00
}
lockFileName := filepath.Join(homeDir, WaveLockFile)
log.Printf("[base] acquiring lock on %s\n", lockFileName)
fd, err := os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
err = unix.Flock(int(fd.Fd()), unix.LOCK_EX|unix.LOCK_NB)
if err != nil {
fd.Close()
return nil, err
}
return fd, nil
}
2023-03-21 03:20:57 +01:00
// deprecated (v0.1.8)
func EnsureSessionDir(sessionId string) (string, error) {
2022-08-24 22:21:54 +02:00
if sessionId == "" {
return "", fmt.Errorf("cannot get session dir for blank sessionid")
}
BaseLock.Lock()
sdir, ok := SessionDirCache[sessionId]
BaseLock.Unlock()
if ok {
return sdir, nil
}
scHome := GetWaveHomeDir()
sdir = filepath.Join(scHome, SessionsDirBaseName, sessionId)
err := ensureDir(sdir)
if err != nil {
return "", err
}
BaseLock.Lock()
SessionDirCache[sessionId] = sdir
BaseLock.Unlock()
return sdir, nil
}
2023-03-21 03:20:57 +01:00
// deprecated (v0.1.8)
2022-12-27 03:49:45 +01:00
func GetSessionsDir() string {
waveHome := GetWaveHomeDir()
sdir := filepath.Join(waveHome, SessionsDirBaseName)
2022-12-27 03:49:45 +01:00
return sdir
}
2023-03-21 03:20:57 +01:00
func EnsureScreenDir(screenId string) (string, error) {
if screenId == "" {
return "", fmt.Errorf("cannot get screen dir for blank sessionid")
}
BaseLock.Lock()
sdir, ok := ScreenDirCache[screenId]
BaseLock.Unlock()
if ok {
return sdir, nil
}
scHome := GetWaveHomeDir()
sdir = filepath.Join(scHome, ScreensDirBaseName, screenId)
2023-03-21 03:20:57 +01:00
err := ensureDir(sdir)
if err != nil {
return "", err
}
BaseLock.Lock()
ScreenDirCache[screenId] = sdir
BaseLock.Unlock()
return sdir, nil
}
func GetScreensDir() string {
waveHome := GetWaveHomeDir()
sdir := filepath.Join(waveHome, ScreensDirBaseName)
2023-03-21 03:20:57 +01:00
return sdir
}
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
func EnsureConfigDirs() (string, error) {
scHome := GetWaveHomeDir()
configDir := filepath.Join(scHome, "config")
err := ensureDir(configDir)
if err != nil {
return "", err
}
keybindingsFile := filepath.Join(configDir, "keybindings.json")
keybindingsFileObj, err := ensureFile(keybindingsFile)
if err != nil {
return "", err
}
if keybindingsFileObj != nil {
keybindingsFileObj.WriteString("[]\n")
keybindingsFileObj.Close()
}
terminalThemesDir := filepath.Join(configDir, "terminal-themes")
Simplified terminal theming (#570) * save work * reusable StyleBlock component * StyleBlock in elements dir * root level * ability to inherit root styles * change prop from classname to selector * selector should always be :root * remove selector prop from StyleBlock * working * cleanup * loadThemeStyles doesn't have to be async * revert changes in tabs2.less * remove old implementation * cleanup * remove file from another branch * fix issue where line in history view doesn't reflect the terminal theme * add key and value validation * add label to tab settings terminal theme dropdown * save work * save work * save work * working * trigger componentDidUpdate when switching tabs and sessions * cleanup * save work * save work * use UpdatePacket for theme changes as well * make methods cohesive * use themes coming from backend * reload terminal when styel block is unmounted and mounted * fix validation * re-render terminal when theme is updated * remove test styles * cleanup * more cleanup * revert unneeded change * more cleanup * fix type * more cleanup * render style blocks in the header instead of body using portal * add ability to reuse and dispose TermThemes instance and file watcher * remove comment * minor change * separate filewatcher as singleton * do not render app when term theme style blocks aren't rendered first * only render main when termstyles have been rendered already * add comment * use DoUpdate to send themes to front-end * support to watch subdirectories * added support for watch subdirectories * make watcher more flexible so it can be closed anywhere * cleanup * undo the app/main split * use TermThemesType in creating initial value for Themes field * simplify code * fix issue where dropdown label doesn't float when the theme selected is Inherit * remove unsed var * start watcher in main, merge themes (don't overwrite) on event. * ensure terminal-themes directory is created on startup * ah, wait for termThemes to be set (the connect packet needs to have been processed to proceed with rendering)
2024-04-24 08:22:35 +02:00
err = ensureDir(terminalThemesDir)
if err != nil {
return "", err
}
return configDir, nil
}
func ensureFile(fileName string) (*os.File, error) {
info, err := os.Stat(fileName)
var myFile *os.File
if errors.Is(err, fs.ErrNotExist) {
myFile, err = os.Create(fileName)
if err != nil {
return nil, err
}
log.Printf("[wave] created file %q\n", fileName)
info, err = myFile.Stat()
}
if err != nil {
return myFile, err
}
if info.IsDir() {
return myFile, fmt.Errorf("'%s' must be a file", fileName)
}
return myFile, nil
}
func ensureDir(dirName string) error {
info, err := os.Stat(dirName)
if errors.Is(err, fs.ErrNotExist) {
err = os.MkdirAll(dirName, 0700)
if err != nil {
return err
}
log.Printf("[wave] created directory %q\n", dirName)
info, err = os.Stat(dirName)
}
if err != nil {
return err
}
if !info.IsDir() {
return fmt.Errorf("'%s' must be a directory", dirName)
}
return nil
}
2023-03-21 03:20:57 +01:00
// deprecated (v0.1.8)
func PtyOutFile_Sessions(sessionId string, cmdId string) (string, error) {
sdir, err := EnsureSessionDir(sessionId)
if err != nil {
return "", err
}
2022-08-24 22:21:54 +02:00
if sessionId == "" {
return "", fmt.Errorf("cannot get ptyout file for blank sessionid")
}
if cmdId == "" {
return "", fmt.Errorf("cannot get ptyout file for blank cmdid")
}
return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil
}
2023-07-31 02:16:43 +02:00
func PtyOutFile(screenId string, lineId string) (string, error) {
2023-03-21 03:20:57 +01:00
sdir, err := EnsureScreenDir(screenId)
if err != nil {
return "", err
}
2023-03-21 03:20:57 +01:00
if screenId == "" {
return "", fmt.Errorf("cannot get ptyout file for blank screenid")
2022-08-24 22:21:54 +02:00
}
2023-07-31 02:16:43 +02:00
if lineId == "" {
return "", fmt.Errorf("cannot get ptyout file for blank lineid")
2022-08-24 22:21:54 +02:00
}
2023-07-31 02:16:43 +02:00
return fmt.Sprintf("%s/%s.ptyout.cf", sdir, lineId), nil
}
2022-09-21 02:37:49 +02:00
func GenWaveUUID() string {
2022-09-21 02:37:49 +02:00
for {
rtn := uuid.New().String()
_, err := strconv.Atoi(rtn[0:8])
if err == nil { // do not allow UUIDs where the initial 8 bytes parse to an integer
continue
}
return rtn
}
}
func NumFormatDec(num int64) string {
var signStr string
absNum := num
if absNum < 0 {
absNum = -absNum
signStr = "-"
}
if absNum < 1000 {
// raw num
return signStr + strconv.FormatInt(absNum, 10)
}
if absNum < 1000000 {
// k num
kVal := float64(absNum) / 1000
return signStr + strconv.FormatFloat(kVal, 'f', 2, 64) + "k"
}
if absNum < 1000000000 {
// M num
mVal := float64(absNum) / 1000000
return signStr + strconv.FormatFloat(mVal, 'f', 2, 64) + "m"
} else {
// G num
gVal := float64(absNum) / 1000000000
return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "g"
}
}
func NumFormatB2(num int64) string {
var signStr string
absNum := num
if absNum < 0 {
absNum = -absNum
signStr = "-"
}
if absNum < 1024 {
// raw num
return signStr + strconv.FormatInt(absNum, 10)
}
if absNum < 1000000 {
// k num
if absNum%1024 == 0 {
return signStr + strconv.FormatInt(absNum/1024, 10) + "K"
}
kVal := float64(absNum) / 1024
return signStr + strconv.FormatFloat(kVal, 'f', 2, 64) + "K"
}
if absNum < 1000000000 {
// M num
if absNum%(1024*1024) == 0 {
return signStr + strconv.FormatInt(absNum/(1024*1024), 10) + "M"
}
mVal := float64(absNum) / (1024 * 1024)
return signStr + strconv.FormatFloat(mVal, 'f', 2, 64) + "M"
} else {
// G num
if absNum%(1024*1024*1024) == 0 {
return signStr + strconv.FormatInt(absNum/(1024*1024*1024), 10) + "G"
}
gVal := float64(absNum) / (1024 * 1024 * 1024)
return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "G"
}
}
2023-01-17 08:36:52 +01:00
func ClientArch() string {
return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
}
2023-02-24 00:17:47 +01:00
var releaseRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)`)
2023-02-24 00:17:47 +01:00
var osReleaseOnce = &sync.Once{}
var osRelease string
func unameKernelRelease() string {
2023-02-24 00:17:47 +01:00
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 {
2023-02-24 00:17:47 +01:00
log.Printf("invalid uname -r output: [%s]\n", releaseStr)
return "-"
}
return m[1]
2023-02-24 00:17:47 +01:00
}
func UnameKernelRelease() string {
2023-02-24 00:17:47 +01:00
osReleaseOnce.Do(func() {
osRelease = unameKernelRelease()
2023-02-24 00:17:47 +01:00
})
return osRelease
}
I18n fixes (#211) * remove byte sanitization for user commands When serializing jsonBytes in packet.go::MarshalPacket, a step existed that attempted to manually sanitize the bytes before sending them. This was initially done to avoid invalid characters in json; however, go should handle this for us. But this sanitization broke internationalization because it excluded characters required for unicode in other languages. Because of that, it has been removed. * properly decode non-ascii on frontend The functions atob and btoa do not convert base 64 to strings in the expected way. The base64ToArray function handles it properly but other cases do not. These other cases have been replaced with a helper function that makes use of the base64-js package. This package has already been included as a dependency of another package we use, but it was added to the package.json file to make the inclusion explicit. * automatically set/share LANG var with waveshell Waveterm previously did not set the LANG environment variable which caused problems for international users. On Linux, this is done automatically, but it needs to be done manually on macos. Even on linux, the wavesrv LANG variable is shared with waveshell to ensure the same one is used on remotes. * only set the lang var if not previously set In order to prevent waveterm from overriding the lang var entirely, this ensures that it is only manually determined if it hasn't previously been set. * use envMap instead of os to determine var This is slightly more performant and relies more directly on our code instead of external code.
2024-01-09 03:31:17 +01:00
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"
I18n fixes (#211) * remove byte sanitization for user commands When serializing jsonBytes in packet.go::MarshalPacket, a step existed that attempted to manually sanitize the bytes before sending them. This was initially done to avoid invalid characters in json; however, go should handle this for us. But this sanitization broke internationalization because it excluded characters required for unicode in other languages. Because of that, it has been removed. * properly decode non-ascii on frontend The functions atob and btoa do not convert base 64 to strings in the expected way. The base64ToArray function handles it properly but other cases do not. These other cases have been replaced with a helper function that makes use of the base64-js package. This package has already been included as a dependency of another package we use, but it was added to the package.json file to make the inclusion explicit. * automatically set/share LANG var with waveshell Waveterm previously did not set the LANG environment variable which caused problems for international users. On Linux, this is done automatically, but it needs to be done manually on macos. Even on linux, the wavesrv LANG variable is shared with waveshell to ensure the same one is used on remotes. * only set the lang var if not previously set In order to prevent waveterm from overriding the lang var entirely, this ensures that it is only manually determined if it hasn't previously been set. * use envMap instead of os to determine var This is slightly more performant and relies more directly on our code instead of external code.
2024-01-09 03:31:17 +01:00
} 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
}