195 lines
5.4 KiB
Go
195 lines
5.4 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var dedicatedWorkspace = "sshkeeper"
|
|
|
|
type Window struct {
|
|
ID string
|
|
Index int
|
|
Name string
|
|
ServerAlias string
|
|
Active bool
|
|
StartedAt time.Time
|
|
}
|
|
|
|
func Available() bool {
|
|
if runtime.GOOS == "windows" {
|
|
return false
|
|
}
|
|
_, err := exec.LookPath("tmux")
|
|
return err == nil
|
|
}
|
|
func workspaceTarget() (string, bool, error) {
|
|
if !Available() {
|
|
return "", false, fmt.Errorf("tmux is unavailable")
|
|
}
|
|
if os.Getenv("TMUX") == "" {
|
|
return dedicatedWorkspace, false, nil
|
|
}
|
|
out, err := exec.Command("tmux", "display-message", "-p", "#{session_name}").Output()
|
|
if err != nil {
|
|
return "", true, fmt.Errorf("resolve current tmux session: %w", err)
|
|
}
|
|
name := strings.TrimSpace(string(out))
|
|
if name == "" {
|
|
return "", true, fmt.Errorf("current tmux session has no name")
|
|
}
|
|
return name, true, nil
|
|
}
|
|
|
|
func sessionExists(target string) bool {
|
|
cmd := exec.Command("tmux", "has-session", "-t", target)
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
|
}
|
|
func Open(serverAlias string) (string, bool, error) {
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("resolve sshkeeper executable: %w", err)
|
|
}
|
|
command := shellQuote(executable) + " __session-connect " + shellQuote(serverAlias)
|
|
return openWindow(serverAlias, command)
|
|
}
|
|
|
|
func openWindow(serverAlias, command string) (string, bool, error) {
|
|
target, insideTmux, err := workspaceTarget()
|
|
if err != nil {
|
|
return "", insideTmux, err
|
|
}
|
|
name := sanitizeWindowName(serverAlias)
|
|
var args []string
|
|
if !insideTmux && !sessionExists(target) {
|
|
args = []string{"new-session", "-d", "-P", "-F", "#{window_id}", "-s", target, "-n", name, command}
|
|
} else {
|
|
args = []string{"new-window", "-d", "-P", "-F", "#{window_id}", "-t", target, "-n", name, command}
|
|
}
|
|
out, err := exec.Command("tmux", args...).CombinedOutput()
|
|
if err != nil {
|
|
return "", insideTmux, fmt.Errorf("create tmux session window: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
windowID := strings.TrimSpace(string(out))
|
|
if windowID == "" {
|
|
return "", insideTmux, fmt.Errorf("tmux did not return a window id")
|
|
}
|
|
if err := setWindowMetadata(windowID, serverAlias, time.Now()); err != nil {
|
|
return "", insideTmux, err
|
|
}
|
|
return windowID, insideTmux, nil
|
|
}
|
|
|
|
func sanitizeWindowName(alias string) string {
|
|
name := strings.TrimSpace(alias)
|
|
if name == "" {
|
|
return "ssh"
|
|
}
|
|
name = strings.ReplaceAll(name, ":", "-")
|
|
name = strings.ReplaceAll(name, " ", "-")
|
|
if len(name) > 40 {
|
|
name = name[:40]
|
|
}
|
|
return name
|
|
}
|
|
|
|
func setWindowMetadata(windowID, alias string, started time.Time) error {
|
|
pairs := [][2]string{
|
|
{"@sshkeeper_server", alias},
|
|
{"@sshkeeper_started", strconv.FormatInt(started.Unix(), 10)},
|
|
}
|
|
for _, pair := range pairs {
|
|
out, err := exec.Command("tmux", "set-window-option", "-t", windowID, pair[0], pair[1]).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("set tmux metadata %s: %s: %w", pair[0], strings.TrimSpace(string(out)), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func List() ([]Window, error) {
|
|
if !Available() {
|
|
return nil, nil
|
|
}
|
|
target, _, err := workspaceTarget()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !sessionExists(target) {
|
|
return nil, nil
|
|
}
|
|
format := "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}\t#{@sshkeeper_server}\t#{@sshkeeper_started}"
|
|
out, err := exec.Command("tmux", "list-windows", "-t", target, "-F", format).CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list tmux windows: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
var result []Window
|
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
parts := strings.Split(line, "\t")
|
|
if len(parts) < 6 || strings.TrimSpace(parts[4]) == "" {
|
|
continue
|
|
}
|
|
index, _ := strconv.Atoi(parts[1])
|
|
startedUnix, _ := strconv.ParseInt(parts[5], 10, 64)
|
|
window := Window{ID: parts[0], Index: index, Name: parts[2], Active: parts[3] == "1", ServerAlias: parts[4]}
|
|
if startedUnix > 0 {
|
|
window.StartedAt = time.Unix(startedUnix, 0)
|
|
}
|
|
result = append(result, window)
|
|
}
|
|
return result, nil
|
|
}
|
|
func Attach(windowID string) error {
|
|
if !Available() {
|
|
return fmt.Errorf("tmux is unavailable")
|
|
}
|
|
if strings.TrimSpace(windowID) == "" {
|
|
return fmt.Errorf("tmux window id is required")
|
|
}
|
|
if os.Getenv("TMUX") != "" {
|
|
out, err := exec.Command("tmux", "select-window", "-t", windowID).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("select tmux window: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
return nil
|
|
}
|
|
target, _, err := workspaceTarget()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out, err := exec.Command("tmux", "select-window", "-t", windowID).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("select tmux window: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
cmd := exec.Command("tmux", "attach-session", "-t", target)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("attach tmux session: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Close(windowID string) error {
|
|
if !Available() {
|
|
return fmt.Errorf("tmux is unavailable")
|
|
}
|
|
out, err := exec.Command("tmux", "kill-window", "-t", windowID).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("close tmux window: %s: %w", strings.TrimSpace(string(out)), err)
|
|
}
|
|
return nil
|
|
}
|