Files
herolib/lib/osal/tmux/tmux_session.v
2025-10-12 12:30:19 +03:00

378 lines
10 KiB
V

module tmux
import incubaid.herolib.osal.core as osal
import incubaid.herolib.core.texttools
import os
import incubaid.herolib.ui.console
@[heap]
struct Session {
pub mut:
tmux &Tmux @[str: skip] // reference back
windows []&Window // session has windows
name string
}
@[params]
pub struct WindowArgs {
pub mut:
name string
cmd string
env map[string]string
reset bool
}
@[params]
pub struct WindowGetArgs {
pub mut:
name string
id int
}
pub fn (mut s Session) create() ! {
// Check if session already exists
cmd_check := 'tmux has-session -t ${s.name}'
check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or {
// Session doesn't exist, this is expected
osal.Job{}
}
if check_result.exit_code == 0 {
return error('duplicate session: ${s.name}')
}
// Create new session
cmd := 'tmux new-session -d -s ${s.name}'
osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or {
return error("Can't create session ${s.name}: ${err}")
}
}
// load info from reality
pub fn (mut s Session) scan() ! {
// Get current windows from tmux for this session
cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'"
result := osal.execute_silent(cmd) or {
if err.msg().contains('session not found') {
return
}
return error('Cannot list windows for session ${s.name}: ${err}')
}
mut current_windows := map[string]bool{}
for line in result.split_into_lines() {
line_trimmed := line.trim_space()
if line_trimmed.len == 0 {
continue
}
if line_trimmed.contains('|') {
parts := line_trimmed.split('|')
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
// Safely extract window name with additional validation
raw_window_name := parts[0].trim_space()
if raw_window_name.len == 0 {
continue
}
// Use safer name processing instead of texttools.name_fix
mut window_name := raw_window_name.to_lower().trim_space()
// Replace problematic characters with underscores
window_name = window_name.replace(' ', '_').replace('-', '_').replace('.',
'_')
// Remove any non-ASCII characters safely
mut safe_name := ''
for c in window_name {
if c.is_letter() || c.is_digit() || c == `_` {
safe_name += c.ascii_str()
}
}
window_name = safe_name
if window_name.len == 0 {
continue
}
window_id := parts[1].replace('@', '').int()
window_active := parts[2] == '1'
// Safe map assignment
current_windows[window_name] = true
// Update existing window or create new one
mut found := false
for mut w in s.windows {
if w.name.len > 0 && window_name.len > 0 && w.name == window_name {
w.id = window_id
w.active = window_active
w.scan()! // Scan panes for this window
found = true
break
}
}
if !found {
mut new_window := Window{
session: &s
name: window_name
id: window_id
active: window_active
panes: []&Pane{}
env: map[string]string{}
}
new_window.scan()! // Scan panes for new window
s.windows << &new_window
}
}
}
}
// Remove windows that no longer exist in tmux
mut valid_windows := []&Window{}
for window in s.windows {
// Safety check: ensure window.name is valid
if window.name.len > 0 {
// Avoid map access entirely - check if window still exists by comparing with current windows
mut window_exists := false
for current_name, _ in current_windows {
if window.name == current_name {
window_exists = true
break
}
}
if window_exists {
valid_windows << window
}
}
}
s.windows = valid_windows
}
// window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file
// environment arguments to use
// reset, if reset it will create window even if it does already exist, will destroy it
// ```
// struct WindowArgs {
// pub mut:
// name string
// cmd string
// env map[string]string
// reset bool
// }
// ```
pub fn (mut s Session) window_new(args WindowArgs) !&Window {
$if debug {
console.print_header(' start window: \n${args}')
}
namel := texttools.name_fix(args.name)
if s.window_exist(name: namel) {
if args.reset {
s.window_delete(name: namel)!
} else {
return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
}
}
mut w := &Window{
session: &s
name: namel
panes: []&Pane{}
env: args.env
}
s.windows << &w
// Create the window with the specified command
w.create(args.cmd)!
s.scan()!
return w
}
// get all windows as found in a session
pub fn (mut s Session) windows_get() []&Window {
mut res := []&Window{}
// os.log('TMUX - Start listing ....')
for _, window in s.windows {
res << window
}
return res
}
// List windows in a session
pub fn (mut s Session) window_list() []&Window {
return s.windows
}
pub fn (mut s Session) window_names() []string {
mut res := []string{}
for _, window in s.windows {
res << window.name
}
return res
}
pub fn (mut s Session) str() string {
mut out := '## Session: ${s.name}\n\n'
for _, w in s.windows {
out += '${*w}\n'
}
return out
}
pub fn (mut s Session) stats() !ProcessStats {
mut total := ProcessStats{}
for mut window in s.windows {
stats := window.stats() or { continue }
total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent
}
return total
}
// pub fn (mut s Session) activate()! {
// active_session := s.tmux.redis.get('tmux:active_session') or { 'No active session found' }
// if active_session != 'No active session found' && s.name != active_session {
// s.tmuxexecutor.db.exec('tmux attach-session -t $active_session') or {
// return error('Fail to attach to current active session: $active_session \n$err')
// }
// s.tmuxexecutor.db.exec('tmux switch -t $s.name') or {
// return error("Can't switch to session $s.name \n$err")
// }
// s.tmux.redis.set('tmux:active_session', s.name) or { panic('Failed to set tmux:active_session') }
// os.log('SESSION - Session: $s.name activated ')
// } else if active_session == 'No active session found' {
// s.tmux.redis.set('tmux:active_session', s.name) or { panic('Failed to set tmux:active_session') }
// os.log('SESSION - Session: $s.name activated ')
// } else {
// os.log('SESSION - Session: $s.name already activate ')
// }
// }
fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_
s.window_get(args) or { return false }
return true
}
pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
mut args := args_
if args.name.len == 0 {
return error('Window name cannot be empty')
}
args.name = texttools.name_fix_token(args.name)
for w in s.windows {
if w.name.len > 0 && w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
return w
}
}
}
return error('Cannot find window ${args.name} in session:${s.name}')
}
pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
// $if debug { console.print_debug(" - window delete: $args_")}
mut args := args_
args.name = texttools.name_fix_token(args.name)
if !(s.window_exist(args)) {
return
}
mut i := 0
for mut w in s.windows {
if w.name == args.name {
if (args.id > 0 && w.id == args.id) || args.id == 0 {
// Enhanced cleanup: kill all processes before stopping the window
w.kill_all_processes() or {
console.print_debug('Failed to kill processes in window ${w.name}: ${err}')
}
w.stop()!
break
}
}
i += 1
}
s.windows.delete(i) // i is now the one in the list which needs to be removed
}
pub fn (mut s Session) restart() ! {
s.stop()!
s.create()!
}
pub fn (mut s Session) stop() ! {
// First, kill all processes in all windows and panes of this session
s.kill_all_processes()!
// Then kill the tmux session itself
osal.execute_silent('tmux kill-session -t ${s.name}') or { return }
}
// Kill all processes in all windows and panes of this session
pub fn (mut s Session) kill_all_processes() ! {
console.print_debug('Killing all processes in session ${s.name}')
// Refresh session information to get current state
s.scan()!
// Kill processes in each window
for mut window in s.windows {
window.kill_all_processes() or {
console.print_debug('Failed to kill processes in window ${window.name}: ${err}')
// Continue with other windows even if one fails
}
}
}
// Run ttyd for this session so it can be accessed in the browser
pub fn (mut s Session) run_ttyd(args TtydArgs) ! {
// Check if the port is available before starting ttyd
osal.port_check_available(args.port) or {
return error('Cannot start ttyd for session ${s.name}: ${err}')
}
target := '${s.name}'
// Add -W flag for write access if editable mode is enabled
mut ttyd_flags := '-p ${args.port}'
if args.editable {
ttyd_flags += ' -W'
}
cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &'
code := os.system(cmd)
if code != 0 {
return error('Failed to start ttyd on port ${args.port} for session ${s.name}')
}
mode_str := if args.editable { 'editable' } else { 'read-only' }
println('ttyd started for session ${s.name} at http://localhost:${args.port} (${mode_str} mode)')
}
// Backward compatibility method - runs ttyd in read-only mode
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
s.run_ttyd(port: port, editable: false)!
}
// Stop ttyd for this session by killing the process on the specified port
pub fn (mut s Session) stop_ttyd(port int) ! {
// Kill any process running on the specified port
cmd := 'lsof -ti:${port} | xargs kill -9'
osal.execute_silent(cmd) or {
// Ignore error if no process is found on the port
// This is normal when no ttyd is running on that port
}
println('ttyd stopped for session ${s.name} on port ${port} (if it was running)')
}
// Stop all ttyd processes (kills all ttyd processes system-wide)
pub fn stop_all_ttyd() ! {
cmd := 'pkill ttyd'
osal.execute_silent(cmd) or {
// Ignore error if no ttyd processes are found (exit code 1)
// This is normal when no ttyd processes are running
}
println('All ttyd processes stopped (if any were running)')
}