From 4373ec21aa47c876141d8456f4d6392a219a358a Mon Sep 17 00:00:00 2001 From: despiegk Date: Wed, 25 Dec 2024 22:30:33 +0100 Subject: [PATCH] fixes installers --- lib/clients/rclone/.heroscript | 7 + lib/{osal => clients}/rclone/config.v | 0 lib/clients/rclone/rclone.v | 83 ++++++ lib/clients/rclone/rclone_factory_.v | 104 +++++++ lib/clients/rclone/rclone_model.v | 62 ++++ lib/clients/rclone/rclone_test.v | 39 +++ lib/{osal => clients}/rclone/readme.md | 0 lib/clients/rclone/readme.v.md | 68 +++++ lib/installers/sysadmintools/b2/b2_install.v | 1 - .../sysadmintools/fungistor/fungistor.v | 1 - .../sysadmintools/grafana/grafana.v | 1 - .../prometheus/prometheus_factory_.v | 4 +- .../prometheus/prometheus_installer_factory.v | 48 ++-- .../sysadmintools/restic/restic_install.v | 1 - lib/installers/web/lighttpd/installer.v | 1 - lib/osal/hostsfile/README.md | 95 +++++++ lib/osal/hostsfile/hostsfile.v | 269 +++++++++++------- lib/osal/hostsfile/hostsfile_test.v | 110 +++++++ lib/osal/screen/readme.md | 56 +++- lib/osal/screen/screen_test.v | 141 ++++++++- lib/osal/startupmanager/readme.md | 15 - lib/osal/startupmanager/startupmanager_test.v | 115 +++++++- 22 files changed, 1061 insertions(+), 160 deletions(-) create mode 100644 lib/clients/rclone/.heroscript rename lib/{osal => clients}/rclone/config.v (100%) create mode 100644 lib/clients/rclone/rclone.v create mode 100644 lib/clients/rclone/rclone_factory_.v create mode 100644 lib/clients/rclone/rclone_model.v create mode 100644 lib/clients/rclone/rclone_test.v rename lib/{osal => clients}/rclone/readme.md (100%) create mode 100644 lib/clients/rclone/readme.v.md create mode 100644 lib/osal/hostsfile/README.md create mode 100644 lib/osal/hostsfile/hostsfile_test.v diff --git a/lib/clients/rclone/.heroscript b/lib/clients/rclone/.heroscript new file mode 100644 index 00000000..c24d5dbf --- /dev/null +++ b/lib/clients/rclone/.heroscript @@ -0,0 +1,7 @@ +!!hero_code.generate_client + name: "rclone" + classname: "RCloneClient" + hasconfig: true + singleton: true + default: true + title: "" diff --git a/lib/osal/rclone/config.v b/lib/clients/rclone/config.v similarity index 100% rename from lib/osal/rclone/config.v rename to lib/clients/rclone/config.v diff --git a/lib/clients/rclone/rclone.v b/lib/clients/rclone/rclone.v new file mode 100644 index 00000000..99caf1b7 --- /dev/null +++ b/lib/clients/rclone/rclone.v @@ -0,0 +1,83 @@ +module rclone + +import os +import freeflowuniverse.herolib.core.texttools + +// // RCloneClient represents a configured rclone instance +// pub struct RCloneClient { +// pub mut: +// name string // name of the remote +// } + +// new creates a new RCloneClient instance +pub fn new(name string) !RCloneClient { + return RCloneClient{ + name: name + } +} + +// mount mounts a remote at the specified path +pub fn (mut r RCloneClient) mount(remote_path string, local_path string) ! { + if !os.exists(local_path) { + os.mkdir_all(local_path) or { return error('Failed to create mount directory: ${err}') } + } + + cmd := 'rclone mount ${r.name}:${remote_path} ${local_path} --daemon' + res := os.execute(cmd) + if res.exit_code != 0 { + return error('Failed to mount remote: ${res.output}') + } +} + +// unmount unmounts a mounted remote +pub fn (mut r RCloneClient) unmount(local_path string) ! { + if os.user_os() == 'macos' { + os.execute_opt('umount ${local_path}') or { return error('Failed to unmount: ${err}') } + } else { + os.execute_opt('fusermount -u ${local_path}') or { + return error('Failed to unmount: ${err}') + } + } +} + +// upload uploads a file or directory to the remote +pub fn (mut r RCloneClient) upload(local_path string, remote_path string) ! { + if !os.exists(local_path) { + return error('Local path does not exist: ${local_path}') + } + + cmd := 'rclone copy ${local_path} ${r.name}:${remote_path}' + res := os.execute(cmd) + if res.exit_code != 0 { + return error('Failed to upload: ${res.output}') + } +} + +// download downloads a file or directory from the remote +pub fn (mut r RCloneClient) download(remote_path string, local_path string) ! { + if !os.exists(local_path) { + os.mkdir_all(local_path) or { return error('Failed to create local directory: ${err}') } + } + + cmd := 'rclone copy ${r.name}:${remote_path} ${local_path}' + res := os.execute(cmd) + if res.exit_code != 0 { + return error('Failed to download: ${res.output}') + } +} + +// list lists contents of a remote path +pub fn (mut r RCloneClient) list(remote_path string) !string { + cmd := 'rclone ls ${r.name}:${remote_path}' + res := os.execute(cmd) + if res.exit_code != 0 { + return error('Failed to list remote contents: ${res.output}') + } + return res.output +} + +// check_installed checks if rclone is installed +pub fn check_installed() bool { + res := os.execute('which rclone') + return res.exit_code == 0 +} diff --git a/lib/clients/rclone/rclone_factory_.v b/lib/clients/rclone/rclone_factory_.v new file mode 100644 index 00000000..7d18df2f --- /dev/null +++ b/lib/clients/rclone/rclone_factory_.v @@ -0,0 +1,104 @@ +module rclone + +import freeflowuniverse.herolib.core.base +import freeflowuniverse.herolib.core.playbook +import freeflowuniverse.herolib.ui.console + +__global ( + rclone_global map[string]&RCloneClient + rclone_default string +) + +/////////FACTORY + +@[params] +pub struct ArgsGet { +pub mut: + name string +} + +fn args_get(args_ ArgsGet) ArgsGet { + mut model := args_ + if model.name == '' { + model.name = rclone_default + } + if model.name == '' { + model.name = 'default' + } + return model +} + +pub fn get(args_ ArgsGet) !&RCloneClient { + mut model := args_get(args_) + if model.name !in rclone_global { + if model.name == 'default' { + if !config_exists(model) { + if default { + config_save(model)! + } + } + config_load(model)! + } + } + return rclone_global[model.name] or { + println(rclone_global) + panic('could not get config for rclone with name:${model.name}') + } +} + +fn config_exists(args_ ArgsGet) bool { + mut model := args_get(args_) + mut context := base.context() or { panic('bug') } + return context.hero_config_exists('rclone', model.name) +} + +fn config_load(args_ ArgsGet) ! { + mut model := args_get(args_) + mut context := base.context()! + mut heroscript := context.hero_config_get('rclone', model.name)! + play(heroscript: heroscript)! +} + +fn config_save(args_ ArgsGet) ! { + mut model := args_get(args_) + mut context := base.context()! + context.hero_config_set('rclone', model.name, heroscript_default()!)! +} + +fn set(o RCloneClient) ! { + mut o2 := obj_init(o)! + rclone_global[o.name] = &o2 + rclone_default = o.name +} + +@[params] +pub struct PlayArgs { +pub mut: + heroscript string // if filled in then plbook will be made out of it + plbook ?playbook.PlayBook + reset bool +} + +pub fn play(args_ PlayArgs) ! { + mut model := args_ + + if model.heroscript == '' { + model.heroscript = heroscript_default()! + } + mut plbook := model.plbook or { playbook.new(text: model.heroscript)! } + + mut install_actions := plbook.find(filter: 'rclone.configure')! + if install_actions.len > 0 { + for install_action in install_actions { + mut p := install_action.params + mycfg := cfg_play(p)! + console.print_debug('install action rclone.configure\n${mycfg}') + set(mycfg)! + } + } +} + +// switch instance to be used for rclone +pub fn switch(name string) { + rclone_default = name +} diff --git a/lib/clients/rclone/rclone_model.v b/lib/clients/rclone/rclone_model.v new file mode 100644 index 00000000..a43ad437 --- /dev/null +++ b/lib/clients/rclone/rclone_model.v @@ -0,0 +1,62 @@ +module rclone + +import freeflowuniverse.herolib.data.paramsparser +import os + +pub const version = '0.0.0' +const singleton = true +const default = true + +pub fn heroscript_default() !string { + name := os.getenv_opt('RCLONE_NAME') or { 'default' } + remote_type := os.getenv_opt('RCLONE_TYPE') or { 's3' } + provider := os.getenv_opt('RCLONE_PROVIDER') or { 'aws' } + access_key := os.getenv_opt('RCLONE_ACCESS_KEY') or { '' } + secret_key := os.getenv_opt('RCLONE_SECRET_KEY') or { '' } + region := os.getenv_opt('RCLONE_REGION') or { 'us-east-1' } + endpoint := os.getenv_opt('RCLONE_ENDPOINT') or { '' } + + heroscript := " + !!rclone.configure + name: '${name}' + type: '${remote_type}' + provider: '${provider}' + access_key: '${access_key}' + secret_key: '${secret_key}' + region: '${region}' + endpoint: '${endpoint}' + " + + return heroscript +} + +@[heap] +pub struct RCloneClient { +pub mut: + name string = 'default' + type_ string = 's3' // remote type (s3, sftp, etc) + provider string = 'aws' // provider for s3 (aws, minio, etc) + access_key string // access key for authentication + secret_key string // secret key for authentication + region string = 'us-east-1' // region for s3 + endpoint string // custom endpoint URL if needed +} + +fn cfg_play(p paramsparser.Params) ! { + mut mycfg := RCloneClient{ + name: p.get_default('name', 'default')! + type_: p.get_default('type', 's3')! + provider: p.get_default('provider', 'aws')! + access_key: p.get('access_key')! + secret_key: p.get('secret_key')! + region: p.get_default('region', 'us-east-1')! + endpoint: p.get_default('endpoint', '')! + } + set(mycfg)! +} + +fn obj_init(obj_ RCloneClient) !RCloneClient { + // never call get here, only thing we can do here is work on object itself + mut obj := obj_ + return obj +} diff --git a/lib/clients/rclone/rclone_test.v b/lib/clients/rclone/rclone_test.v new file mode 100644 index 00000000..41d45f95 --- /dev/null +++ b/lib/clients/rclone/rclone_test.v @@ -0,0 +1,39 @@ +module rclone + +fn test_rclone_new() { + rclone := new('test_remote') or { panic(err) } + assert rclone.name == 'test_remote' +} + +fn test_check_installed() { + installed := check_installed() + // This test will pass or fail depending on whether rclone is installed + // on the system. It's mainly for documentation purposes. + println('RCloneClient installed: ${installed}') +} + +// Note: The following tests are commented out as they require an actual rclone +// configuration and remote to work with. They serve as examples of how to use +// the RCloneClient module. + +/* +fn test_rclone_operations() ! { + mut rclone := new('my_remote')! + + // Test upload + rclone.upload('./testdata', 'backup/testdata')! + + // Test download + rclone.download('backup/testdata', './testdata_download')! + + // Test mount + rclone.mount('backup', './mounted_backup')! + + // Test list + content := rclone.list('backup')! + println(content) + + // Test unmount + rclone.unmount('./mounted_backup')! +} +*/ diff --git a/lib/osal/rclone/readme.md b/lib/clients/rclone/readme.md similarity index 100% rename from lib/osal/rclone/readme.md rename to lib/clients/rclone/readme.md diff --git a/lib/clients/rclone/readme.v.md b/lib/clients/rclone/readme.v.md new file mode 100644 index 00000000..b31afd64 --- /dev/null +++ b/lib/clients/rclone/readme.v.md @@ -0,0 +1,68 @@ +# RCloneClient Module + +This module provides a V language interface to RCloneClient, a command line program to manage files on cloud storage. + +## Features + +- Mount/unmount remote storage +- Upload files and directories +- Download files and directories +- List remote contents +- Configuration management through heroscript format + +## Prerequisites + +RCloneClient must be installed on your system. Visit https://rclone.org/install/ for installation instructions. + +## Usage + +```v +import freeflowuniverse.herolib.osal.rclone + +fn main() { + // Create a new RCloneClient instance + mut rc := rclone.new('my_remote') or { panic(err) } + + // Upload a directory + rc.upload('./local_dir', 'backup/remote_dir') or { panic(err) } + + // Download a directory + rc.download('backup/remote_dir', './downloaded_dir') or { panic(err) } + + // Mount a remote + rc.mount('backup', './mounted_backup') or { panic(err) } + + // List contents + content := rc.list('backup') or { panic(err) } + println(content) + + // Unmount when done + rc.unmount('./mounted_backup') or { panic(err) } +} +``` + +## Configuration + +Configuration is managed through heroscript format in `~/hero/config`. Example configuration: + +```heroscript +!!config.s3server_define + name:'my_remote' + description:'My Remote Storage' + keyid:'your_key_id' + keyname:'your_key_name' + appkey:'your_app_key' + url:'your_url' +``` + +The configuration will be automatically loaded and applied when creating a new RCloneClient instance. + +## Testing + +To run the tests: + +```bash +vtest ~/code/github/freeflowuniverse/herolib/lib/osal/rclone/rclone_test.v +``` + +Note: Some tests are commented out as they require an actual rclone configuration and remote to work with. They serve as examples of how to use the RCloneClient module. diff --git a/lib/installers/sysadmintools/b2/b2_install.v b/lib/installers/sysadmintools/b2/b2_install.v index fb4e7c6e..ec48910c 100644 --- a/lib/installers/sysadmintools/b2/b2_install.v +++ b/lib/installers/sysadmintools/b2/b2_install.v @@ -5,7 +5,6 @@ import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.lang.python // import os - pub fn installll(args_ InstallArgs) ! { mut args := args_ diff --git a/lib/installers/sysadmintools/fungistor/fungistor.v b/lib/installers/sysadmintools/fungistor/fungistor.v index 6e9521ce..207ab71f 100644 --- a/lib/installers/sysadmintools/fungistor/fungistor.v +++ b/lib/installers/sysadmintools/fungistor/fungistor.v @@ -5,7 +5,6 @@ import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.core.texttools import os - pub fn installlll(args_ InstallArgs) ! { mut args := args_ version := '2.0.6' diff --git a/lib/installers/sysadmintools/grafana/grafana.v b/lib/installers/sysadmintools/grafana/grafana.v index ab681808..0381b9b1 100644 --- a/lib/installers/sysadmintools/grafana/grafana.v +++ b/lib/installers/sysadmintools/grafana/grafana.v @@ -9,7 +9,6 @@ import freeflowuniverse.herolib.sysadmin.startupmanager import os import time - pub fn installll(args_ InstallArgs) ! { mut args := args_ diff --git a/lib/installers/sysadmintools/prometheus/prometheus_factory_.v b/lib/installers/sysadmintools/prometheus/prometheus_factory_.v index 2482b948..3b3adf55 100644 --- a/lib/installers/sysadmintools/prometheus/prometheus_factory_.v +++ b/lib/installers/sysadmintools/prometheus/prometheus_factory_.v @@ -124,12 +124,12 @@ pub fn (mut self Prometheus) running() !bool { } @[params] -pub struct InstallArgss { +pub struct InstallArgs { pub mut: reset bool } -pub fn (mut self Prometheus) install(model InstallArgss) ! { +pub fn (mut self Prometheus) install(model InstallArgs) ! { switch(self.name) if model.reset || (!installed()!) { install()! diff --git a/lib/installers/sysadmintools/prometheus/prometheus_installer_factory.v b/lib/installers/sysadmintools/prometheus/prometheus_installer_factory.v index 43e60c5f..c2645201 100644 --- a/lib/installers/sysadmintools/prometheus/prometheus_installer_factory.v +++ b/lib/installers/sysadmintools/prometheus/prometheus_installer_factory.v @@ -9,28 +9,28 @@ import freeflowuniverse.herolib.sysadmin.startupmanager import os import time -@[params] -pub struct InstallArgs { -pub mut: - // homedir string - // configpath string - // username string = "admin" - // password string @[secret] - // secret string @[secret] - // title string = 'My Hero DAG' - reset bool - start bool = true - stop bool - restart bool - uninstall bool - // host string = 'localhost' // server host (default is localhost) - // port int = 8888 -} +// @[params] +// pub struct InstallArgs { +// pub mut: +// // homedir string +// // configpath string +// // username string = "admin" +// // password string @[secret] +// // secret string @[secret] +// // title string = 'My Hero DAG' +// reset bool +// start bool = true +// stop bool +// restart bool +// uninstall bool +// // host string = 'localhost' // server host (default is localhost) +// // port int = 8888 +// } -pub fn install(args_ InstallArgs) ! { - install_prometheus(args_)! - install_alertmanager(args_)! - install_node_exporter(args_)! - install_blackbox_exporter(args_)! - install_prom2json(args_)! -} +// pub fn install(args_ InstallArgs) ! { +// install_prometheus(args_)! +// install_alertmanager(args_)! +// install_node_exporter(args_)! +// install_blackbox_exporter(args_)! +// install_prom2json(args_)! +// } diff --git a/lib/installers/sysadmintools/restic/restic_install.v b/lib/installers/sysadmintools/restic/restic_install.v index e8a72f1f..26cb69d0 100644 --- a/lib/installers/sysadmintools/restic/restic_install.v +++ b/lib/installers/sysadmintools/restic/restic_install.v @@ -5,7 +5,6 @@ import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.core.texttools import os - pub fn installll(args_ InstallArgs) ! { mut args := args_ version := '0.16.2' diff --git a/lib/installers/web/lighttpd/installer.v b/lib/installers/web/lighttpd/installer.v index 28652a3f..33b632a8 100644 --- a/lib/installers/web/lighttpd/installer.v +++ b/lib/installers/web/lighttpd/installer.v @@ -10,7 +10,6 @@ import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.screen import os - // install lighttpd will return true if it was already installed pub fn installll(args InstallArgs) ! { // make sure we install base on the node diff --git a/lib/osal/hostsfile/README.md b/lib/osal/hostsfile/README.md new file mode 100644 index 00000000..f42ba55e --- /dev/null +++ b/lib/osal/hostsfile/README.md @@ -0,0 +1,95 @@ +# Hosts File Manager + +This module provides functionality to manage the system's hosts file (`/etc/hosts`) in a safe and structured way. It supports both Linux and macOS systems, automatically handling sudo permissions when required. + +## Features + +- Read and parse the system's hosts file +- Add new host entries with IP and domain +- Remove host entries by domain +- Manage sections with comments +- Remove or clear entire sections +- Check for existing domains +- Automatic sudo handling for macOS and Linux when needed + +## Usage + +Create a file `example.vsh`: + +```v +#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.osal.hostsfile +import os + +// Create a new instance by reading the hosts file +mut hosts := hostsfile.new() or { + eprintln('Failed to read hosts file: ${err}') + exit(1) +} + +// Add a new host entry to a section +hosts.add_host('127.0.0.1', 'mysite.local', 'Development') or { + eprintln('Failed to add host: ${err}') + exit(1) +} + +// Remove a host entry +hosts.remove_host('mysite.local') or { + eprintln('Failed to remove host: ${err}') + exit(1) +} + +// Check if a domain exists +if hosts.exists('example.com') { + println('Domain exists') +} + +// Clear all entries in a section +hosts.clear_section('Development') or { + eprintln('Failed to clear section: ${err}') + exit(1) +} + +// Remove an entire section +hosts.remove_section('Development') or { + eprintln('Failed to remove section: ${err}') + exit(1) +} + +// Save changes back to the hosts file +// This will automatically use sudo when needed +hosts.save() or { + eprintln('Failed to save hosts file: ${err}') + exit(1) +} +``` + +## File Structure + +The hosts file is organized into sections marked by comments. For example: + +``` +# Development +127.0.0.1 localhost +127.0.0.1 mysite.local + +# Production +192.168.1.100 prod.example.com +``` + +## Error Handling + +All functions that can fail return a Result type and should be handled appropriately: + +```v +hosts.add_host('127.0.0.1', 'mysite.local', 'Development') or { + eprintln('Failed to add host: ${err}') + exit(1) +} +``` + +## Platform Support + +- Linux: Direct write with fallback to sudo if needed +- macOS: Always uses sudo due to system restrictions diff --git a/lib/osal/hostsfile/hostsfile.v b/lib/osal/hostsfile/hostsfile.v index 5963ea22..9ecb6cf6 100644 --- a/lib/osal/hostsfile/hostsfile.v +++ b/lib/osal/hostsfile/hostsfile.v @@ -3,7 +3,7 @@ module hostsfile import os import freeflowuniverse.herolib.osal -// TODO: will be broken now +const hosts_file_path = '/etc/hosts' @[heap] pub struct HostsFile { @@ -23,119 +23,182 @@ pub mut: domain string } -// pub fn new() HostsFile { -// mut obj := HostsFile{} +// new creates a new HostsFile instance by reading the system's hosts file +pub fn new() !HostsFile { + mut obj := HostsFile{} + mut content := os.read_file(hosts_file_path) or { + return error('Failed to read hosts file: ${err}') + } + mut current_section := Section{ + name: '' + } -// mut content := os.read_file('/etc/hosts') or { panic(err) } -// mut section := '' + for line in content.split_into_lines() { + trimmed := line.trim_space() + if trimmed == '' { + continue + } -// for mut line in content.split('\n') { -// line = line.trim_space() -// if line.starts_with('#') { -// section = line.trim('#').trim_space() -// continue -// } + if trimmed.starts_with('#') { + // If we have hosts in the current section, add it to sections + if current_section.hosts.len > 0 { + obj.sections << current_section + } + // Start a new section + current_section = Section{ + name: trimmed[1..].trim_space() + } + continue + } -// mut splitted := line.fields() -// if splitted.len > 1 { -// if section !in obj.hosts { -// obj.hosts[section] = []map[string]string{} -// } -// obj.hosts[section] << { -// splitted[0]: splitted[1] -// } -// } -// } -// return obj -// } + // Parse host entries + parts := trimmed.fields() + if parts.len >= 2 { + current_section.hosts << Host{ + ip: parts[0] + domain: parts[1] + } + } + } -// pub fn (mut hostsfile HostsFile) save(sudo bool) &HostsFile { -// mut str := '' -// for section, items in hostsfile.hosts { -// if section != '' { -// str = str + '# ${section}\n\n' -// } + // Add the last section if it has hosts + if current_section.hosts.len > 0 { + obj.sections << current_section + } -// for item in items { -// for ip, domain in item { -// str = str + '${ip}\t${domain}\n' -// } -// } -// str = str + '\n\n' -// } -// if sudo { -// osal.execute_interactive('sudo -- sh -c -e "echo \'${str}\' > /etc/hosts"') or { -// panic(err) -// } -// } else { -// os.write_file('/etc/hosts', str) or { panic(err) } -// } -// return hostsfile -// } + return obj +} -// pub fn (mut hostsfile HostsFile) reset(sections []string) &HostsFile { -// for section in sections { -// if section in hostsfile.hosts { -// hostsfile.hosts[section] = []map[string]string{} -// } -// } -// return hostsfile -// } +// add_host adds a new host entry to the specified section +pub fn (mut h HostsFile) add_host(ip string, domain string, section string) ! { + // Validate inputs + if ip == '' { + return error('IP address cannot be empty') + } + if domain == '' { + return error('Domain cannot be empty') + } -// pub struct HostItemArg{ -// pub mut: -// ip string -// domain string -// section string = "main" -// } + // Check if domain already exists + if h.exists(domain) { + return error('Domain ${domain} already exists in hosts file') + } -// pub fn (mut hostsfile HostsFile) add(args HostItemArg) &HostsFile { -// if args.section !in hostsfile.hosts { -// hostsfile.hosts[args.section] = []map[string]string{} -// } -// hostsfile.hosts[args.section] << { -// ip: domain -// } -// return hostsfile -// } + // Find or create section + mut found_section := false + for mut s in h.sections { + if s.name == section { + s.hosts << Host{ + ip: ip + domain: domain + } + found_section = true + break + } + } -// pub fn (mut hostsfile HostsFile) delete(domain string) &HostsFile { -// mut indexes := map[string][]int{} + if !found_section { + h.sections << Section{ + name: section + hosts: [Host{ + ip: ip + domain: domain + }] + } + } +} -// for section, items in hostsfile.hosts { -// indexes[section] = []int{} -// for i, item in items { -// for _, dom in item { -// if dom == domain { -// indexes[section] << i -// } -// } -// } -// } +// remove_host removes all entries for the specified domain +pub fn (mut h HostsFile) remove_host(domain string) ! { + mut found := false + for mut section in h.sections { + // Filter out hosts with matching domain + old_len := section.hosts.len + section.hosts = section.hosts.filter(it.domain != domain) + if section.hosts.len < old_len { + found = true + } + } -// for section, items in indexes { -// for i in items { -// hostsfile.hosts[section].delete(i) -// } -// } + if !found { + return error('Domain ${domain} not found in hosts file') + } +} -// return hostsfile -// } +// exists checks if a domain exists in any section +pub fn (h &HostsFile) exists(domain string) bool { + for section in h.sections { + for host in section.hosts { + if host.domain == domain { + return true + } + } + } + return false +} -// pub fn (mut hostsfile HostsFile) delete_section(section string) &HostsFile { -// hostsfile.hosts.delete(section) -// return hostsfile -// } +// save writes the hosts file back to disk +pub fn (h &HostsFile) save() ! { + mut content := '' -// pub fn (mut hostsfile HostsFile) exists(domain string) bool { -// for _, items in hostsfile.hosts { -// for item in items { -// for _, dom in item { -// if dom == domain { -// return true -// } -// } -// } -// } -// return false -// } + for section in h.sections { + if section.name != '' { + content += '# ${section.name}\n' + } + + for host in section.hosts { + content += '${host.ip}\t${host.domain}\n' + } + content += '\n' + } + + // Check if we're on macOS + is_macos := os.user_os() == 'macos' + + if is_macos { + // On macOS, we need to use sudo + osal.execute_interactive('sudo -- sh -c -e "echo \'${content}\' > ${hosts_file_path}"') or { + return error('Failed to write hosts file with sudo: ${err}') + } + } else { + // On Linux, try direct write first, fallback to sudo if needed + os.write_file(hosts_file_path, content) or { + // If direct write fails, try with sudo + osal.execute_interactive('sudo -- sh -c -e "echo \'${content}\' > ${hosts_file_path}"') or { + return error('Failed to write hosts file: ${err}') + } + } + } +} + +// remove_section removes an entire section and its hosts +pub fn (mut h HostsFile) remove_section(section_name string) ! { + mut found := false + for i, section in h.sections { + if section.name == section_name { + h.sections.delete(i) + found = true + break + } + } + + if !found { + return error('Section ${section_name} not found') + } +} + +// clear_section removes all hosts from a section but keeps the section +pub fn (mut h HostsFile) clear_section(section_name string) ! { + mut found := false + for mut section in h.sections { + if section.name == section_name { + section.hosts.clear() + found = true + break + } + } + + if !found { + return error('Section ${section_name} not found') + } +} diff --git a/lib/osal/hostsfile/hostsfile_test.v b/lib/osal/hostsfile/hostsfile_test.v new file mode 100644 index 00000000..b8ad72c2 --- /dev/null +++ b/lib/osal/hostsfile/hostsfile_test.v @@ -0,0 +1,110 @@ +module hostsfile + +fn test_hostsfile_basic() { + // Create new hosts file instance + mut hosts := new() or { + assert false, 'Failed to create hosts file: ${err}' + return + } + + // Test adding a host + hosts.add_host('127.0.0.1', 'test.local', 'Test') or { + assert false, 'Failed to add host: ${err}' + return + } + + // Verify host exists + assert hosts.exists('test.local'), 'Added host test.local not found' + + // Test adding duplicate host (should fail) + hosts.add_host('127.0.0.1', 'test.local', 'Test') or { + assert err.str() == 'Domain test.local already exists in hosts file' + goto next_test + } + assert false, 'Adding duplicate host should fail' + next_test: + // Test removing host + hosts.remove_host('test.local') or { + assert false, 'Failed to remove host: ${err}' + return + } + + // Verify host was removed + assert !hosts.exists('test.local'), 'Host test.local still exists after removal' + + // Test removing non-existent host (should fail) + hosts.remove_host('nonexistent.local') or { + assert err.str() == 'Domain nonexistent.local not found in hosts file' + return + } + assert false, 'Removing non-existent host should fail' +} + +fn test_hostsfile_sections() { + mut hosts := new() or { + assert false, 'Failed to create hosts file: ${err}' + return + } + + // Add hosts to different sections + hosts.add_host('127.0.0.1', 'dev.local', 'Development') or { + assert false, 'Failed to add dev host: ${err}' + return + } + hosts.add_host('127.0.0.1', 'prod.local', 'Production') or { + assert false, 'Failed to add prod host: ${err}' + return + } + + // Verify both hosts exist + assert hosts.exists('dev.local'), 'dev.local not found' + assert hosts.exists('prod.local'), 'prod.local not found' + + // Test clearing a section + hosts.clear_section('Development') or { + assert false, 'Failed to clear Development section: ${err}' + return + } + + // Verify Development host removed but Production remains + assert !hosts.exists('dev.local'), 'dev.local still exists after clearing section' + assert hosts.exists('prod.local'), 'prod.local was incorrectly removed' + + // Test removing a section + hosts.remove_section('Production') or { + assert false, 'Failed to remove Production section: ${err}' + return + } + + // Verify all hosts removed + assert !hosts.exists('dev.local'), 'dev.local still exists' + assert !hosts.exists('prod.local'), 'prod.local still exists' + + // Test removing non-existent section (should fail) + hosts.remove_section('NonExistent') or { + assert err.str() == 'Section NonExistent not found' + return + } + assert false, 'Removing non-existent section should fail' +} + +fn test_hostsfile_validation() { + mut hosts := new() or { + assert false, 'Failed to create hosts file: ${err}' + return + } + + // Test empty IP + hosts.add_host('', 'test.local', 'Test') or { + assert err.str() == 'IP address cannot be empty' + goto next_test1 + } + assert false, 'Empty IP should fail' + next_test1: + // Test empty domain + hosts.add_host('127.0.0.1', '', 'Test') or { + assert err.str() == 'Domain cannot be empty' + return + } + assert false, 'Empty domain should fail' +} diff --git a/lib/osal/screen/readme.md b/lib/osal/screen/readme.md index f7ef222b..28cd2048 100644 --- a/lib/osal/screen/readme.md +++ b/lib/osal/screen/readme.md @@ -1,13 +1,61 @@ -# screen +# Screen + +The Screen module provides a V interface to manage GNU Screen sessions. + +## Example Script + +Create a file `screen_example.vsh`: + +```v +#!/usr/bin/env -S v run + +import freeflowuniverse.herolib.osal.screen + +// Create a new screen session with hardcoded parameters +mut s := screen.Screen{ + name: 'test_session' + cmd: '/bin/bash' // Default shell +} + +// Check if screen is running +is_running := s.is_running() or { + println('Error checking screen status: ${err}') + return +} + +// Get session status +status := s.status() or { + println('Error getting status: ${err}') + return +} + +// Send a command to the screen session +s.cmd_send('ls -la') or { + println('Error sending command: ${err}') + return +} + +// Attach to the session +s.attach() or { + println('Error attaching: ${err}') + return +} +``` + +## Basic Screen Commands ```bash #to see sessions which have been created - screen -ls There is a screen on: - 3230.test (Detached) + 3230.test (Detached) #now to attach to this screen screen -r test -``` \ No newline at end of file +``` + +## Testing + +```bash +vtest ~/code/github/freeflowuniverse/herolib/lib/osal/screen/screen_test.v diff --git a/lib/osal/screen/screen_test.v b/lib/osal/screen/screen_test.v index ab92903b..f56cd77a 100644 --- a/lib/osal/screen/screen_test.v +++ b/lib/osal/screen/screen_test.v @@ -1,17 +1,148 @@ module screen -import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.osal import os import time +const test_screen_name = 'test_screen_session' +const test_cmd = 'echo "test command"' + +// Initialize test environment pub fn testsuite_begin() ! { + // Check if screen is installed + res := os.execute('which screen') + if res.exit_code != 0 { + return error('screen is not installed. Please install screen first.') + } + + // Ensure screen directory exists with proper permissions + home := os.home_dir() + screen_dir := '${home}/.screen' + if !os.exists(screen_dir) { + // Create directory with proper permissions using mkdir -m + res2 := os.execute('mkdir -m 700 ${screen_dir}') + if res2.exit_code != 0 { + return error('Failed to create screen directory: ${res2.output}') + } + } + mut screen_factory := new(reset: true)! + cleanup_test_screens()! } -pub fn test_screen_status() ! { - mut screen_factory := new()! - mut screen := screen_factory.add(name: 'testservice', cmd: 'redis-server --port 1234')! +// Cleanup after all tests +pub fn testsuite_end() ! { + cleanup_test_screens()! +} + +fn cleanup_test_screens() ! { + mut screen_factory := new(reset: false)! + screen_factory.scan()! + + // Clean up main test screen + if screen_factory.exists(test_screen_name) { + screen_factory.kill(test_screen_name)! + time.sleep(200 * time.millisecond) // Give time for cleanup + } + + // Clean up multiple test screens + if screen_factory.exists('${test_screen_name}_1') { + screen_factory.kill('${test_screen_name}_1')! + time.sleep(200 * time.millisecond) + } + if screen_factory.exists('${test_screen_name}_2') { + screen_factory.kill('${test_screen_name}_2')! + time.sleep(200 * time.millisecond) + } + + // Final scan to ensure cleanup + screen_factory.scan()! +} + +// Helper function to create and verify screen +fn create_and_verify_screen(mut screen_factory ScreensFactory, name string, cmd string) !&Screen { + mut screen := screen_factory.add( + name: name + cmd: cmd + )! + + // Give screen time to initialize + time.sleep(500 * time.millisecond) + + // Verify screen exists and is running + screen_factory.scan()! + if !screen_factory.exists(name) { + return error('Screen ${name} was not found after creation') + } + + mut result := screen_factory.get(name)! + return &result +} + +// Test screen creation and basic status +pub fn test_screen_creation() ! { + mut screen_factory := new(reset: false)! + mut screen := create_and_verify_screen(mut &screen_factory, test_screen_name, '/bin/bash')! + + assert screen.name == test_screen_name status := screen.status()! assert status == .active } + +// Test command sending functionality +pub fn test_screen_cmd_send() ! { + mut screen_factory := new(reset: false)! + mut screen := create_and_verify_screen(mut &screen_factory, test_screen_name, '/bin/bash')! + + // Send a test command + screen.cmd_send(test_cmd)! + + // Give some time for command execution + time.sleep(200 * time.millisecond) + + // Verify screen status after command + status := screen.status()! + assert status == .active +} + +// Test error cases +pub fn test_screen_errors() ! { + mut screen_factory := new(reset: false)! + + // Test invalid screen name + if _ := screen_factory.get('nonexistent_screen') { + assert false, 'Should not find nonexistent screen' + } else { + assert true + } + + // Test screen status after creation but before start + mut screen := screen_factory.add( + name: test_screen_name + cmd: '/bin/bash' + start: false + )! + status := screen.status()! + assert status == .inactive, 'Screen should be inactive before start' +} + +// Test multiple screens +pub fn test_multiple_screens() ! { + mut screen_factory := new(reset: false)! + + screen1_name := '${test_screen_name}_1' + screen2_name := '${test_screen_name}_2' + + mut screen1 := create_and_verify_screen(mut &screen_factory, screen1_name, '/bin/bash')! + mut screen2 := create_and_verify_screen(mut &screen_factory, screen2_name, '/bin/bash')! + + assert screen1.status()! == .active + assert screen2.status()! == .active + + screen_factory.kill(screen1_name)! + time.sleep(200 * time.millisecond) + assert screen1.status()! == .inactive + assert screen2.status()! == .active + + screen_factory.kill(screen2_name)! + time.sleep(200 * time.millisecond) +} diff --git a/lib/osal/startupmanager/readme.md b/lib/osal/startupmanager/readme.md index 6baae9d3..d06199da 100644 --- a/lib/osal/startupmanager/readme.md +++ b/lib/osal/startupmanager/readme.md @@ -14,18 +14,3 @@ sm.start( ``` -## some basic commands for screen - -```bash -#list the screens -screen -ls -#attach to the screens -screen -r myscreen -``` - -to exit a screen to - -``` -ctrl a d -``` - diff --git a/lib/osal/startupmanager/startupmanager_test.v b/lib/osal/startupmanager/startupmanager_test.v index 9168c52a..fe136fb7 100644 --- a/lib/osal/startupmanager/startupmanager_test.v +++ b/lib/osal/startupmanager/startupmanager_test.v @@ -3,13 +3,31 @@ module startupmanager import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.screen import freeflowuniverse.herolib.osal.systemd +import os +import time const process_name = 'testprocess' +// Initialize test environment pub fn testsuite_begin() ! { + // Initialize screen factory + mut screen_factory := screen.new(reset: true)! + + // Ensure screen directory exists with proper permissions + home := os.home_dir() + screen_dir := '${home}/.screen' + if !os.exists(screen_dir) { + res := os.execute('mkdir -m 700 ${screen_dir}') + if res.exit_code != 0 { + return error('Failed to create screen directory: ${res.output}') + } + } + + // Clean up any existing process mut sm := get()! if sm.exists(process_name)! { sm.stop(process_name)! + time.sleep(200 * time.millisecond) // Give time for cleanup } } @@ -17,22 +35,115 @@ pub fn testsuite_end() ! { mut sm := get()! if sm.exists(process_name)! { sm.stop(process_name)! + time.sleep(200 * time.millisecond) // Give time for cleanup + } + + // Clean up screen sessions + mut screen_factory := screen.new(reset: false)! + screen_factory.scan()! + if screen_factory.exists(process_name) { + screen_factory.kill(process_name)! + time.sleep(200 * time.millisecond) } } -// remove from the startup manager +// Test startup manager status functionality pub fn test_status() ! { mut sm := get()! + mut screen_factory := screen.new(reset: false)! + if sm.exists(process_name)! { sm.stop(process_name)! + time.sleep(200 * time.millisecond) + sm.start(process_name)! + time.sleep(500 * time.millisecond) // Give time for startup + status := sm.status(process_name)! assert status == .inactive } else { - sm.new(name: process_name, cmd: 'sleep 100')! + // Create new process with screen session + sm.new( + name: process_name + cmd: 'sleep 100' + description: 'Test process for startup manager' + )! + time.sleep(200 * time.millisecond) + sm.start(process_name)! + time.sleep(500 * time.millisecond) // Give time for startup + status := sm.status(process_name)! assert status == .active + + // Verify screen session + screen_factory.scan()! + assert screen_factory.exists(process_name), 'Screen session not found' } + + // Cleanup sm.stop(process_name)! + time.sleep(200 * time.millisecond) +} + +// Test process creation with description +pub fn test_process_with_description() ! { + mut sm := get()! + mut screen_factory := screen.new(reset: false)! + + description := 'Test process with custom description' + + // Create new process + sm.new( + name: '${process_name}_desc' + cmd: 'sleep 50' + description: description + )! + time.sleep(200 * time.millisecond) + + // Start and verify + sm.start('${process_name}_desc')! + time.sleep(500 * time.millisecond) + + // Verify screen session + screen_factory.scan()! + assert screen_factory.exists('${process_name}_desc'), 'Screen session not found' + + // Verify screen is running + mut screen := screen_factory.get('${process_name}_desc')! + assert screen.is_running()!, 'Screen should be running' + + // Cleanup + sm.stop('${process_name}_desc')! + time.sleep(200 * time.millisecond) + + // Verify screen is not running after cleanup + assert !screen.is_running()!, 'Screen should not be running after cleanup' +} + +// Test error handling +pub fn test_error_handling() ! { + mut sm := get()! + mut screen_factory := screen.new(reset: false)! + + // Test non-existent process + if _ := sm.status('nonexistent_process') { + assert false, 'Should not get status of non-existent process' + } else { + assert true + } + + // Test invalid screen session + if _ := screen_factory.get('nonexistent_screen') { + assert false, 'Should not get non-existent screen' + } else { + assert true + } + + // Test stopping non-existent process + if _ := sm.stop('nonexistent_process') { + assert false, 'Should not stop non-existent process' + } else { + assert true + } }