├── .gitignore ├── Emakefile ├── AUTHORS ├── src ├── Makefile.am ├── essh_scheduler.erl ├── essh_agent.erl └── essh.erl ├── Makefile.am ├── configure.ac ├── LICENSE.MIT ├── native └── linux │ └── readline_drv.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | priv 4 | *.o 5 | *.beam 6 | *.plt -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | {'src/*', 2 | [debug_info, 3 | {i,"include"}, 4 | {outdir,"ebin"} 5 | ]}. -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * E Chen 2 | 供职于网易杭州研究院前台技术中心,目前致力于云计算相关技术研发,常用语言Java,Erlang,偏好计算机语言,网络相关技术领域。 -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | #erlsrcdir = $(prefix)/src 2 | #erlsrc_DATA = essh.erl \ 3 | # essh_agent.erl \ 4 | # essh_scheduler.erl 5 | 6 | erlbeamdir = $(prefix)/erlyssh/ebin 7 | erlbeam_DATA = essh.beam \ 8 | essh_agent.beam \ 9 | essh_scheduler.beam 10 | 11 | CLEANFILES = $(erlbeam_DATA) 12 | SUFFIXES = .erl .beam 13 | .erl.beam: 14 | $(ERLC) $(ERLCFLAGS) -b beam $< 15 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = src 2 | libdir = $(prefix)/erlyssh/lib 3 | lib_LTLIBRARIES = libreadlinedrv.la 4 | libreadlinedrv_la_SOURCES = $(top_srcdir)/native/linux/readline_drv.c 5 | INCLUDES = -I $(ERLANG_ROOT_DIR)/usr/include 6 | libreadlinedrv_la_LIBADD = -lreadline -lhistory 7 | bin_PROGRAMS = essh 8 | 9 | essh: 10 | echo "#!/bin/sh\n" > essh 11 | echo "erl -noinput -pa $(prefix)/erlyssh/ebin -run essh start $(libdir) \$${ESSH_LIST_HOME}/\$$* " >>essh 12 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_PREREQ([2.68]) 5 | AC_INIT([readline_drv], [0.1], [radiumce@gmail.com]) 6 | AM_INIT_AUTOMAKE([foreign]) 7 | AC_PROG_LIBTOOL 8 | AC_CONFIG_SRCDIR([native/linux/readline_drv.c]) 9 | 10 | AC_ERLANG_NEED_ERLC 11 | AC_ERLANG_SUBST_ROOT_DIR 12 | AC_ERLANG_SUBST_LIB_DIR 13 | # Checks for programs. 14 | AC_PROG_CC 15 | 16 | CFLAGS="$CFLAGS -L/opt/local/lib -L/usr/lib -L/usr/local/lib -I/opt/local/include/readline -I/opt/include -I/usr/local/include" 17 | 18 | 19 | # Checks for typedefs, structures, and compiler characteristics. 20 | AC_TYPE_SIZE_T 21 | 22 | # Checks for library functions. 23 | AC_FUNC_MALLOC 24 | AC_CHECK_FUNCS([getcwd isascii select strdup strerror strrchr]) 25 | 26 | AC_CONFIG_FILES([Makefile src/Makefile]) 27 | AC_OUTPUT 28 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2012 NetEase, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the Software), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, andor sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /native/linux/readline_drv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "erl_driver.h" 3 | #include 4 | #include 5 | 6 | typedef struct { 7 | ErlDrvPort port; 8 | } readline_data; 9 | 10 | static char *line_read = (char*)NULL; 11 | static char *prompt = "essh>: "; 12 | 13 | static ErlDrvData libreadlinedrv_start(ErlDrvPort port, char *buff){ 14 | 15 | readline_data* d = (readline_data*)driver_alloc(sizeof(readline_data)); 16 | d->port = port; 17 | return (ErlDrvData)d; 18 | } 19 | 20 | static void libreadlinedrv_stop(ErlDrvData handle) 21 | { 22 | driver_free((char*)handle); 23 | } 24 | 25 | static void libreadlinedrv_output(ErlDrvData handle, char *buff, int bufflen) 26 | { 27 | readline_data* d = (readline_data*)handle; 28 | 29 | if (line_read) 30 | { 31 | free(line_read); 32 | line_read = (char *)NULL; 33 | } 34 | 35 | line_read = readline(prompt); 36 | 37 | if (line_read && *line_read) 38 | add_history(line_read); 39 | 40 | driver_output(d->port, line_read, strlen(line_read)); 41 | } 42 | 43 | ErlDrvEntry readline_driver_entry = { 44 | NULL, /* F_PTR init, N/A */ 45 | libreadlinedrv_start, /* L_PTR start, called when port is opened */ 46 | libreadlinedrv_stop, /* F_PTR stop, called when port is closed */ 47 | libreadlinedrv_output, /* F_PTR output, called when erlang has sent */ 48 | NULL, /* F_PTR ready_input, called when input descriptor ready */ 49 | NULL, /* F_PTR ready_output, called when output descriptor ready */ 50 | "libreadlinedrv", /* char *driver_name, the argument to open_port */ 51 | NULL, /* F_PTR finish, called when unloaded */ 52 | NULL, /* F_PTR control, port_command callback */ 53 | NULL, /* F_PTR timeout, reserved */ 54 | NULL /* F_PTR outputv, reserved */ 55 | }; 56 | 57 | DRIVER_INIT(libreadlinedrv) /* must match name in driver_entry */ 58 | { 59 | return &readline_driver_entry; 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | erlyssh - A Parallel SSH Execution Tool 2 | ======= 3 | 4 | This is an Erlang Powered linux command line tool.You can use this tool to 5 | connect to mass of servers through SSH client simultaneously and parallel 6 | execute noninteractive shell commands on every connected server. 7 | 8 | * Homepage: 9 | * Tags: tools, ssh, operation, erlang 10 | 11 | 12 | Requirements: 13 | -------- 14 | 1. openssh client with agent foward enabled 15 | 2. GNU lib readline(libreadline.so,libhistory.so) 16 | 3. Erlang 5.6/OTP R12B or later 17 | 4. Linux or Mac OSX 18 | 5. autoconf automake libtool required for building 19 | 20 | Install: 21 | -------- 22 | run autoreconf -i to generate configure Makefile.in etc. 23 | 24 | run ./configure 25 | 26 | run sudo make install 27 | 28 | or you can provide --prefix=/custom-install-path/ as ./configure option 29 | 30 | if any buid error occurs, try to check 31 | 32 | 1. Is libreadline's arch compatible with your erlang runtime 33 | 2. Check include path and library path, add your custom -I -L to CFLAGS env 34 | 3. Is your default system arch compatible with your erlang runtime, otherwise 35 | add -arch parameter to CFLAGS env 36 | 37 | Usage: 38 | -------- 39 | ###configure server list 40 | export ESSH_LIST_HOME = /your-path-to-put-server-list-files 41 | 42 | in ESSH_HOME_DIR put your server list files, Use the following format: 43 | 44 | www-11.internal.org 45 | www-15.internal.org 46 | 172.19.0.86 47 | -p1046 172.19.1.87 48 | #comment: each server address(domain or ip) a line 49 | #as ssh command's paras` 50 | 51 | 52 | ###run shell script 53 | Start erlyssh with: 54 | 55 | radiumce@app-88:~$ essh cometd 56 | -----------------www-11 connected------------------- 57 | -----------------www-15 connected------------------- 58 | 59 | essh>: 60 | After 'essh>:' prompt, you can run any non-interactive commands. 61 | for exmaple: 62 | 63 | radiumce@app-88:~$ essh cometd 64 | -----------------www-11 connected------------------- 65 | -----------------www-15 connected------------------- 66 | 67 | essh>: ls 68 | --------------www-11--------------- 69 | erlang 70 | 71 | 72 | [primary server done] 73 | ->> 74 | 75 | essh>: 76 | Input a 'ls' command, and it will parallel executed on www-11,www-15. 77 | In erlyssh the first server on the list will be the primary server. Its output is 78 | realtime(it is useful when running some long duration commands, such as 'svn up'). 79 | when the command on www-15 is done, it will compare with the out put of www-11. 80 | when their output is identical, erlssh just print a '->>' as www-15's output. 81 | 82 | 83 | essh>: #con 5 84 | set concurrent execution limits = 5 85 | essh>: 86 | Schedule parameter 'con'(limit of parallel connections, default 256) can be set by '#con number' 87 | 88 | 89 | essh>: #intv 5 90 | set execution interval = 5s 91 | essh>: 92 | Schedule parameter 'intv'(execution interval, default 0) can be set by '#intv ${seconds}' 93 | 94 | essh>: exit; 95 | Thanks for using essh, bye. 96 | Use command 'exit;'(ends with ';') to exit the shell. 97 | 98 | Tips: 99 | -------- 100 | 1. erlyssh is interactive(you can use 'cd' to change path) but it can only 101 | execute non-interactive commands. 102 | 2. When there is mass of servers, add 103 | 104 | CheckHostIP no 105 | 106 | StrictHostKeyChecking no 107 | 108 | to your .ssh/config can avoid some yes/no security confirm 109 | 3. Many shell commands have their non-interactive mode or corresponded 110 | non-interactive commands. Such as 'svn' commands have non-interactive 111 | mode and 'sed' can help you to perform plain text processing. 112 | 113 | 114 | How Can I Contribute 115 | -------------------- 116 | Fork this project on [GitHub](https://github.com/NetEase/erlyssh), add your improvement, push it to a branch in your fork named for the topic, send a pull request. 117 | 118 | You can also file bugs or feature requests under the [issues](https://github.com/NetEase/erlyssh/issues/) page on GitHub. 119 | -------------------------------------------------------------------------------- /src/essh_scheduler.erl: -------------------------------------------------------------------------------- 1 | %%% ------------------------------------------------------------------- 2 | %%% Author : CE 3 | %%% Description : 4 | %%% 5 | %%% Created : 2009-1-18 6 | %%% ------------------------------------------------------------------- 7 | -module(essh_scheduler). 8 | 9 | -behaviour(gen_server). 10 | %% -------------------------------------------------------------------- 11 | %% Include files 12 | %% -------------------------------------------------------------------- 13 | 14 | %% -------------------------------------------------------------------- 15 | %% External exports 16 | -export([start/0]). 17 | 18 | %% gen_server callbacks 19 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 20 | 21 | -record(state, {con = 256, intv = 0, pids = [], suspend = 0, cmd}). 22 | 23 | %% ==================================================================== 24 | %% External functions 25 | %% ==================================================================== 26 | %% 27 | %% TODO: Add description of start/function_arity 28 | %% 29 | start() -> 30 | gen_server:start_link({local,?MODULE}, ?MODULE, [], []), 31 | ok. 32 | 33 | 34 | %% ==================================================================== 35 | %% Server functions 36 | %% ==================================================================== 37 | 38 | %% -------------------------------------------------------------------- 39 | %% Function: init/1 40 | %% Description: Initiates the server 41 | %% Returns: {ok, State} | 42 | %% {ok, State, Timeout} | 43 | %% ignore | 44 | %% {stop, Reason} 45 | %% -------------------------------------------------------------------- 46 | init([]) -> 47 | {ok, #state{}}. 48 | 49 | %% -------------------------------------------------------------------- 50 | %% Function: handle_call/3 51 | %% Description: Handling call messages 52 | %% Returns: {reply, Reply, State} | 53 | %% {reply, Reply, State, Timeout} | 54 | %% {noreply, State} | 55 | %% {noreply, State, Timeout} | 56 | %% {stop, Reason, Reply, State} | (terminate/2 is called) 57 | %% {stop, Reason, State} (terminate/2 is called) 58 | %% -------------------------------------------------------------------- 59 | handle_call({opt, con ,V}, _From, State) -> 60 | Reply = ok, 61 | StateN = if 62 | V >= 0 -> 63 | io:format("set concurrent execution limits = ~p~n", [V]), 64 | State#state{con = V}; 65 | true -> 66 | io:format("error:the value must above 0~n"), 67 | State 68 | end, 69 | {reply, Reply, StateN}; 70 | handle_call({opt, intv ,V}, _From, State) -> 71 | Reply = ok, 72 | StateN = if 73 | V >= 0 -> 74 | V1 = V * 1000, 75 | io:format("set execution interval = ~ps~n", [V]), 76 | State#state{intv = V1}; 77 | true -> 78 | io:format("error:the value must above 0~n"), 79 | State 80 | end, 81 | {reply, Reply, StateN}; 82 | handle_call({cmd, Pids , Cmd}, _From, State = #state{con = Con}) -> 83 | {Pids1, Pids2} = split_pids(Pids, Con), 84 | send_cmd(Pids1, Cmd, 0), 85 | StateN = State#state{pids = Pids2, cmd = Cmd, suspend = length(Pids1)}, 86 | {reply, ok, StateN}; 87 | handle_call({continue}, _From, 88 | State = #state{con = Con, cmd = Cmd, intv = Intv, pids = Pids, suspend = 1}) -> 89 | case Pids of 90 | [] -> 91 | {reply, ok, State#state{suspend = 0}}; 92 | _ -> 93 | {Pids1, Pids2} = split_pids(Pids, Con), 94 | send_cmd(Pids1, Cmd, Intv), 95 | {reply, ok, State#state{pids = Pids2, suspend = length(Pids1)}} 96 | end; 97 | handle_call({continue}, _From, 98 | State = #state{suspend = Suspend}) when Suspend > 1-> 99 | {reply, ok, State#state{suspend = Suspend - 1}}. 100 | 101 | 102 | split_pids(Pids, Con) -> 103 | if 104 | length(Pids) =< Con -> 105 | {Pids, []}; 106 | true -> 107 | lists:split(Con, Pids) 108 | end. 109 | 110 | send_cmd(Pids, Cmd, Intv) -> 111 | spawn( 112 | fun() -> 113 | if 114 | Intv > 0 -> 115 | timer:sleep(Intv); 116 | true -> ok 117 | end, 118 | [gen_fsm:send_event(Pid, {cmd, Cmd}) || Pid <- Pids] 119 | end). 120 | 121 | %% -------------------------------------------------------------------- 122 | %% Function: handle_cast/2 123 | %% Description: Handling cast messages 124 | %% Returns: {noreply, State} | 125 | %% {noreply, State, Timeout} | 126 | %% {stop, Reason, State} (terminate/2 is called) 127 | %% -------------------------------------------------------------------- 128 | handle_cast(Msg, State) -> 129 | {noreply, State}. 130 | 131 | %% -------------------------------------------------------------------- 132 | %% Function: handle_info/2 133 | %% Description: Handling all non call/cast messages 134 | %% Returns: {noreply, State} | 135 | %% {noreply, State, Timeout} | 136 | %% {stop, Reason, State} (terminate/2 is called) 137 | %% -------------------------------------------------------------------- 138 | handle_info(Info, State) -> 139 | {noreply, State}. 140 | 141 | %% -------------------------------------------------------------------- 142 | %% Function: terminate/2 143 | %% Description: Shutdown the server 144 | %% Returns: any (ignored by gen_server) 145 | %% -------------------------------------------------------------------- 146 | terminate(Reason, State) -> 147 | ok. 148 | 149 | %% -------------------------------------------------------------------- 150 | %% Func: code_change/3 151 | %% Purpose: Convert process state when code is changed 152 | %% Returns: {ok, NewState} 153 | %% -------------------------------------------------------------------- 154 | code_change(OldVsn, State, Extra) -> 155 | {ok, State}. 156 | 157 | %% -------------------------------------------------------------------- 158 | %%% Internal functions 159 | %% -------------------------------------------------------------------- 160 | 161 | -------------------------------------------------------------------------------- /src/essh_agent.erl: -------------------------------------------------------------------------------- 1 | %%% ------------------------------------------------------------------- 2 | %%% Author : CE 3 | %%% Description : 4 | %%% 5 | %%% Created : 2009-1-2 6 | %%% ------------------------------------------------------------------- 7 | -module(essh_agent). 8 | 9 | -behaviour(gen_fsm). 10 | %% -------------------------------------------------------------------- 11 | %% Include files 12 | %% -------------------------------------------------------------------- 13 | 14 | %% -------------------------------------------------------------------- 15 | %% External exports 16 | -export([]). 17 | 18 | %% gen_fsm callbacks 19 | -export([init/1, connect/2, connected/2, connection_failed/2, handle_event/3, 20 | handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). 21 | 22 | -record(state, {server, primary, port}). 23 | -define(SSH, "ssh "). 24 | -define(MAGIC, "@[6769]@"). 25 | -define(ECHO, "echo @[6769]@"). 26 | -define(TIMEOUT, 60000). 27 | -define(CMD_TIMEOUT, 300000). 28 | %% ==================================================================== 29 | %% External functions 30 | %% ==================================================================== 31 | 32 | 33 | %% ==================================================================== 34 | %% Server functions 35 | %% ==================================================================== 36 | %% -------------------------------------------------------------------- 37 | %% Func: init/1 38 | %% Returns: {ok, StateName, StateData} | 39 | %% {ok, StateName, StateData, Timeout} | 40 | %% ignore | 41 | %% {stop, StopReason} 42 | %% -------------------------------------------------------------------- 43 | init([Server, Primary]) -> 44 | {ok, connect, #state{server = Server, primary = Primary}}. 45 | 46 | %% -------------------------------------------------------------------- 47 | %% Func: StateName/2 48 | %% Returns: {next_state, NextStateName, NextStateData} | 49 | %% {next_state, NextStateName, NextStateData, Timeout} | 50 | %% {stop, Reason, NewStateData} 51 | %% -------------------------------------------------------------------- 52 | connect(_, State = #state{server = Server}) -> 53 | %io:format("INFO:before connecting ~p~n", [Server]), 54 | case open_ssh_port(Server, 0) of 55 | {ok, Port} -> 56 | State1 = State#state{port = Port}, 57 | gen_server:call(essh, {res, Server, connected, ok}, infinity), 58 | {next_state, connected, State1}; 59 | {failed, _Reason} -> 60 | gen_server:call(essh, {res, Server, connected, failed}, infinity), 61 | {next_state, connection_failed, State} 62 | end. 63 | 64 | connected({cmd, C}, State = #state{server = Server, primary = Primary, port = Port}) -> 65 | send_cmd(Port, C), 66 | case receive_port_res(Primary, Port, []) of 67 | {ok, Res} -> 68 | gen_server:call(essh, {res, Server, responsed, Res}, infinity); 69 | {failed, Reason} -> 70 | gen_server:call(essh, {res, Server, failed, Reason}, infinity) 71 | end, 72 | {next_state, connected, State}. 73 | 74 | connection_failed(restart, State) -> 75 | {next_state, connect, State}; 76 | connection_failed( _Cmd , State = #state{server = Server}) -> 77 | gen_server:call(essh, {res, Server, failed, inactive}), 78 | {next_state, connection_failed, State}. 79 | 80 | send_cmd(Port, C) -> 81 | %io:format("INFO:send command ~p~n", [C]), 82 | Port ! {self(), {command, lists:concat([C, "\n"])}}, 83 | Port ! {self(), {command, "\n"}}, 84 | Port ! {self(), {command, lists:concat(["echo", "\n"])}}, 85 | Port ! {self(), {command, lists:concat([?ECHO, "\n"])}}. 86 | 87 | receive_port_res(Primary, Port, RAcc) -> 88 | receive 89 | {Port, {data, {eol, ?MAGIC}}} -> 90 | {ok, RAcc}; 91 | {Port, {data, {eol, Line}}} -> 92 | if 93 | %print primary server's response 94 | Primary =:= true -> 95 | io:format("~s~n", [Line]); 96 | true -> ok 97 | end, 98 | receive_port_res(Primary, Port, [Line|RAcc]); 99 | {'EXIT', Port, Reason} -> 100 | {failed, Reason} 101 | after ?CMD_TIMEOUT -> 102 | {failed, cmd_timeout} 103 | end. 104 | 105 | open_ssh_port(Server, Acc) -> 106 | if 107 | Acc < 3 -> 108 | Port = open_port({spawn, lists:concat([?SSH, Server])}, [{line, 4096}, stderr_to_stdout]), 109 | Port ! {self(), {command, lists:concat([?ECHO, "\n"])}}, 110 | receive 111 | {Port, {data, {eol, ?MAGIC}}} -> 112 | %io:format("INFO:~p connected ~n", [Server]), 113 | {ok, Port}; 114 | {'EXIT', Port, Reason} -> 115 | io:format("ERROR:ssh port exit reason:~p~n", [Reason]), 116 | {failed, Reason} 117 | after ?TIMEOUT -> 118 | io:format("ERROR:connecting to ~p timeout~n", [Server]), 119 | Port ! {self(), close}, 120 | receive 121 | {Port, closed} -> 122 | io:format("INFO:retry connecting to ~p after prev port closed~n", [Server]), 123 | open_ssh_port(Server, Acc + 1) 124 | after 10000 -> 125 | io:format("INFO:retry connecting to ~p after prev port closing timeout~n", [Server]), 126 | open_ssh_port(Server, Acc + 1) 127 | end 128 | end; 129 | true -> 130 | {failed, "max connection retry reached"} 131 | end. 132 | 133 | %% -------------------------------------------------------------------- 134 | %% Func: handle_event/3 135 | %% Returns: {next_state, NextStateName, NextStateData} | 136 | %% {next_state, NextStateName, NextStateData, Timeout} | 137 | %% {stop, Reason, NewStateData} 138 | %% -------------------------------------------------------------------- 139 | handle_event(Event, StateName, StateData) -> 140 | {next_state, StateName, StateData}. 141 | 142 | %% -------------------------------------------------------------------- 143 | %% Func: handle_sync_event/4 144 | %% Returns: {next_state, NextStateName, NextStateData} | 145 | %% {next_state, NextStateName, NextStateData, Timeout} | 146 | %% {reply, Reply, NextStateName, NextStateData} | 147 | %% {reply, Reply, NextStateName, NextStateData, Timeout} | 148 | %% {stop, Reason, NewStateData} | 149 | %% {stop, Reason, Reply, NewStateData} 150 | %% -------------------------------------------------------------------- 151 | handle_sync_event(Event, From, StateName, StateData) -> 152 | Reply = ok, 153 | {reply, Reply, StateName, StateData}. 154 | 155 | %% -------------------------------------------------------------------- 156 | %% Func: handle_info/3 157 | %% Returns: {next_state, NextStateName, NextStateData} | 158 | %% {next_state, NextStateName, NextStateData, Timeout} | 159 | %% {stop, Reason, NewStateData} 160 | %% -------------------------------------------------------------------- 161 | handle_info(Info, StateName, StateData) -> 162 | {next_state, StateName, StateData}. 163 | 164 | %% -------------------------------------------------------------------- 165 | %% Func: terminate/3 166 | %% Purpose: Shutdown the fsm 167 | %% Returns: any 168 | %% -------------------------------------------------------------------- 169 | terminate(Reason, StateName, StatData) -> 170 | ok. 171 | 172 | %% -------------------------------------------------------------------- 173 | %% Func: code_change/4 174 | %% Purpose: Convert process state when code is changed 175 | %% Returns: {ok, NewState, NewStateData} 176 | %% -------------------------------------------------------------------- 177 | code_change(OldVsn, StateName, StateData, Extra) -> 178 | {ok, StateName, StateData}. 179 | 180 | %% -------------------------------------------------------------------- 181 | %%% Internal functions 182 | %% -------------------------------------------------------------------- 183 | 184 | -------------------------------------------------------------------------------- /src/essh.erl: -------------------------------------------------------------------------------- 1 | %%% ------------------------------------------------------------------- 2 | %%% Author : CE 3 | %%% Description : 4 | %%% 5 | %%% Created : 2008-12-28 6 | %%% ------------------------------------------------------------------- 7 | -module(essh). 8 | 9 | -behaviour(gen_server). 10 | %% -------------------------------------------------------------------- 11 | %% Include files 12 | %% -------------------------------------------------------------------- 13 | 14 | %% -------------------------------------------------------------------- 15 | %% External exports 16 | -export([start/1]). 17 | 18 | %% gen_server callbacks 19 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, get_start_opts/2]). 20 | 21 | -record(options, {con = 256, intv = 0}). 22 | -record(state, {shell,pids = [], servers, cur = empty, opt = #options{}}). 23 | 24 | %% ==================================================================== 25 | %% External functions 26 | %% ==================================================================== 27 | %% 28 | %% 29 | start(StartParas) -> 30 | [SharedPath, FilePath | Command] = StartParas, 31 | {Command2, Options} = get_start_opts(Command, []), 32 | StartCommand = get_start_command(Command2, ""), 33 | SharedLib = "libreadlinedrv", 34 | %io:format("starting essh server~n"), 35 | essh_scheduler:start(), 36 | gen_server:start_link({local,?MODULE}, ?MODULE, 37 | [SharedPath, SharedLib, FilePath, StartCommand, Options], []). 38 | 39 | %% ==================================================================== 40 | %% Server functions 41 | %% ==================================================================== 42 | 43 | %% -------------------------------------------------------------------- 44 | %% Function: init/1 45 | %% Description: Initiates the server 46 | %% Returns: {ok, State} | 47 | %% {ok, State, Timeout} | 48 | %% ignore | 49 | %% {stop, Reason} 50 | %% -------------------------------------------------------------------- 51 | init([SharedPath, SharedLib, FilePath, StartCommand, Options]) -> 52 | [init_options(K, V) || {K, V} <- Options], 53 | Servers = try get_server_lst(FilePath) of 54 | S -> S 55 | catch 56 | _:_ -> io:format("ERROR: can't open server list file~n"), 57 | halt() 58 | end, 59 | %io:format("INFO:servers = ~p~n", [Servers]), 60 | [Primary | _ ] = Servers, 61 | put(primary, Primary), 62 | %io:format("INFO:primary server = ~p~n", [Primary]), 63 | Shell = case StartCommand of 64 | "" -> 65 | spawn_link(fun() -> loop(SharedPath, SharedLib) end); 66 | _ -> 67 | spawn_link(fun() -> non_interactive_loop(StartCommand) end) 68 | end, 69 | Pids = start_agents(Servers), 70 | call_ext(Pids, ""), 71 | {ok, #state{shell = Shell, pids = Pids, servers = Servers, cur = Servers}}. 72 | 73 | init_options(con, Val) -> 74 | gen_server:call(essh_scheduler, {opt, con, Val}), 75 | gen_server:cast(essh, {opt, con, Val}); 76 | init_options(intv, Val) -> 77 | gen_server:call(essh_scheduler, {opt, intv, Val}), 78 | gen_server:cast(essh, {opt, intv, Val}). 79 | 80 | 81 | get_start_opts([], Acc) -> 82 | {[], Acc}; 83 | get_start_opts([C], Acc) -> 84 | case parse_opt(C, []) of 85 | {opt, s, OptTup} -> 86 | {[], [OptTup|Acc]}; 87 | _ -> 88 | {[C], Acc} 89 | end; 90 | get_start_opts(Cmd = [C, C1|CR], Acc) -> 91 | case parse_opt(C, C1) of 92 | {opt, m, OptTup} -> 93 | get_start_opts(CR, [OptTup|Acc]); 94 | {opt, s, OptTup} -> 95 | get_start_opts([C1|CR], [OptTup|Acc]); 96 | no_opt -> 97 | {Cmd, Acc} 98 | end. 99 | 100 | parse_opt("-D", Opt) -> 101 | T = parse_opt2(Opt), 102 | {opt, m, T}; 103 | parse_opt("-D" ++ Opt, _) -> 104 | T = parse_opt2(Opt), 105 | {opt, s, T}; 106 | parse_opt(_, _) -> 107 | no_opt. 108 | 109 | parse_opt2(Opt) -> 110 | [K, V] = string:tokens(Opt, "="), 111 | case string:to_integer(V) of 112 | {IntVal, _} -> 113 | {erlang:list_to_atom(K), IntVal}; 114 | _ -> 115 | {erlang:list_to_atom(K), V} 116 | end. 117 | 118 | 119 | 120 | get_start_command([C | CR], Cmd) -> 121 | get_start_command(CR, lists:concat([Cmd, C, " "])); 122 | get_start_command([], Cmd) -> 123 | Cmd. 124 | 125 | get_server_lst(FilePath) -> 126 | {ok, IoDev} = file:open(FilePath, [read]), 127 | get_lst_from_file(IoDev, []). 128 | 129 | get_lst_from_file(IoDev, L) -> 130 | case io:get_line(IoDev, "") of 131 | eof -> 132 | lists:reverse(L); 133 | Line -> 134 | case get_server_line(Line) of 135 | undefined -> 136 | get_lst_from_file(IoDev, L); 137 | CurLine -> 138 | get_lst_from_file(IoDev, [CurLine | L]) 139 | end 140 | end. 141 | 142 | get_server_line(Line) -> 143 | CurLine = string:strip(Line, both, $\n), 144 | if 145 | CurLine =:= "" -> 146 | undefined; 147 | true -> 148 | [ C0 | _] = CurLine, 149 | if 150 | C0 =:= $# -> 151 | undefined; 152 | true -> 153 | CurLine 154 | end 155 | end. 156 | 157 | is_primary_server(S) -> 158 | P = get(primary), 159 | if 160 | P =:= S -> 161 | true; 162 | true -> 163 | false 164 | end. 165 | 166 | start_agent_fsm(S) -> 167 | IsP = is_primary_server(S), 168 | if 169 | IsP =:= true -> 170 | %io:format("start fsm of ~p as primary server~n", [S]), 171 | gen_fsm:start_link(essh_agent, [S, true], []); 172 | true -> 173 | gen_fsm:start_link(essh_agent, [S, false], []) 174 | end. 175 | 176 | start_agents(Servers) -> 177 | Pids = [Pid || {ok, Pid}<- [start_agent_fsm(S) || S <- Servers]], 178 | %io:format("INFO:agents = ~p~n", [Pids]), 179 | Pids. 180 | 181 | %% -------------------------------------------------------------------- 182 | %% Function: handle_call/3 183 | %% Description: Handling call messages 184 | %% Returns: {reply, Reply, State} | 185 | %% {reply, Reply, State, Timeout} | 186 | %% {noreply, State} | 187 | %% {noreply, State, Timeout} | 188 | %% {stop, Reason, Reply, State} | (terminate/2 is called) 189 | %% {stop, Reason, State} (terminate/2 is called) 190 | %% -------------------------------------------------------------------- 191 | handle_call({shell_cmd, Cmd}, _From, State) -> 192 | %io:format("shell cmd received ~p~n", [Cmd]), 193 | Cmd0 = string:strip(Cmd), 194 | process_input_cmd(State, Cmd0), 195 | {reply, {cmd, continue}, State}; 196 | 197 | %%handle multipul ssh connection initialization 198 | handle_call({res, Server, connected, Res}, _From, State)-> 199 | StateN = get_processed_state(Server, Res, fun print_connect_info/2, {init, shell}, State), 200 | %io:format("~p connected ok~n", [Server]), 201 | {reply, ok, StateN}; 202 | 203 | %%handle command results 204 | handle_call({res, Server, responsed, Res}, _From, State) -> 205 | gen_server:call(essh_scheduler, {continue}, infinity), 206 | %io:format("~p responsed: ~p~n", [Server, Res]), 207 | StateN = get_processed_state(Server, Res, fun print_cmd_res/2, done, State), 208 | {reply, ok, StateN}; 209 | handle_call({res, Server, failed, Reason}, _From, State) -> 210 | gen_server:call(essh_scheduler, {continue}, infinity), 211 | io:format("~p failed: ~p~n", [Server, Reason]), 212 | StateN = get_processed_state(Server, Reason, fun print_cmd_res/2, done, State), 213 | {reply, ok, StateN}. 214 | 215 | %%----------------------------------------------------------------- 216 | get_int_value(Value) -> 217 | Vl1 = string:strip(Value), 218 | try list_to_integer(Vl1) of 219 | Vl2 -> Vl2 220 | catch 221 | _ : X0 -> {error, X0} 222 | end. 223 | 224 | %%process shell commands 225 | process_input_cmd(#state{shell = Shell}, "#con"++Value) -> 226 | Vl = get_int_value(Value), 227 | case Vl of 228 | {error, Error} -> io:format("~p~n",[Error]); 229 | _ -> 230 | gen_server:call(essh_scheduler, {opt, con, Vl}), 231 | gen_server:cast(essh, {opt, con, Vl}) 232 | end, 233 | Shell ! done; 234 | process_input_cmd(#state{shell = Shell}, "#intv"++Value) -> 235 | Vl = get_int_value(Value), 236 | case Vl of 237 | {error, Error} -> io:format("~p~n",[Error]); 238 | _ -> 239 | gen_server:call(essh_scheduler, {opt, intv, Vl}), 240 | gen_server:cast(essh, {opt, intv, Vl}) 241 | end, 242 | Shell ! done; 243 | process_input_cmd(#state{shell = Shell}, "options;") -> 244 | gen_server:cast(essh, {opt, show}), 245 | Shell ! done; 246 | process_input_cmd(#state{shell = Shell}, "exit") -> 247 | Shell ! done; 248 | process_input_cmd(#state{shell = Shell},"") -> 249 | Shell ! done; 250 | process_input_cmd(#state{shell = Shell},";") -> 251 | Shell ! done; 252 | process_input_cmd(#state{shell = Shell},"!") -> 253 | Shell ! done; 254 | process_input_cmd(_, "exit;") -> 255 | io:format("Thanks for using essh, bye.~n"), 256 | halt(); 257 | process_input_cmd(#state{pids = Pids}, Cmd) -> 258 | io:format("--------------~s---------------~n", [get(primary)]), 259 | call_ext(Pids, Cmd). 260 | %--------------------------------------------------------------------- 261 | 262 | get_processed_state(Server, Res, PrintMethod, DoneMessage, 263 | State = #state{shell = Shell, cur = CurServers, servers = Servers}) -> 264 | case process_cur_res(Server, CurServers, Res, PrintMethod) of 265 | [] -> 266 | clear_dict(), 267 | Shell ! DoneMessage, 268 | State#state{cur = Servers}; 269 | Rest -> 270 | State#state{cur = Rest} 271 | end. 272 | 273 | call_ext(Pids, Cmd) -> 274 | gen_server:call(essh_scheduler, {cmd, Pids, Cmd}). 275 | 276 | 277 | clear_dict() -> 278 | PrimServer = get(primary), 279 | erase(), 280 | put(primary, PrimServer). 281 | 282 | process_cur_res(Server, CurServers = [Cur | RestServers], Res, PrintMethod) -> 283 | if 284 | Server =/= Cur -> 285 | put(Server, {true, Res}), 286 | CurServers; 287 | true -> 288 | output_cmd_res(Cur, RestServers, Res, PrintMethod) 289 | end. 290 | 291 | output_cmd_res(Cur, Lst = [N | R], Res, PrintMethod) -> 292 | PrintMethod(Cur, Res), 293 | {NOk, NRes} = case V = get(N) of 294 | {true, _} -> V; 295 | _ -> {undefined, undefined} 296 | end, 297 | if 298 | NOk =:= true -> 299 | output_cmd_res(N, R, NRes, PrintMethod); 300 | true -> Lst 301 | end; 302 | output_cmd_res(Cur, [], Res, PrintMethod) -> 303 | PrintMethod(Cur, Res), 304 | io:format("~n"), 305 | []. 306 | 307 | do_print_cmd_res([]) -> 308 | ok; 309 | do_print_cmd_res([Line | Rest]) -> 310 | io:format("~s~n", [Line]), 311 | do_print_cmd_res(Rest). 312 | 313 | print_cmd_res(S, Res) -> 314 | IsP = is_primary_server(S), 315 | if 316 | IsP =:= true -> 317 | put(last, Res), 318 | io:format("~n[primary server done]~n"); 319 | true -> 320 | Last = get(last), 321 | if 322 | Res =:= Last -> 323 | put(identical, true), 324 | io:format("->>"); 325 | true -> 326 | put(last, Res), 327 | Identical = get(identical), 328 | if 329 | Identical =:= true -> 330 | io:format("~n"); 331 | true -> ok 332 | end, 333 | put(identical, false), 334 | io:format("--------------~s---------------~n", [S]), 335 | if 336 | is_list(Res) -> 337 | do_print_cmd_res(lists:reverse(Res)); 338 | true -> 339 | io:format("~p~n", [Res]) 340 | end 341 | end 342 | end. 343 | 344 | print_connect_info(Cur, Res) -> 345 | if 346 | Res =:= ok -> 347 | io:format("-----------------~s connected-------------------~n", [Cur]); 348 | true -> 349 | io:format("-----------------connect to ~s failed-------------------~n", [Cur]) 350 | end. 351 | 352 | %% -------------------------------------------------------------------- 353 | %% Function: handle_cast/2 354 | %% Description: Handling cast messages 355 | %% Returns: {noreply, State} | 356 | %% {noreply, State, Timeout} | 357 | %% {stop, Reason, State} (terminate/2 is called) 358 | %% -------------------------------------------------------------------- 359 | %%handler for processing options setting 360 | %% option : limits of concurrent running process 361 | handle_cast({opt, Opt, Value}, State = #state{opt = Options}) -> 362 | OptionsN = case Opt of 363 | con -> 364 | Options#options{con = Value}; 365 | intv -> 366 | Options#options{intv = Value} 367 | end, 368 | StateN = State#state{opt = OptionsN}, 369 | {noreply, StateN}; 370 | handle_cast({opt, show}, State = #state{opt = Options}) -> 371 | io:format("~ninfo:options = ~p~n", [Options]), 372 | {noreply, State}. 373 | 374 | %% -------------------------------------------------------------------- 375 | %% Function: handle_info/2 376 | %% Description: Handling all non call/cast messages 377 | %% Returns: {noreply, State} | 378 | %% {noreply, State, Timeout} | 379 | %% {stop, Reason, State} (terminate/2 is called) 380 | %% -------------------------------------------------------------------- 381 | handle_info(Info, State) -> 382 | io:format("essh received ~p~n", [Info]), 383 | {stop, "exit command received", State}. 384 | 385 | %% -------------------------------------------------------------------- 386 | %% Function: terminate/2 387 | %% Description: Shutdown the server 388 | %% Returns: any (ignored by gen_server) 389 | %% -------------------------------------------------------------------- 390 | terminate(Reason, State) -> 391 | ok. 392 | 393 | %% -------------------------------------------------------------------- 394 | %% Func: code_change/3 395 | %% Purpose: Convert process state when code is changed 396 | %% Returns: {ok, NewState} 397 | %% -------------------------------------------------------------------- 398 | code_change(OldVsn, State, Extra) -> 399 | {ok, State}. 400 | 401 | %% -------------------------------------------------------------------- 402 | %%% Internal functions 403 | %% -------------------------------------------------------------------- 404 | loop(SharedPath, SharedLib) -> 405 | receive 406 | {init, shell} -> ok 407 | end, 408 | %io:format("in loop ~p ~p~n", [SharedPath, SharedLib]), 409 | case erl_ddll:load_driver(SharedPath, SharedLib) of 410 | ok -> ok; 411 | {error, already_loaded} -> ok; 412 | {error, ErrorDesc} -> 413 | io:format("Error:~p~n", [erl_ddll:format_error(ErrorDesc)]), 414 | exit(error) 415 | end, 416 | %io:format("driver loaded~n"), 417 | Port = open_port({spawn, SharedLib}, []), 418 | loop_cmd(Port). 419 | 420 | loop_cmd(Port) -> 421 | Port!{self(), {command, "start"}}, 422 | receive 423 | {Port, {data, Data}} -> 424 | %io:format("received data ~p~n", [Data]), 425 | gen_server:call(essh, {shell_cmd, Data}), 426 | receive 427 | done -> 428 | ok 429 | end, 430 | loop_cmd(Port) 431 | end. 432 | 433 | non_interactive_loop(Cmd) -> 434 | receive 435 | {init, shell} -> ok 436 | end, 437 | gen_server:call(essh, {shell_cmd, Cmd}), 438 | receive 439 | done -> 440 | gen_server:call(essh, {shell_cmd, "exit;"}) 441 | end. 442 | 443 | --------------------------------------------------------------------------------