diff --git a/Makefile b/Makefile index 079d40e..0687bc8 100644 --- a/Makefile +++ b/Makefile @@ -2,5 +2,5 @@ build: go build -v ./cmd/CyclicCommandCheckerAndExecutive/CyclicCommandCheckerAndExecutive.go $(eval NEW_VER:=$(shell cat version | cut -d '_' -f 2 )) - mkdir build + mkdir -p build mv CyclicCommandCheckerAndExecutive ./build/CyclicCommandCheckerAndExecutive.$(NEW_VER) \ No newline at end of file diff --git a/README.md b/README.md index 868ba46..ca1ce14 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,54 @@ - **CLI Parameters**: Provides command-line options to start, stop, restart, or create jobs. - **Logging**: Logs execution results and errors for debugging purposes. +## Setting up a command +to create a configuration file, use +```bash +nano /etc/CyclicCommandCheckerAndExecutive/config.json +``` +Example of a configuration file: +```json +[ + { + "checkingCommand": "cat /path/to/your/file.txt | tr -d '\n'", + "interval": 5, + "branchCommand": [ + { + "resultExecution": "success", + "commands": [ + "echo 'Command executed successfully.'", + "echo '' > /path/to/your/file.txt" + ] + }, + { + "resultExecution": "failure", + "commands": [ + "bash command 1", + "bash command 2" + ] + } + ] + } +] +``` + +In this example, cat is used to display the contents of the specified file /path/to/your/file.txt . You should replace /path/to/your/file.txt to the actual path to the file you want to track. +```bash +cat /path/to/your/file.txt +``` +interval: Sets the time interval (in seconds) between each execution of the checkingCommand command. In this case, the value is 5, which means that the command will be executed every 5 seconds. + +branchCommand command: This section defines the actions to be performed based on the result of the checkingCommand command. + +resultExecution - The first object in the array indicates the actions to be performed if the string success (resultExecution: "success") was in the file. Here it includes: + +commands - the second object is an array of commands that will be executed if the result of executing the checkingCommand command matches the expected resultExecution + +In this example, Command executed successfully will be output to the application logs. and the file /path/to/your/file.txt it will be overwritten with an empty string. + +To use this configuration, save it in your configuration file (for example, /etc/CyclicCommandCheckerAndExecutive/config.json). The application will read this configuration and execute the cat command every 5 seconds, handling success and failure cases as defined. + + #### Components - **CliParamsParser**: Parses command-line arguments to determine the action to be performed. - **Actions**: Contains functions for starting, stopping, restarting, and creating jobs. @@ -22,8 +70,13 @@ - **FileUtils**: Utility functions for file operations. #### Usage +#### sudo permissions are required to use the utility. + 1. **Configuration**: Define your commands in `/etc/CyclicCommandCheckerAndExecutive/config.json`. 2. **Start the Application**: Use `-start` parameter to start the cyclic execution. +```bash +sudo ./build/CyclicCommandCheckerAndExecutive.0.0.1 +``` 3. **Stop the Application**: Use `-stop` parameter to stop the application. 4. **Restart the Application**: Use `-restart` parameter to restart the application. 5. **Create a Job**: Use `-create-job` parameter to create new job entries. @@ -33,6 +86,7 @@ **CyclicCommandCheckerAndExecutive** — это приложение на языке Go, предназначенное для циклического выполнения команд и управления их выполнением на основе заранее заданных условий. Оно предоставляет функционал для запуска, остановки, перезапуска и создания заданий через параметры командной строки. Программа может работать в режиме демона, обеспечивая непрерывную работу и мониторинг. #### Основные возможности +#### для использования утилиты требуются разрешения sudo - **Циклическое выполнение команд**: Программа повторно выполняет команды, указанные в конфигурационном файле. - **Логика ветвления**: Поддерживает условное выполнение команд на основе результатов ранее выполненных команд. - **Режим демона**: Работает как фоновый сервис. @@ -40,6 +94,56 @@ - **Параметры командной строки**: Предоставляет опции командной строки для запуска, остановки, перезапуска или создания заданий. - **Логирование**: Ведет журнал результатов выполнения и ошибок для целей отладки. +## Настройка комманд +для создания конфигурационного файла используйте +```bash +nano /etc/CyclicCommandCheckerAndExecutive/config.json +``` +Пример конфигурационного файла: +```json +[ + { + "checkingCommand": "cat /path/to/your/file.txt | tr -d '\n'", + "interval": 5, + "branchCommand": [ + { + "resultExecution": "success", + "commands": [ + "echo 'Command executed successfully.'", + "echo '' > /path/to/your/file.txt" + ] + }, + { + "resultExecution": "failure", + "commands": [ + "log_error", + "send_alert" + ] + } + ] + } +] + + +``` + +В этом примере для отображения содержимого указанного файла используется cat /path/to/your/file.txt. Вам следует заменить /path/to/your/file.txt на фактический путь к файлу, который вы хотите отслеживать. +```bash +cat /path/to/your/file.txt +``` +интервал: задает интервал времени (в секундах) между каждым выполнением команды checkingCommand. В данном случае значение равно 5, что означает, что команда будет выполняться каждые 5 секунд. + +Команда branchCommand: В этом разделе определяются действия, которые необходимо выполнить на основе результата команды checkingCommand. + +resultExecution - Первый объект в массиве указывает действия, которые необходимо выполнить в случае если в файле лежала строка success (resultExecution: "success"). Здесь он включает в себя: + +commands - второй объект это массив команд, которые будут выполнены в случае если результат выполнения команды checkingCommand будет совпадать с ожидаемым resultExecution + +В данном примере будет выведено в логи приложения Command executed successfully. и файл /path/to/your/file.txt будет перезаписан пустой строкой. + +Чтобы использовать эту конфигурацию, сохраните ее в своем конфигурационном файле (например, /etc/CyclicCommandCheckerAndExecutive/config.json). Приложение будет считывать эту конфигурацию и выполнять команду cat каждые 5 секунд, обрабатывая случаи успешного выполнения и сбоя, как определено. + + #### Компоненты - **CliParamsParser**: Разбирает аргументы командной строки для определения действия, которое необходимо выполнить. - **Actions**: Содержит функции для запуска, остановки, перезапуска и создания заданий. diff --git a/example.json b/example.json new file mode 100644 index 0000000..e0a764f --- /dev/null +++ b/example.json @@ -0,0 +1,22 @@ +[ + { + "checkingCommand": "cat /path/to/your/file.txt | tr -d '\n'", + "interval": 5, + "branchCommand": [ + { + "resultExecution": "success", + "commands": [ + "echo 'Command executed successfully.'", + "echo '' > /path/to/your/file.txt" + ] + }, + { + "resultExecution": "failure", + "commands": [ + "log_error", + "send_alert" + ] + } + ] + } +] \ No newline at end of file diff --git a/go.mod b/go.mod index 7b6ecca..fa46860 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,10 @@ module github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive + +go 1.21.9 + +require github.com/sevlyar/go-daemon v0.1.6 + +require ( + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..da1af94 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/sevlyar/go-daemon v0.1.6 h1:EUh1MDjEM4BI109Jign0EaknA2izkOyi0LV3ro3QQGs= +github.com/sevlyar/go-daemon v0.1.6/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/Actions/CreateJob.go b/internal/Actions/CreateJob.go index bd342fc..750b3a4 100644 --- a/internal/Actions/CreateJob.go +++ b/internal/Actions/CreateJob.go @@ -1,103 +1,7 @@ package Actions -import ( - "bufio" - "encoding/json" - "fmt" - "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/Config" - "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/fileutils" - "os" - "strconv" - "strings" -) +import "fmt" func CreateJob() { - configPath := Config.ConfigPath - config := Config.CreateConfig(configPath) - - reader := bufio.NewReader(os.Stdin) - - for { - fmt.Println("1. Create a new command for monitoring") - fmt.Println("2. Save changes and exit") - fmt.Print("Choose an option: ") - option, _ := reader.ReadString('\n') - option = strings.TrimSpace(option) - - if option == "2" { - break - } - - if option == "1" { - var newCommand Config.Command - - fmt.Print("Enter the command for cyclic execution and monitoring: ") - command, _ := reader.ReadString('\n') - newCommand.Command = strings.TrimSpace(command) - - fmt.Print("Enter the time interval in seconds for executing the above command: ") - intervalStr, _ := reader.ReadString('\n') - interval, err := strconv.Atoi(strings.TrimSpace(intervalStr)) - if err != nil { - fmt.Println("Invalid interval. Please enter a number.") - continue - } - newCommand.Interval = interval - - for { - fmt.Println("1. Add a new expected result") - fmt.Println("2. Finish configuration and proceed to save") - fmt.Print("Choose an option: ") - resultOption, _ := reader.ReadString('\n') - resultOption = strings.TrimSpace(resultOption) - - if resultOption == "2" { - break - } - - if resultOption == "1" { - var branch Config.BranchResultExecution - - fmt.Print("Enter the expected result: ") - result, _ := reader.ReadString('\n') - branch.ResultExecution = strings.TrimSpace(result) - - for { - fmt.Println("1. Enter a command to execute if the result matches") - fmt.Println("2. Create a new expected result") - fmt.Print("Choose an option: ") - commandOption, _ := reader.ReadString('\n') - commandOption = strings.TrimSpace(commandOption) - - if commandOption == "2" { - break - } - - if commandOption == "1" { - fmt.Print("Enter the command to execute if the result matches: ") - execCommand, _ := reader.ReadString('\n') - branch.Commands = append(branch.Commands, strings.TrimSpace(execCommand)) - } - } - - newCommand.BranchCommand = append(newCommand.BranchCommand, branch) - } - } - - config = append(config, newCommand) - } - } - - // Display and save the new configuration - configData, err := json.MarshalIndent(config, "", " ") - if err != nil { - fmt.Println("Error marshalling config:", err) - return - } - - fmt.Println("New configuration:") - fmt.Println(string(configData)) - - fileutils.RewriteFile(string(configData), configPath) - fmt.Println("Configuration saved successfully.") + fmt.Println("this logic will appear in future versions") } diff --git a/internal/Actions/Restart.go b/internal/Actions/Restart.go index 28b0b30..f4a607e 100644 --- a/internal/Actions/Restart.go +++ b/internal/Actions/Restart.go @@ -1,5 +1,7 @@ package Actions -func Restart() { +import "fmt" +func Restart() { + fmt.Println("this logic will appear in future versions") } diff --git a/internal/Actions/Start.go b/internal/Actions/Start.go index cc5e5b2..0e84e6c 100644 --- a/internal/Actions/Start.go +++ b/internal/Actions/Start.go @@ -2,10 +2,73 @@ package Actions import ( "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/Config" + "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/RunController" + "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/fileutils" + "github.com/sevlyar/go-daemon" "log" + "os/exec" + "time" ) func Start() { - config := Config.CreateConfig("") - log.Print(config) + CommandConfig := Config.CreateConfig("") + //todo приложениие теряет контекст и аргументы , с которыми оно было запущено здесь дебажить конфиг и ключи + log.Println("CommandConfig = ") + log.Println(CommandConfig) + + fileutils.CheckAndCreatDir(RunController.RunDirName) + fileutils.CheckAndCreatDir(RunController.LogDir) + cntxt := &daemon.Context{ + PidFileName: RunController.RunDirName + RunController.PidFile, + PidFilePerm: 0644, + LogFileName: RunController.LogDir + ".sample.log", + LogFilePerm: 0640, + WorkDir: "/", + Umask: 027, + Args: []string{"[go-daemon CyclicCommandCheckerAndExecutive] -start"}, + } + + d, err := cntxt.Reborn() + if err != nil { + log.Fatal("Unable to run: ", err) + } + if d != nil { + return + } + defer cntxt.Release() + + runCyclicCommand(CommandConfig) +} +func runCyclicCommand(CommandConfig Config.Config) { + for _, command := range CommandConfig { + //runCommand(command) + go runCommand(command) + } + RunController.RunningController() +} + +func runCommand(command Config.Command) { + for { + commandResult, _ := exec.Command("/bin/sh", "-c", command.Command).Output() + + log.Println("command from command`" + command.Command + "` result is `" + string(commandResult) + "`") + for _, branch := range command.BranchCommand { + log.Println("branch.ResultExecution=`" + branch.ResultExecution + "` commandResult=`" + string(commandResult) + "`") + if branch.ResultExecution == string(commandResult) { + executeCommands(branch.Commands) + } + } + if string(commandResult) == "" { + log.Fatalln("test") + } + log.Println(string(commandResult)) + time.Sleep(time.Duration(command.Interval) * time.Second) + } +} + +func executeCommands(commands []string) { + for _, cmd := range commands { + result, _ := exec.Command("/bin/sh", "-c", cmd).Output() + log.Println("command:" + cmd + "execute from result:" + string(result)) + } } diff --git a/internal/Actions/Stop.go b/internal/Actions/Stop.go index 1439d47..a74f725 100644 --- a/internal/Actions/Stop.go +++ b/internal/Actions/Stop.go @@ -1,5 +1,29 @@ package Actions -func Stop() { +import ( + "fmt" + "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/RunController" + "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/fileutils" + "log" + "time" +) -} \ No newline at end of file +func Stop() { + if !fileutils.HasFile(RunController.RunDirName + RunController.PidFile) { + log.Println("App not running") + } else { + fmt.Printf("%s", "stopping app in progress") + fileutils.WriteFile("", RunController.RunDirName+RunController.StopFile) + // TODO сделать проверку на запущенный процесс с пидом указанным в файлике старта + for { + isStoped := fileutils.ReadFile(RunController.RunDirName + RunController.StopFile) + fmt.Printf("%s", ".") + if isStoped == "stopped" { + fmt.Println("") + log.Println("app is stoped") + break + } + time.Sleep(1 * time.Second) + } + } +} diff --git a/internal/CliParamsParser/CliParamsParser.go b/internal/CliParamsParser/CliParamsParser.go index 66467bc..87bbe0d 100644 --- a/internal/CliParamsParser/CliParamsParser.go +++ b/internal/CliParamsParser/CliParamsParser.go @@ -8,6 +8,7 @@ import ( type CliParams struct { Action string + Debug bool } const ( @@ -23,12 +24,22 @@ func NewCliParams() *CliParams { var start bool var createJob bool var restart bool + var debug bool flag.BoolVar(&stop, "stop", false, "set this param to stopping demon") + flag.BoolVar(&debug, "debug", false, "set this param to write logs executing") flag.BoolVar(&start, "start", false, "set this param from start program") flag.BoolVar(&createJob, "create-job", false, "set this param from start program") flag.BoolVar(&restart, "restart", false, "set this param from start program") - if stop { + + flag.Parse() + + if debug { + c.Debug = true + } else { + c.Debug = false + } + if start { c.Action = Start } if stop { @@ -41,8 +52,9 @@ func NewCliParams() *CliParams { c.Action = Restart } if c.Action == "" { - log.Fatal("To run the program, use one of the keys -stop, -start, -create-job, -restart is required.") + c.Action = Start } + log.Println(c) return &c } func (c CliParams) StartAction() { diff --git a/internal/Config/Config.go b/internal/Config/Config.go index 4ac749d..4b5a36b 100644 --- a/internal/Config/Config.go +++ b/internal/Config/Config.go @@ -9,6 +9,8 @@ import ( "path/filepath" ) +const ConfigPath = "/etc/CyclicCommandCheckerAndExecutive/config.json" + type Config []Command // Config структура для хранения конфигурации @@ -28,7 +30,7 @@ func (config *Config) create(configPath string) { func CreateConfig(configPath string) Config { var config Config if configPath == "" { - configPath = "/etc/ContainerFailedListener/config.json" + configPath = ConfigPath } dirPath := filepath.Dir(configPath) diff --git a/internal/RunController/RunController.go b/internal/RunController/RunController.go new file mode 100644 index 0000000..b6fd001 --- /dev/null +++ b/internal/RunController/RunController.go @@ -0,0 +1,37 @@ +package RunController + +import ( + "fmt" + "github.com/SidorkinAlex/CyclicCommandCheckerAndExecutive/internal/fileutils" + "log" + "os" + "time" +) + +const RunDirName = "/var/run/CyclicCommandCheckerAndExecutive/" +const LogDir = "/var/log/CyclicCommandCheckerAndExecutive/" +const StopFile = ".stop.pid" +const PidFile = ".sample.pid" + +func RunningController() { + if fileutils.HasFile(RunDirName + StopFile) { + err := os.Remove(RunDirName + StopFile) + if err != nil { + log.Fatal("the stop file cannot be deleted. The program CyclicCommandCheckerAndExecutive exited with an error. ") + return + } + } + log.Println("Program CyclicCommandCheckerAndExecutive has been success running") + for { + if _, err := os.Stat(RunDirName + StopFile); err == nil { + fileutils.WriteFile("stopped", RunDirName+StopFile) + RemovePidFile() + fmt.Println("") + log.Fatalln("service CyclicCommandCheckerAndExecutive stopped") + } + time.Sleep(5 * time.Second) + } +} +func RemovePidFile() { + fileutils.WriteFile("", RunDirName+PidFile) +} diff --git a/internal/fileutils/file_utils.go b/internal/fileutils/file_utils.go index b687d36..1224dff 100755 --- a/internal/fileutils/file_utils.go +++ b/internal/fileutils/file_utils.go @@ -83,3 +83,12 @@ func ReadFile(filename string) string { return fileContent } + +func CheckAndCreatDir(dirPath string) { + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + err := os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + log.Fatalf("Error creating config dirrectory: %v\n", err) + } + } +}