2023-10-17 12:31:13 +08:00
|
|
|
// Copyright 2023, Command Line Inc.
|
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
2022-06-10 00:35:24 -07:00
|
|
|
package base
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2022-07-01 17:37:37 -07:00
|
|
|
"io"
|
2022-06-10 00:35:24 -07:00
|
|
|
"io/fs"
|
2022-11-28 18:05:54 -08:00
|
|
|
"log"
|
2022-06-10 00:35:24 -07:00
|
|
|
"os"
|
2022-06-23 10:16:54 -07:00
|
|
|
"os/exec"
|
2022-06-10 00:35:24 -07:00
|
|
|
"path"
|
2022-06-24 23:42:00 -07:00
|
|
|
"strings"
|
2022-08-08 09:52:50 -07:00
|
|
|
"sync"
|
2022-06-27 12:03:47 -07:00
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2022-09-26 13:02:34 -07:00
|
|
|
"golang.org/x/mod/semver"
|
2022-06-10 00:35:24 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const HomeVarName = "HOME"
|
2024-05-02 14:16:00 -07:00
|
|
|
const DefaultWaveshellHome = "~/.mshell"
|
|
|
|
|
const DefaultWaveshellName = "mshell"
|
|
|
|
|
const WaveshellPathVarName = "MSHELL_PATH"
|
|
|
|
|
const WaveshellHomeVarName = "MSHELL_HOME"
|
|
|
|
|
const WaveshellInstallBinVarName = "MSHELL_INSTALLBIN_PATH"
|
2022-07-01 17:37:37 -07:00
|
|
|
const SSHCommandVarName = "SSH_COMMAND"
|
2024-05-02 14:16:00 -07:00
|
|
|
const WaveshellDebugVarName = "MSHELL_DEBUG"
|
2022-07-01 17:37:37 -07:00
|
|
|
const SessionsDirBaseName = "sessions"
|
2023-10-25 15:20:25 -07:00
|
|
|
const RcFilesDirBaseName = "rcfiles"
|
2024-05-02 14:16:00 -07:00
|
|
|
const WaveshellVersion = "v0.7.0"
|
2022-07-01 17:37:37 -07:00
|
|
|
const RemoteIdFile = "remoteid"
|
2024-05-02 14:16:00 -07:00
|
|
|
const DefaultWaveshellInstallBinDir = "/opt/mshell/bin"
|
2022-11-28 18:05:54 -08:00
|
|
|
const LogFileName = "mshell.log"
|
|
|
|
|
const ForceDebugLog = false
|
2022-06-10 00:35:24 -07:00
|
|
|
|
2023-08-14 12:23:33 -07:00
|
|
|
const DebugFlag_LogRcFile = "logrc"
|
2024-03-06 11:38:27 -08:00
|
|
|
const DebugRcFileName = "debug.rcfile"
|
|
|
|
|
const DebugReturnStateFileName = "debug.returnstate"
|
2023-08-14 12:23:33 -07:00
|
|
|
|
2024-01-16 16:11:04 -08:00
|
|
|
const (
|
|
|
|
|
ProcessType_Unknown = "unknown"
|
|
|
|
|
ProcessType_WaveSrv = "wavesrv"
|
2024-02-15 17:42:43 -08:00
|
|
|
ProcessType_WaveShellSingle = "wsh-1"
|
|
|
|
|
ProcessType_WaveShellServer = "wsh-s"
|
2024-01-16 16:11:04 -08:00
|
|
|
)
|
|
|
|
|
|
2023-10-25 15:20:25 -07:00
|
|
|
// keys are sessionids (also the key RcFilesDirBaseName)
|
|
|
|
|
var ensureDirCache = make(map[string]bool)
|
2022-08-08 09:52:50 -07:00
|
|
|
var baseLock = &sync.Mutex{}
|
2022-11-28 18:05:54 -08:00
|
|
|
var DebugLogEnabled = false
|
|
|
|
|
var DebugLogger *log.Logger
|
2023-04-12 21:45:45 -07:00
|
|
|
var BuildTime string = "0"
|
2022-08-08 09:52:50 -07:00
|
|
|
|
2024-01-16 16:11:04 -08:00
|
|
|
var ProcessType string = ProcessType_Unknown
|
|
|
|
|
|
2022-06-10 00:35:24 -07:00
|
|
|
type CommandFileNames struct {
|
2022-06-10 21:37:21 -07:00
|
|
|
PtyOutFile string
|
|
|
|
|
StdinFifo string
|
|
|
|
|
RunnerOutFile string
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-27 12:03:47 -07:00
|
|
|
type CommandKey string
|
|
|
|
|
|
2023-04-12 21:45:45 -07:00
|
|
|
func SetBuildTime(build string) {
|
|
|
|
|
BuildTime = build
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 16:11:04 -08:00
|
|
|
func IsWaveSrv() bool {
|
|
|
|
|
return ProcessType == ProcessType_WaveSrv
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 23:38:05 -07:00
|
|
|
func MakeCommandKey(screenId string, lineId string) CommandKey {
|
|
|
|
|
if screenId == "" && lineId == "" {
|
2022-06-27 12:03:47 -07:00
|
|
|
return CommandKey("")
|
|
|
|
|
}
|
2024-03-20 23:38:05 -07:00
|
|
|
return CommandKey(fmt.Sprintf("%s/%s", screenId, lineId))
|
2022-06-27 12:03:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ckey CommandKey) IsEmpty() bool {
|
|
|
|
|
return string(ckey) == ""
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 18:05:54 -08:00
|
|
|
func Logf(fmtStr string, args ...interface{}) {
|
|
|
|
|
if (!DebugLogEnabled && !ForceDebugLog) || DebugLogger == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
DebugLogger.Printf(fmtStr, args...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func InitDebugLog(prefix string) {
|
2024-05-02 14:16:00 -07:00
|
|
|
homeDir := GetWaveshellHomeDir()
|
2022-11-28 18:05:54 -08:00
|
|
|
err := os.MkdirAll(homeDir, 0777)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
logFile := path.Join(homeDir, LogFileName)
|
|
|
|
|
fd, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
DebugLogger = log.New(fd, prefix+" ", log.LstdFlags)
|
2022-12-05 22:26:13 -08:00
|
|
|
Logf("logger initialized\n")
|
2022-11-28 18:05:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func SetEnableDebugLog(enable bool) {
|
|
|
|
|
DebugLogEnabled = enable
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-20 19:21:23 -07:00
|
|
|
// deprecated (use GetGroupId instead)
|
2022-06-27 12:03:47 -07:00
|
|
|
func (ckey CommandKey) GetSessionId() string {
|
2023-03-20 19:21:23 -07:00
|
|
|
return ckey.GetGroupId()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ckey CommandKey) GetGroupId() string {
|
2022-06-27 12:03:47 -07:00
|
|
|
slashIdx := strings.Index(string(ckey), "/")
|
|
|
|
|
if slashIdx == -1 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return string(ckey[0:slashIdx])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ckey CommandKey) GetCmdId() string {
|
|
|
|
|
slashIdx := strings.Index(string(ckey), "/")
|
|
|
|
|
if slashIdx == -1 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return string(ckey[slashIdx+1:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ckey CommandKey) Split() (string, string) {
|
|
|
|
|
fields := strings.SplitN(string(ckey), "/", 2)
|
|
|
|
|
if len(fields) < 2 {
|
|
|
|
|
return "", ""
|
|
|
|
|
}
|
|
|
|
|
return fields[0], fields[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ckey CommandKey) Validate(typeStr string) error {
|
|
|
|
|
if typeStr == "" {
|
|
|
|
|
typeStr = "ck"
|
|
|
|
|
}
|
|
|
|
|
if ckey == "" {
|
|
|
|
|
return fmt.Errorf("%s has empty commandkey", typeStr)
|
|
|
|
|
}
|
|
|
|
|
sessionId, cmdId := ckey.Split()
|
|
|
|
|
if sessionId == "" {
|
|
|
|
|
return fmt.Errorf("%s does not have sessionid", typeStr)
|
|
|
|
|
}
|
|
|
|
|
_, err := uuid.Parse(sessionId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%s has invalid sessionid '%s'", typeStr, sessionId)
|
|
|
|
|
}
|
|
|
|
|
if cmdId == "" {
|
|
|
|
|
return fmt.Errorf("%s does not have cmdid", typeStr)
|
|
|
|
|
}
|
|
|
|
|
_, err = uuid.Parse(cmdId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%s has invalid cmdid '%s'", typeStr, cmdId)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 12:23:33 -07:00
|
|
|
func HasDebugFlag(envMap map[string]string, flagName string) bool {
|
2024-05-02 14:16:00 -07:00
|
|
|
msDebug := envMap[WaveshellDebugVarName]
|
2023-08-14 12:23:33 -07:00
|
|
|
flags := strings.Split(msDebug, ",")
|
|
|
|
|
for _, flag := range flags {
|
|
|
|
|
if strings.TrimSpace(flag) == flagName {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetDebugRcFileName() string {
|
2024-05-02 14:16:00 -07:00
|
|
|
wsHome := GetWaveshellHomeDir()
|
|
|
|
|
return path.Join(wsHome, DebugRcFileName)
|
2024-03-06 11:38:27 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetDebugReturnStateFileName() string {
|
2024-05-02 14:16:00 -07:00
|
|
|
wsHome := GetWaveshellHomeDir()
|
|
|
|
|
return path.Join(wsHome, DebugReturnStateFileName)
|
2023-08-14 12:23:33 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-14 22:16:58 -07:00
|
|
|
func GetHomeDir() string {
|
|
|
|
|
homeVar := os.Getenv(HomeVarName)
|
|
|
|
|
if homeVar == "" {
|
|
|
|
|
return "/"
|
|
|
|
|
}
|
|
|
|
|
return homeVar
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 14:16:00 -07:00
|
|
|
func GetWaveshellHomeDir() string {
|
|
|
|
|
homeVar := os.Getenv(WaveshellHomeVarName)
|
2022-07-01 17:37:37 -07:00
|
|
|
if homeVar != "" {
|
|
|
|
|
return homeVar
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
2024-05-02 14:16:00 -07:00
|
|
|
return ExpandHomeDir(DefaultWaveshellHome)
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
|
|
|
|
|
2023-10-25 15:20:25 -07:00
|
|
|
func EnsureRcFilesDir() (string, error) {
|
2024-05-02 14:16:00 -07:00
|
|
|
mhome := GetWaveshellHomeDir()
|
2023-10-25 15:20:25 -07:00
|
|
|
dirName := path.Join(mhome, RcFilesDirBaseName)
|
|
|
|
|
err := CacheEnsureDir(dirName, RcFilesDirBaseName, 0700, "rcfiles dir")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return dirName, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 14:16:00 -07:00
|
|
|
func GetWaveshellPath() (string, error) {
|
|
|
|
|
wsPath := os.Getenv(WaveshellPathVarName) // use MSHELL_PATH -- will require rename
|
|
|
|
|
if wsPath != "" {
|
|
|
|
|
return exec.LookPath(wsPath)
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
2024-05-02 14:16:00 -07:00
|
|
|
mhome := GetWaveshellHomeDir()
|
|
|
|
|
userWaveshellPath := path.Join(mhome, DefaultWaveshellName) // look in ~/.mshell -- will require rename
|
|
|
|
|
wsPath, err := exec.LookPath(userWaveshellPath)
|
2022-07-01 17:37:37 -07:00
|
|
|
if err == nil {
|
2024-05-02 14:16:00 -07:00
|
|
|
return wsPath, nil
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
2024-05-02 14:16:00 -07:00
|
|
|
return exec.LookPath(DefaultWaveshellName) // standard path lookup for 'mshell'-- will require rename
|
2022-06-10 00:35:24 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-24 23:42:00 -07:00
|
|
|
func ExpandHomeDir(pathStr string) string {
|
|
|
|
|
if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") {
|
|
|
|
|
return pathStr
|
|
|
|
|
}
|
|
|
|
|
homeDir := GetHomeDir()
|
|
|
|
|
if pathStr == "~" {
|
|
|
|
|
return homeDir
|
|
|
|
|
}
|
|
|
|
|
return path.Join(homeDir, pathStr[2:])
|
|
|
|
|
}
|
2022-06-27 22:39:16 -07:00
|
|
|
|
|
|
|
|
func ValidGoArch(goos string, goarch string) bool {
|
|
|
|
|
return (goos == "darwin" || goos == "linux") && (goarch == "amd64" || goarch == "arm64")
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-26 13:02:34 -07:00
|
|
|
func GoArchOptFile(version string, goos string, goarch string) string {
|
2024-05-02 14:16:00 -07:00
|
|
|
installBinDir := os.Getenv(WaveshellInstallBinVarName)
|
2022-09-26 13:02:34 -07:00
|
|
|
if installBinDir == "" {
|
2024-05-02 14:16:00 -07:00
|
|
|
installBinDir = DefaultWaveshellInstallBinDir
|
2022-09-26 13:02:34 -07:00
|
|
|
}
|
|
|
|
|
versionStr := semver.MajorMinor(version)
|
|
|
|
|
if versionStr == "" {
|
|
|
|
|
versionStr = "unknown"
|
|
|
|
|
}
|
|
|
|
|
binBaseName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch)
|
|
|
|
|
return fmt.Sprintf(path.Join(installBinDir, binBaseName))
|
2022-06-27 22:39:16 -07:00
|
|
|
}
|
2022-07-01 17:37:37 -07:00
|
|
|
|
|
|
|
|
func GetRemoteId() (string, error) {
|
2024-05-02 14:16:00 -07:00
|
|
|
wsHome := GetWaveshellHomeDir()
|
|
|
|
|
homeInfo, err := os.Stat(wsHome)
|
2022-08-16 16:26:06 -07:00
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
2024-05-02 14:16:00 -07:00
|
|
|
err = os.MkdirAll(wsHome, 0777)
|
2022-08-16 16:26:06 -07:00
|
|
|
if err != nil {
|
2024-05-02 14:16:00 -07:00
|
|
|
return "", fmt.Errorf("cannot make waveshell home directory[%s]: %w", wsHome, err)
|
2022-08-16 16:26:06 -07:00
|
|
|
}
|
2024-05-02 14:16:00 -07:00
|
|
|
homeInfo, err = os.Stat(wsHome)
|
2022-08-16 16:26:06 -07:00
|
|
|
}
|
|
|
|
|
if err != nil {
|
2024-05-02 14:16:00 -07:00
|
|
|
return "", fmt.Errorf("cannot stat waveshell home directory[%s]: %w", wsHome, err)
|
2022-08-16 16:26:06 -07:00
|
|
|
}
|
|
|
|
|
if !homeInfo.IsDir() {
|
2024-05-02 14:16:00 -07:00
|
|
|
return "", fmt.Errorf("waveshell home directory[%s] is not a directory", wsHome)
|
2022-08-16 16:26:06 -07:00
|
|
|
}
|
2024-05-02 14:16:00 -07:00
|
|
|
remoteIdFile := path.Join(wsHome, RemoteIdFile)
|
2022-07-01 17:37:37 -07:00
|
|
|
fd, err := os.Open(remoteIdFile)
|
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
|
// write the file
|
|
|
|
|
remoteId := uuid.New().String()
|
|
|
|
|
err = os.WriteFile(remoteIdFile, []byte(remoteId), 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("cannot write remoteid to '%s': %w", remoteIdFile, err)
|
|
|
|
|
}
|
|
|
|
|
return remoteId, nil
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
return "", fmt.Errorf("cannot read remoteid file '%s': %w", remoteIdFile, err)
|
|
|
|
|
} else {
|
|
|
|
|
defer fd.Close()
|
|
|
|
|
contents, err := io.ReadAll(fd)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("cannot read remoteid file '%s': %w", remoteIdFile, err)
|
|
|
|
|
}
|
|
|
|
|
uuidStr := string(contents)
|
|
|
|
|
_, err = uuid.Parse(uuidStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("invalid uuid read from '%s': %w", remoteIdFile, err)
|
|
|
|
|
}
|
|
|
|
|
return uuidStr, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-03 23:26:57 -07:00
|
|
|
|
|
|
|
|
func BoundInt(ival int, minVal int, maxVal int) int {
|
|
|
|
|
if ival < minVal {
|
|
|
|
|
return minVal
|
|
|
|
|
}
|
|
|
|
|
if ival > maxVal {
|
|
|
|
|
return maxVal
|
|
|
|
|
}
|
|
|
|
|
return ival
|
|
|
|
|
}
|
2022-09-03 23:38:35 -07:00
|
|
|
|
|
|
|
|
func BoundInt64(ival int64, minVal int64, maxVal int64) int64 {
|
|
|
|
|
if ival < minVal {
|
|
|
|
|
return minVal
|
|
|
|
|
}
|
|
|
|
|
if ival > maxVal {
|
|
|
|
|
return maxVal
|
|
|
|
|
}
|
|
|
|
|
return ival
|
|
|
|
|
}
|
2023-10-25 15:20:25 -07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|