waveterm/wavesrv/pkg/scbase/scbase.go

419 lines
10 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"
2022-07-01 02:02:19 +02:00
"path"
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"
2024-03-07 22:06:34 +01:00
const MShellVersion = "v0.5.0"
// 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 = path.Join(homeVar, WaveDevDirName)
2022-12-28 22:56:19 +01:00
} else {
scHome = path.Join(homeVar, WaveDirName)
2022-12-28 22:56:19 +01:00
}
2022-07-01 02:02:19 +02:00
}
return scHome
}
func MShellBinaryDir() string {
appPath := os.Getenv(WaveAppPathVarName)
if appPath == "" {
appPath = "."
}
return path.Join(appPath, "bin", "mshell")
}
func MShellBinaryPath(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 := MShellBinaryDir()
versionStr := semver.MajorMinor(version)
if versionStr == "" {
return "", fmt.Errorf("invalid mshell version: %q", version)
}
fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch)
fullFileName := path.Join(binaryDir, fileName)
return fullFileName, nil
}
func LocalMShellBinaryPath() (string, error) {
return MShellBinaryPath(MShellVersion, runtime.GOOS, runtime.GOARCH)
}
func MShellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) {
mshellPath, err := MShellBinaryPath(version, goos, goarch)
if err != nil {
return nil, err
}
fd, err := os.Open(mshellPath)
if err != nil {
return nil, fmt.Errorf("cannot open mshell binary %q: %v", mshellPath, 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 := path.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 := path.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 = path.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 := path.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()
2023-03-21 03:20:57 +01:00
sdir = path.Join(scHome, ScreensDirBaseName, screenId)
err := ensureDir(sdir)
if err != nil {
return "", err
}
BaseLock.Lock()
ScreenDirCache[screenId] = sdir
BaseLock.Unlock()
return sdir, nil
}
func GetScreensDir() string {
waveHome := GetWaveHomeDir()
sdir := path.Join(waveHome, ScreensDirBaseName)
2023-03-21 03:20:57 +01:00
return sdir
}
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
}