This commit is contained in:
2024-12-25 08:40:56 +01:00
parent 97e896b1a2
commit 4a50de92e3
169 changed files with 16476 additions and 1 deletions

24
lib/osal/tmux/readme.md Normal file
View File

@@ -0,0 +1,24 @@
# TMUX
TMUX is a very capable process manager.
### Concepts
- tmux = is the factory, it represents the tmux process manager, linked to a node
- session = is a set of windows, it has a name and groups windows
- window = is typically one process running (you can have panes but in our implementation we skip this)
## structure
tmux library provides functions for managing tmux sessions
- session is the top one
- then windows (is where you see the app running)
- then panes in windows (we don't support yet)
## to attach to a tmux session
> TODO:

View File

@@ -0,0 +1,86 @@
module tmux
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.installers.tmux
// fn testsuite_end() {
//
// }
fn testsuite_begin() {
mut tmux := Tmux{}
if tmux.is_running()! {
tmux.stop()!
}
}
fn test_session_create() {
// installer := tmux.get_install(
// panic('could not install tmux: ${err}')
// }
mut tmux := Tmux{}
tmux.start() or { panic('cannot start tmux: ${err}') }
mut s := Session{
tmux: &tmux
windows: map[string]&Window{}
name: 'testsession'
}
mut s2 := Session{
tmux: &tmux
windows: map[string]&Window{}
name: 'testsession2'
}
// test testsession exists after session_create
mut tmux_ls := osal.execute_silent('tmux ls') or { panic("can't exec: ${err}") }
assert !tmux_ls.contains('testsession: 1 windows')
s.create() or { panic('Cannot create session: ${err}') }
tmux_ls = osal.execute_silent('tmux ls') or { panic("can't exec: ${err}") }
assert tmux_ls.contains('testsession: 1 windows')
// test multiple session_create for same tmux
tmux_ls = osal.execute_silent('tmux ls') or { panic("can't exec: ${err}") }
assert !tmux_ls.contains('testsession2: 1 windows')
s2.create() or { panic('Cannot create session: ${err}') }
tmux_ls = osal.execute_silent('tmux ls') or { panic("can't exec: ${err}") }
assert tmux_ls.contains('testsession2: 1 windows')
// test session_create with duplicate session
mut create_err := ''
s2.create() or { create_err = err.msg() }
assert create_err != ''
assert create_err.contains('duplicate session: testsession2')
tmux_ls = osal.execute_silent('tmux ls') or { panic("can't exec: ${err}") }
assert tmux_ls.contains('testsession2: 1 windows')
s.stop() or { panic('Cannot stop session: ${err}') }
s2.stop() or { panic('Cannot stop session: ${err}') }
}
// fn test_session_stop() {
//
// installer := tmux.get_install(
// mut tmux := Tmux {
// node: node_ssh
// }
// mut s := Session{
// tmux: &tmux // reference back
// windows: map[string]&Window{}
// name: 'testsession3'
// }
// s.create() or { panic("Cannot create session: $err") }
// mut tmux_ls := osal.execute_silent('tmux ls') or { panic("can't exec: $err") }
// assert tmux_ls.contains("testsession3: 1 windows")
// s.stop() or { panic("Cannot stop session: $err")}
// tmux_ls = osal.execute_silent('tmux ls') or { panic("can't exec: $err") }
// assert !tmux_ls.contains("testsession3: 1 windows")
// }

View File

@@ -0,0 +1,67 @@
module tmux
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.installers.tmux
import freeflowuniverse.herolib.ui.console
// uses single tmux instance for all tests
__global (
tmux Tmux
)
fn init() {
tmux = get_remote('185.69.166.152')!
// reset tmux for tests
if tmux.is_running() {
tmux.stop() or { panic('Cannot stop tmux') }
}
}
fn testsuite_end() {
if tmux.is_running() {
tmux.stop()!
}
}
fn test_window_new() {
tmux.start() or { panic("can't start tmux: ${err}") }
// test window new with only name arg
window_args := WindowArgs{
name: 'TestWindow'
}
assert !tmux.sessions.keys().contains('main')
mut window := tmux.window_new(window_args) or { panic("Can't create new window: ${err}") }
assert tmux.sessions.keys().contains('main')
window.delete() or { panic('Cant delete window') }
}
// // tests creating duplicate windows
// fn test_window_new0() {
//
// installer := tmux.get_install(
// mut tmux := Tmux {
// node: node_ssh
// }
// window_args := WindowArgs {
// name: 'TestWindow0'
// }
// // console.print_debug(tmux)
// mut window := tmux.window_new(window_args) or {
// panic("Can't create new window: $err")
// }
// assert tmux.sessions.keys().contains('main')
// mut window_dup := tmux.window_new(window_args) or {
// panic("Can't create new window: $err")
// }
// console.print_debug(node_ssh.exec('tmux ls') or { panic("fail:$err")})
// window.delete() or { panic("Cant delete window") }
// // console.print_debug(tmux)
// }

116
lib/osal/tmux/tmux.v Normal file
View File

@@ -0,0 +1,116 @@
module tmux
import freeflowuniverse.herolib.osal
// import freeflowuniverse.herolib.session
import os
import time
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct Tmux {
pub mut:
sessions []&Session
sessionid string // unique link to job
}
@[params]
pub struct TmuxNewArgs {
sessionid string
}
// return tmux instance
pub fn new(args TmuxNewArgs) !Tmux {
mut t := Tmux{
sessionid: args.sessionid
}
t.load()!
t.scan()!
return t
}
// loads tmux session, populate the object
pub fn (mut tmux Tmux) load() ! {
isrunning := tmux.is_running()!
if !isrunning {
tmux.start()!
}
// console.print_debug("SCAN")
tmux.scan()!
}
pub fn (mut t Tmux) stop() ! {
$if debug {
console.print_debug('Stopping tmux...')
}
t.sessions = []&Session{}
t.scan()!
for _, mut session in t.sessions {
session.stop()!
}
cmd := 'tmux kill-server'
_ := osal.exec(cmd: cmd, stdout: false, name: 'tmux_kill_server', ignore_error: true) or {
panic('bug')
}
os.log('TMUX - All sessions stopped .')
}
pub fn (mut t Tmux) start() ! {
cmd := 'tmux new-sess -d -s main'
_ := osal.exec(cmd: cmd, stdout: false, name: 'tmux_start') or {
return error("Can't execute ${cmd} \n${err}")
}
// scan and add default bash window created with session init
time.sleep(time.Duration(100 * time.millisecond))
t.scan()!
}
// print list of tmux sessions
pub fn (mut t Tmux) list_print() {
// os.log('TMUX - Start listing ....')
for _, session in t.sessions {
for _, window in session.windows {
console.print_debug(window)
}
}
}
// get all windows as found in all sessions
pub fn (mut t Tmux) windows_get() []&Window {
mut res := []&Window{}
// os.log('TMUX - Start listing ....')
for _, session in t.sessions {
for _, window in session.windows {
res << window
}
}
return res
}
// checks whether tmux server is running
pub fn (mut t Tmux) is_running() !bool {
res := osal.exec(cmd: 'tmux info', stdout: false, name: 'tmux_info', raise_error: false) or {
panic('bug')
}
if res.error.contains('no server running') {
// console.print_debug(" TMUX NOT RUNNING")
return false
}
if res.error.contains('no current client') {
return true
}
if res.exit_code > 0 {
return error('could not execute tmux info.\n${res}')
}
return true
}
pub fn (mut t Tmux) str() string {
mut out := '# Tmux\n\n'
for s in t.sessions {
out += '${*s}\n'
}
return out
}

95
lib/osal/tmux/tmux_scan.v Normal file
View File

@@ -0,0 +1,95 @@
module tmux
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.ui.console
fn (mut t Tmux) scan_add(line string) !&Window {
// console.print_debug(" -- scan add: $line")
if line.count('|') < 4 {
return error(@FN + 'expects line with at least 5 params separated by |')
}
line_arr := line.split('|')
session_name := line_arr[0]
window_name := line_arr[1]
window_id := line_arr[2]
pane_active := line_arr[3]
pane_id := line_arr[4]
pane_pid := line_arr[5]
pane_start_command := line_arr[6] or { '' }
wid := (window_id.replace('@', '')).int()
// os.log('TMUX FOUND: $line\n ++ $session_name:$window_name wid:$window_id pid:$pane_pid entrypoint:$pane_start_command')
mut s := t.session_get(session_name)!
mut active := false
if pane_active == '1' {
active = true
}
mut name := texttools.name_fix(window_name)
mut w := Window{
session: s
name: name
}
if !(s.window_exist(name: window_name, id: wid)) {
// console.print_debug("window not exists")
s.windows << &w
} else {
w = s.window_get(name: window_name, id: wid)!
}
w.id = wid
w.active = active
w.pid = pane_pid.int()
w.paneid = (pane_id.replace('%', '')).int()
w.cmd = pane_start_command
return &w
}
// scan the system to detect sessions .
pub fn (mut t Tmux) scan() ! {
// os.log('TMUX - Scanning ....')
cmd_list_session := "tmux list-sessions -F '#{session_name}'"
exec_list := osal.exec(cmd: cmd_list_session, stdout: false, name: 'tmux_list') or {
return error('could not execute list sessions.\n${err}')
}
// console.print_debug('execlist out for sessions: ${exec_list}')
// make sure we have all sessions
for line in exec_list.output.split_into_lines() {
session_name := line.trim(' \n').to_lower()
if session_name == '' {
continue
}
if t.session_exist(session_name) {
continue
}
mut s := Session{
tmux: &t // reference back
name: session_name
}
t.sessions << &s
}
console.print_debug(t)
// mut done := map[string]bool{}
cmd := "tmux list-panes -a -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'"
out := osal.execute_silent(cmd) or { return error("Can't execute ${cmd} \n${err}") }
// $if debug{console.print_debug('tmux list panes out:\n${out}')}
for line in out.split_into_lines() {
if line.contains('|') {
t.scan_add(line)!
}
}
}

View File

@@ -0,0 +1,153 @@
module tmux
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.core.texttools
import os
import freeflowuniverse.herolib.ui.console
@[heap]
struct Session {
pub mut:
tmux &Tmux @[str: skip] // reference back
windows []&Window // session has windows
name string
}
// get session (session has windows) .
// returns none if not found
pub fn (mut t Tmux) session_get(name_ string) !&Session {
name := texttools.name_fix(name_)
for s in t.sessions {
if s.name == name {
return s
}
}
return error('Can not find session with name: \'${name_}\', out of loaded sessions.')
}
pub fn (mut t Tmux) session_exist(name_ string) bool {
name := texttools.name_fix(name_)
t.session_get(name) or { return false }
return true
}
pub fn (mut t Tmux) session_delete(name_ string) ! {
if !(t.session_exist(name_)) {
return
}
name := texttools.name_fix(name_)
mut i := 0
for mut s in t.sessions {
if s.name == name {
s.stop()!
break
}
i += 1
}
t.sessions.delete(i)
}
@[params]
pub struct SessionCreateArgs {
pub mut:
name string @[required]
reset bool
}
// create session, if reset will re-create
pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session {
name := texttools.name_fix(args.name)
if !(t.session_exist(name)) {
$if debug {
console.print_header(' tmux - create session: ${args}')
}
mut s2 := Session{
tmux: t // reference back
name: name
}
s2.create()!
t.sessions << &s2
}
mut s := t.session_get(name)!
if args.reset {
$if debug {
console.print_header(' tmux - session ${name} will be restarted.')
}
s.restart()!
}
t.scan()!
return s
}
pub fn (mut s Session) create() ! {
res_opt := "-P -F '#\{window_id\}'"
cmd := "tmux new-session ${res_opt} -d -s ${s.name} 'sh'"
window_id_ := osal.execute_silent(cmd) or {
return error("Can't create tmux session ${s.name} \n${cmd}\n${err}")
}
cmd3 := 'tmux set-option remain-on-exit on'
osal.execute_silent(cmd3) or { return error("Can't execute ${cmd3}\n${err}") }
window_id := window_id_.trim(' \n')
cmd2 := "tmux rename-window -t ${window_id} 'notused'"
osal.execute_silent(cmd2) or {
return error("Can't rename window ${window_id} to notused \n${cmd2}\n${err}")
}
}
pub fn (mut s Session) restart() ! {
s.stop()!
s.create()!
}
pub fn (mut s Session) stop() ! {
osal.execute_silent('tmux kill-session -t ${s.name}') or {
return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}")
}
}
// 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
}
pub fn (mut s Session) windownames_get() []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) 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 ')
// }
// }

