diff --git a/README.md b/README.md index 68da355..7e871e5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Options: -u user the default user name to use when connecting to the remote server -c opts specify the common ssh options (i.e. '-p 22 -i identity_file') -o file write remote command output to a file + -O write remote command output to one log file per server + > file name like ~/.hss//date_per_day.log + -s transfer files over scp + -i force use a vi-style line editing interface -v be more verbose -V show program version @@ -40,6 +44,31 @@ For more information, see https://github.com/six-ddc/hss [![asciicast](https://asciinema.org/a/233954.svg)](https://asciinema.org/a/233954) +### Hosts files + +You can pass "-f hostfile" where hostfile is a full path of a plain text file. + +The application also uses folder named .hss/ in home as a default for the host files. +if the path given is not found, **the program will look for the file in .hss folder.** +You can then maintain easily hosts groups in a central way. + +## scp transfer files + +Please look at these commands: + + hss -f demo -s /path/to/main.c remote:/tmp + hss -f demo -s remote:/path/to/main.c /tmp + +You have to specify which side is the remote machine as SCP can go in either direction. +You specify it always using "remote" keyword (it is not an alias) + +you can also type: + + hss -f demo -s /path/to/main.c + +In this case /tmp is used as the default target. + + ## How to install it? * MacOS diff --git a/common.h b/common.h index 7330d28..0c6b640 100644 --- a/common.h +++ b/common.h @@ -15,6 +15,8 @@ #ifndef NOT_USER_COLOR + + // RL_PROMPT_START_IGNORE RL_PROMPT_END_IGNORE #define ANSI_COLOR_RED "\001\x1b[31m\002" #define ANSI_COLOR_GREEN "\001\x1b[32m\002" @@ -32,6 +34,28 @@ #define ANSI_COLOR_CYAN_BOLD "\001\x1b[36;1m\002" #define ANSI_COLOR_RESET "\001\x1b[0m\001" + +/* +// from pull https://github.com/six-ddc/hss/pull/9 by zhanglistar : ANSI_COLOR_RESET cause abnormal display on ubuntu 18.04 #8 +// RL_PROMPT_START_IGNORE RL_PROMPT_END_IGNORE +#define ANSI_COLOR_RED "\033[0;31m" +#define ANSI_COLOR_GREEN "\033[0;32m" +#define ANSI_COLOR_YELLOW "\033[0;33m" +#define ANSI_COLOR_BLUE "\033[0;34m" +#define ANSI_COLOR_MAGENTA "\033[0;35m" +#define ANSI_COLOR_CYAN "\033[0;36m" + +#define ANSI_COLOR_BOLD "\033[1m" +#define ANSI_COLOR_RED_BOLD "\033[1;31m" +#define ANSI_COLOR_GREEN_BOLD "\033[1;31m" +#define ANSI_COLOR_YELLOW_BOLD "\033[1;31m" +#define ANSI_COLOR_BLUE_BOLD "\033[1;31m" +#define ANSI_COLOR_MAGENTA_BOLD "\033[1;31m" +#define ANSI_COLOR_CYAN_BOLD "\033[1;31m" + +#define ANSI_COLOR_RESET "\033[0m" +*/ + #else #define ANSI_COLOR_RED #define ANSI_COLOR_GREEN @@ -39,7 +63,16 @@ #define ANSI_COLOR_BLUE #define ANSI_COLOR_MAGENTA #define ANSI_COLOR_CYAN -#define ANSI_COLOR_RESET + +#define ANSI_COLOR_BOLD +#define ANSI_COLOR_RED_BOLD +#define ANSI_COLOR_GREEN_BOLD +#define ANSI_COLOR_YELLOW_BOLD +#define ANSI_COLOR_BLUE_BOLD +#define ANSI_COLOR_MAGENTA_BOLD +#define ANSI_COLOR_CYAN_BOLD + +#define ANSI_COLOR_RESET " " #endif #define eprintf(...) fprintf(stderr, __VA_ARGS__) @@ -49,13 +82,20 @@ extern struct slot *slots; +enum mode { + MODE_SSH, + MODE_SCP, +}; + extern struct hss_config { bool verbose; char *user; int common_options_argc; const char **common_options_argv; char *output_file; + bool split_server_logs; int concurrency; + enum mode mode; } *pconfig; extern int stdout_isatty; diff --git a/executor.c b/executor.c index 9b74198..b6943d2 100644 --- a/executor.c +++ b/executor.c @@ -1,5 +1,7 @@ +#include #include #include +#include #include "common.h" #include "sstring.h" #include "executor.h" @@ -17,6 +19,8 @@ sigset_t sigmask; sigset_t osigmask; +typedef void (*executor)(struct slot *pslot, int argc, char **argv); + #define UPLOAD "0" #define DOWNLOAD "1" @@ -32,8 +36,158 @@ print_line(struct slot *pslot, int io_type, sstring buf, void *data) { printf(ANSI_COLOR_GREEN "[E] %s -> " ANSI_COLOR_RESET, pslot->host); } } - fwrite(buf, 1, string_length(buf), output); - fflush(output); + if (output != stdout) { + fwrite(buf, 1, string_length(buf), output); + fflush(output); + } + fwrite(buf, 1, string_length(buf), stdout); + fflush(stdout); +} + +static char * +server_specific_directory(const char *path, const struct slot *pslot) { + char nameBuf[1024]; + char *end = nameBuf; + size_t chars; + size_t remain = 1023; + + // find text after last / + char *name = strrchr(path, '/') + 1; + + chars = name - path; + if (chars > remain) { + return NULL; + } + memcpy(end, path, chars); + remain -= chars; + end += chars; + + // append host + chars = strlen(pslot->host); + if (chars > remain) { + return NULL; + } + strncpy(end, pslot->host, remain); + remain -= chars; + end += chars; + + mkdir(nameBuf, S_IRWXU | S_IRWXG); + + // Append filename + name--; // Take into account / + + chars = strlen(name); + if (chars > remain) { + return NULL; + } + + strncpy(end, name, remain); + remain -= chars; + end += chars; + + char *dirName = malloc(end - nameBuf + 1); + strcpy(dirName, nameBuf); + return dirName; +} + +//return success flag +int +insure_folder_exists(char *subFolder) { + + char nameBuf[1024]; + size_t chars; + const char *homeDir = getenv("HOME"); + if (!homeDir) { + eprintf("no home directory set\n"); + return false; + } + + //creates the parent logs folder + chars = snprintf(nameBuf, 1024, "%s/%s", homeDir,subFolder); + if (chars >= 1024) { + eprintf("file name too long for %s\n", subFolder); + return false; + } else if (chars < 0) { + eprintf("failed to encode file name for %s\n", subFolder); + return false; + } + + //return zero on success, or -1 if an error occurred + return mkdir(nameBuf, S_IRWXU | S_IRWXG)==0; +} + +static FILE * +server_log_file(const struct slot *pslot) { + char nameBuf[1024]; + size_t chars; + FILE *output; + time_t now = time(NULL); + + const char *homeDir = getenv("HOME"); + if (!homeDir) { + eprintf("no home directory set\n"); + return NULL; + } + + insure_folder_exists(".hss"); + //creates the parent logs folder + insure_folder_exists(".hss/logs"); + + chars = snprintf(nameBuf, 1024, "%s/.hss/logs/", homeDir); + + if (chars >= 1024) { + eprintf("file name too long for %s\n", pslot->host); + return NULL; + } else if (chars < 0) { + eprintf("failed to encode file name for %s\n", pslot->host); + return NULL; + } + + chars = strftime(nameBuf + chars, 1024 - chars, "%F.log", localtime(&now)); + if (chars == 0) { + eprintf("file name too long for %s\n", pslot->host); + return NULL; + } + + char *fileName = server_specific_directory(nameBuf, pslot); + if (fileName == NULL) { + eprintf("file name too long for %s\n", pslot->host); + return NULL; + } + + output = fopen(fileName, "a"); + if (!output) { + eprintf("can not open file %s (%s)\n", fileName, strerror(errno)); + free(fileName); + return NULL; + } + free(fileName); + + return output; +} + +static char * +scp_remote_filepath(const char *filename, struct slot *pslot) { + char host[256]; + + if (!strchr(pslot->host, '@') && pconfig->user) { + int err = snprintf(host, sizeof(host), "%s@%s", pconfig->user, pslot->host); + if (err == 0) { + eprintf("host too long for %s\n", pslot->host); + return NULL; + } + } else { + if (strlen(pslot->host) >= 256) { + eprintf("host too long for %s\n", pslot->host); + return NULL; + } + strcpy(host, pslot->host); + } + + size_t filepathLen = strlen(filename) + strlen(host) + 2; + char *filepath = malloc(filepathLen); + snprintf(filepath, filepathLen, "%s:%s", host, filename); + return filepath; } static void @@ -98,17 +252,17 @@ poll_alive_slots(struct slot *pslot, struct pollfd *pfd, size_t *cnt) { } static void -read_alive_slots(struct slot *pslot, struct pollfd *pfd, FILE *output) { +read_alive_slots(struct slot *pslot, struct pollfd *pfd) { while (pslot) { if (!pslot->alive || pslot->poll_index < 0) { pslot = pslot->next; continue; } if (pfd[pslot->poll_index].revents & POLLIN) { - slot_read_line(pslot, STDOUT_FILENO, print_line, output); + slot_read_line(pslot, STDOUT_FILENO, print_line, pslot->output); } if (pfd[pslot->poll_index + 1].revents & POLLIN) { - slot_read_line(pslot, STDERR_FILENO, print_line, output); + slot_read_line(pslot, STDERR_FILENO, print_line, pslot->output); } pslot = pslot->next; } @@ -181,32 +335,125 @@ exec_ssh_cmd(struct slot *pslot, int argc, char **argv) { ssh_argv[idx++] = pslot->ssh_argv[i]; } } + + ssh_argv[idx++] = "TERM=xterm-mono"; + for (i = 0; i < argc; ++i) { ssh_argv[idx++] = argv[i]; } ssh_argv[idx++] = NULL; + + if (pslot->output != stdout) { + printf("$"); + for (int i = 0; i < argc; i++) { + printf(" %s", argv[i]); + } + printf("\n"); + fflush(stdout); + } + ret = execvp("ssh", ssh_argv); eprintf("failed to exec the ssh binary: (%d) %s\n", ret, strerror(errno)); exit(1); } +static void +exec_scp_cmd(struct slot *pslot, int argc, char **argv) { + char *scp_argv[128]; + int idx = 0; + int i; + int ret; + + close(STDIN_FILENO); + + close(pslot->io.out[PIPE_READ_END]); + close(pslot->io.err[PIPE_READ_END]); + + if (dup2(pslot->io.out[PIPE_WRITE_END], STDOUT_FILENO) == -1) { + eprintf("failed to dup stdout: %s\n", strerror(errno)); + } + if (dup2(pslot->io.err[PIPE_WRITE_END], STDERR_FILENO) == -1) { + eprintf("failed to dup stderr: %s\n", strerror(errno)); + } + + scp_argv[idx++] = "-q"; + scp_argv[idx++] = "-oNumberOfPasswordPrompts=0"; + scp_argv[idx++] = "-oStrictHostKeyChecking=no"; + + for (i = 0; i < pconfig->common_options_argc; ++i) { + scp_argv[idx++] = (char *) pconfig->common_options_argv[i]; + } + + for (i = 0; i < pslot->ssh_argc - 1; ++i) { + scp_argv[idx++] = pslot->ssh_argv[i]; + } + if (argc < 1) { + eprintf("scp requires at least one argument\n"); + exit(1); + } else if (argc == 1) { + // Transfer file to /tmp + scp_argv[idx++] = argv[0]; + scp_argv[idx++] = scp_remote_filepath("/tmp/", pslot); + } else if (argc == 2) { + if (strncmp(argv[0], "remote:", 7) == 0) { + char dstBuffer[1024]; + if (strlen(argv[1]) > 1022) { + eprintf("destination filename too long for %s\n", pslot->host); + } + if (argv[1][strlen(argv[1]) - 1] != '/') { + strncpy(dstBuffer, argv[1], 1022); + strncat(dstBuffer, "/", 1023); + } else { + strncpy(dstBuffer, argv[1], 1023); + } + mkdir(dstBuffer, S_IRWXU | S_IRWXG); + + scp_argv[idx++] = scp_remote_filepath(argv[0] + 7, pslot); + scp_argv[idx++] = server_specific_directory(dstBuffer, pslot); + if (scp_argv[idx - 1] == NULL) { + eprintf("destination filename too long for %s\n", pslot->host); + } + } else if (strncmp(argv[1], "remote:", 7) == 0) { + scp_argv[idx++] = argv[0]; + scp_argv[idx++] = scp_remote_filepath(argv[1] + 7, pslot); + } else { + eprintf("no remote file specified (\"%s\", \"%s\")\n", argv[0], argv[1]); + exit(1); + } + } else { + eprintf("scp requires at most two arguments, %i provided\n", argc); + exit(1); + } + scp_argv[idx++] = NULL; + + ret = execvp("scp", scp_argv); + + eprintf("failed to exec the scp binary: (%d) %s\n", ret, strerror(errno)); + exit(1); +} + +executor executors[] = { + [MODE_SSH] = exec_ssh_cmd, + [MODE_SCP] = exec_scp_cmd, +}; + static int -exec_command_foreach(struct slot *pslot_list, void (*fn_fork)(struct slot *, int, char **), int argc, char **argv, +exec_command_foreach(struct slot *pslot_list, executor fn_fork, int argc, char **argv, int concurrency) { struct pollfd *pfd; struct slot *pslot_head = pslot_list->next; struct slot *pslot = pslot_head; int timeout; - FILE *output; + FILE *output = NULL; size_t cnt = 0; if (!pslot) { return 0; } - if (pconfig->output_file) { + if (pconfig->output_file && !pconfig->split_server_logs) { output = fopen(pconfig->output_file, "a"); if (!output) { eprintf("can not open file %s (%s)\n", pconfig->output_file, strerror(errno)); @@ -219,7 +466,14 @@ exec_command_foreach(struct slot *pslot_list, void (*fn_fork)(struct slot *, int while (pslot) { // poll stdout & stderr cnt += 2; - pslot->output = output; + if (pconfig->split_server_logs) { + pslot->output = server_log_file(pslot); + if (!pslot->output) { + return -1; + } + } else { + pslot->output = output; + } pslot = pslot->next; } pfd = calloc(cnt, sizeof(struct pollfd)); @@ -242,12 +496,18 @@ exec_command_foreach(struct slot *pslot_list, void (*fn_fork)(struct slot *, int timeout = pslot ? 0 : -1; if (poll(pfd, (nfds_t) cnt, timeout) > 0) { - read_alive_slots(pslot_head, pfd, output); + read_alive_slots(pslot_head, pfd); } UNBLOCK_SIGCHLD; } - if (output != stdout) { + if (pconfig->split_server_logs) { + pslot = pslot_head; + while (pslot) { + fclose(pslot->output); + pslot = pslot->next; + } + } else if (output != stdout) { fclose(output); } @@ -264,7 +524,7 @@ exec_remote_cmd(struct slot *pslot_list, char *cmd, int concurrency) { int exec_remote_cmd_args(struct slot *pslot_list, int argc, char **argv, int concurrency) { - return exec_command_foreach(pslot_list, exec_ssh_cmd, argc, argv, concurrency); + return exec_command_foreach(pslot_list, executors[pconfig->mode], argc, argv, concurrency); } int diff --git a/executor.h b/executor.h index cb5f0d8..422e6b1 100644 --- a/executor.h +++ b/executor.h @@ -17,4 +17,8 @@ exec_remote_cmd_args(struct slot *pslot_list, int argc, char **argv, int concurr int sync_exec_remote_cmd(struct slot *pslot_list, char *cmd, sstring *out, sstring *err); + +int +insure_folder_exists(char *subFolder); + #endif //_HSS_EXECUTOR_H_ diff --git a/main.c b/main.c index fa6fa1b..69c3369 100644 --- a/main.c +++ b/main.c @@ -104,8 +104,31 @@ add_hostfile(const char *fname) { char *p; FILE *f = fopen(fname, "r"); if (!f) { - eprintf("can not open file %s (%s)\n", fname, strerror(errno)); - exit(1); + if (errno == ENOENT) { + // No file? Try ~/.hss directory + char nameBuf[1024]; + + char *homeDir = getenv("HOME"); + if (!homeDir) { + eprintf("no home directory set\n"); + exit(1); + } + + int chars = snprintf(nameBuf, 1024, "%s/.hss/%s", homeDir, fname); + if (chars >= 1024) { + eprintf("hostfile name is too long\n"); + exit(1); + } else if (chars < 0) { + eprintf("encoding error while locating hostfile %s\n", fname); + exit(1); + } + + f = fopen(nameBuf, "r"); + } + if (!f) { + eprintf("can not open file %s (%s)\n", fname, strerror(errno)); + exit(1); + } } while (fgets(line, sizeof(line), f)) { @@ -131,7 +154,9 @@ void usage(const char *msg) { " -u user the default user name to use when connecting to the remote server\n" " -c opts specify the common ssh options (i.e. '-p 22 -i identity_file')\n" " -o file write remote command output to a file\n" + " -O write remote command output to log file per server\n" " -i force use a vi-style line editing interface\n" + " -s transfer files over scp\n" " -v be more verbose\n" " -V show program version\n" " -h display this message\n" @@ -157,7 +182,7 @@ parse_opts(int argc, char **argv) { int ret; int opt; - const char *short_opts = "hif:H:c:u:o:l:vV"; + const char *short_opts = "hif:H:c:u:o:Ol:svV"; pconfig = calloc(1, sizeof(struct hss_config)); @@ -191,6 +216,12 @@ parse_opts(int argc, char **argv) { case 'o': pconfig->output_file = new_string(optarg); break; + case 'O': + pconfig->split_server_logs = true; + break; + case 's': + pconfig->mode = MODE_SCP; + break; case 'v': pconfig->verbose = true; break; @@ -247,7 +278,9 @@ main(int argc, char **argv) { rl_initialize(); using_history(); - sprintf(history_file, "%s/%s", getenv("HOME"), ".hss_history"); + insure_folder_exists(".hss"); + insure_folder_exists(".hss/logs"); + sprintf(history_file, "%s/.hss/logs/%s", getenv("HOME"), "history.log"); read_history(history_file); update_completion_function();