118
lib/osal/tmux/tmux_test.v Normal file
View File

@@ -0,0 +1,118 @@
module tmux
import freeflowuniverse.herolib.osal
// import freeflowuniverse.herolib.installers.tmux
import os
import freeflowuniverse.herolib.ui.console
const testpath = os.dir(@FILE) + '/testdata'
// make sure tmux isn't running prior to test
fn testsuite_begin() {
mut tmux := get_remote('185.69.166.152')!
if tmux.is_running() {
tmux.stop()!
}
}
// make sure tmux isn't running after test
fn testsuite_end() {
mut tmux := get_remote('185.69.166.152')!
if tmux.is_running() {
tmux.stop()!
}
}
fn test_start() ! {
mut tmux := get_remote('185.69.166.152')!
// test server is running after start()
tmux.start() or { panic('cannot start tmux: ${err}') }
mut tmux_ls := osal.execute_silent('tmux ls') or { panic('Cannot execute tmux ls: ${err}') }
// test started tmux contains windows
assert tmux_ls.contains('init: 1 windows')
tmux.stop() or { panic('cannot stop tmux: ${err}') }
}
fn test_stop() ! {
mut tmux := get_remote('185.69.166.152')!
// test server is running after start()
tmux.start() or { panic('cannot start tmux: ${err}') }
assert tmux.is_running()
tmux.stop() or { panic('cannot stop tmux: ${err}') }
assert !tmux.is_running()
}
fn test_windows_get() ! {
mut tmux := get_remote('185.69.166.152')!
// test windows_get when only starting window is running
tmux.start()!
mut windows := tmux.windows_get()
assert windows.len == 1
// test getting newly created window
tmux.window_new(WindowArgs{ name: 'testwindow' })!
windows = tmux.windows_get()
unsafe {
assert windows.keys().contains('testwindow')
}
assert windows['testwindow'].name == 'testwindow'
assert windows['testwindow'].active
tmux.stop()!
}
// TODO: fix test
fn test_scan() ! {
console.print_debug('-----Testing scan------')
mut tmux := get_remote('185.69.166.152')!
tmux.start()!
// check bash window is initialized
mut new_windows := tmux.windows_get()
unsafe {
assert new_windows.keys() == ['bash']
}
// test scan, should return no windows
mut windows := tmux.windows_get()
unsafe {
assert windows.keys().len == 0
}
// test scan with window in tmux but not in tmux struct
// mocking a failed command to see if scan identifies
tmux.sessions['init'].windows['test'] = &Window{
session: tmux.sessions['init']
name: 'test'
}
new_windows = tmux.windows_get()
panic('new windows ${new_windows.keys()}')
unsafe {
assert new_windows.keys().len == 1
}
new_windows = tmux.scan()!
tmux.stop()!
}
// //TODO: fix test
// fn test_scan_add() ! {
// console.print_debug("-----Testing scan_add------")
//
// mut tmux := Tmux { node: node_ssh }
// windows := tmux.scan_add("line")!
// }
// remaining tests are run synchronously to avoid conflicts
fn test_tmux_window() {
res := os.execute('${os.quoted_path(@VEXE)} test ${testpath}/tmux_window_test.v')
// assert res.exit_code == 1
// assert res.output.contains('other_test.v does not exist')
}
fn test_tmux_scan() {
res := os.execute('${os.quoted_path(@VEXE)} test ${testpath}/tmux_window_test.v')
// assert res.exit_code == 1
// assert res.output.contains('other_test.v does not exist')
}

257
lib/osal/tmux/tmux_window.v Normal file
View File

@@ -0,0 +1,257 @@
module tmux
import os
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.data.ourtime
import time
import freeflowuniverse.herolib.ui.console
@[heap]
struct Window {
pub mut:
session &Session @[skip]
name string
id int
active bool
pid int
paneid int
cmd string
env map[string]string
}
pub struct WindowArgs {
pub mut:
name string
cmd string
env map[string]string
reset bool
}
// 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 t Tmux) window_new(args WindowArgs) !Window {
mut s := t.session_create(name: 'main', reset: false)!
mut w := s.window_new(args)!
return w
}
// is always in the main tmux
pub fn (mut t Tmux) window_delete(args WindowGetArgs) ! {
mut s := t.session_create(name: 'main', reset: false)!
s.window_delete(name: args.name)!
}
// 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
cmd: args.cmd
env: args.env
}
s.windows << &w
w.create()!
s.window_delete(name: 'notused')!
return w
}
pub struct WindowGetArgs {
pub mut:
name string
cmd string
id int
}
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_
args.name = texttools.name_fix(args.name)
for w in s.windows {
if 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(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 {
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 w Window) create() ! {
// tmux new-window -P -c /tmp -e good=1 -e bad=0 -n koekoe -t main bash
if w.cmd.contains('\n') {
// means is multiline need to write it
// scriptpath string // is the path where the script will be put which is executed
// scriptkeep bool // means we don't remove the script
os.mkdir_all('/tmp/tmux/${w.session.name}')!
cmd_new := osal.exec_string(
cmd: w.cmd
scriptpath: '/tmp/tmux/${w.session.name}/${w.name}.sh'
scriptkeep: true
)!
w.cmd = cmd_new
}
// console.print_debug(w)
if w.active == false {
res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'"
cmd := 'tmux new-window ${res_opt} -t ${w.session.name} -n ${w.name} \'/bin/bash -c ${w.cmd}\''
console.print_debug(cmd)
res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_window_create') or {
return error("Can't create new window ${w.name} \n${cmd}\n${err}")
}
// now look at output to get the window id = wid
line_arr := res.output.split('|')
wid := line_arr[2] or { panic('cannot split line for window create.\n${line_arr}') }
w.id = wid.replace('@', '').int()
$if debug {
console.print_header(' WINDOW - Window: ${w.name} created in session: ${w.session.name}')
}
} else {
return error('cannot create window, it already exists.\n${w.name}:${w.id}:${w.cmd}')
}
}
// do some good checks if the window is still active
// not implemented yet
pub fn (mut w Window) check() ! {
panic('not implemented yet')
}
// restart the window
pub fn (mut w Window) restart() ! {
w.stop()!
w.create()!
}
// stop the window
pub fn (mut w Window) stop() ! {
osal.exec(
cmd: 'tmux kill-window -t @${w.id}'
stdout: false
name: 'tmux_kill-window'
die: false
) or { return error("Can't kill window with id:${w.id}") }
w.pid = 0
w.active = false
}
pub fn (window Window) str() string {
return ' - name:${window.name} wid:${window.id} active:${window.active} pid:${window.pid} cmd:${window.cmd}'
}
// will select the current window so with tmux a we can go there .
// to login into a session do `tmux a -s mysessionname`
fn (mut w Window) activate() ! {
cmd2 := 'tmux select-window -t %${w.id}'
osal.execute_silent(cmd2) or {
return error("Couldn't select window ${w.name} \n${cmd2}\n${err}")
}
}
// show the environment
pub fn (mut w Window) environment_print() ! {
res := osal.execute_silent('tmux show-environment -t %${w.paneid}') or {
return error('Couldnt show enviroment cmd: ${w.cmd} \n${err}')
}
os.log(res)
}
// capture the output
pub fn (mut w Window) output_print() ! {
o := w.output()!
console.print_debug(o)
}
// capture the output
pub fn (mut w Window) output() !string {
//-S is start, minus means go in history, otherwise its only the active output
// tmux capture-pane -t your-session-name:your-window-number -S -1000
cmd := 'tmux capture-pane -t ${w.session.name}:@${w.id} -S -1000 && tmux show-buffer'
res := osal.execute_silent(cmd) or {
return error('Couldnt show enviroment cmd: ${w.cmd} \n${err}')
}
return texttools.remove_empty_lines(res)
}
pub fn (mut w Window) output_wait(c_ string, timeoutsec int) ! {
mut t := ourtime.now()
start := t.unix()
c := c_.replace('\n', '')
for i in 0 .. 2000 {
o := w.output()!
// console.print_debug(o)
$if debug {
console.print_debug(" - tmux ${w.name}: wait for: '${c}'")
}
// need to replace \n because can be wrapped because of size of pane
if o.replace('\n', '').contains(c) {
return
}
mut t2 := ourtime.now()
if t2.unix() > start + timeoutsec {
return error('timeout on output wait for tmux.\n${w} .\nwaiting for:\n${c}')
}
time.sleep(100 * time.millisecond)
}
}