reshd

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

reshd

Tomas Abrahamsson-3

Here's a small program, that provides a telnet
interface to the erlang shell.  I called it reshd,
which is short for remote erlang shell daemon.

It is an alternative to run_erl/to_erl.  It opens a
tcp/ip port and listens for incoming connections.  Any
connection to this port drops right into an erlang
shell.

Example:

  First I make the erlang node execute reshd:start(5000).

  Then from a unix shell:
    prompt% telnet localhost 5000
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    Eshell V5.0.1
    (node)1> io:format("Hello!~n").
    Hello!
    ok
    (node)2> l(my_module).
    {module,my_module}
    (node)3> exit().
    ** Terminating shell **
    Connection closed by foreign host.
    prompt%

The following is attached:

  - reshd.erl -- the remote erlang shell daemon.
  - resh.el   -- emacs extensions to the erlang mode,
                 for connecting to a reshd. It offers
                 the keybinding C-c s, which sets
                 the resh-buffer as current
                 compilation-buffer.

No efforts have been made regarding security, but I
guess it would be easy to add ssl and password
authentication or whatever is needed.

/Tomas
-------------- next part --------------
%%%----------------------------------------------------------------------
%%% Purpose : Remote erlang shell daemon -- a telnet interface to the shell
%%% File    : reshd.erl
%%% Author  : Tomas Abrahamsson <tab>
%%% Created : 12 Apr 2001 by Tomas Abrahamsson <tab>
%%%
%%% COPYRIGHT
%%%
%%% These programs are released into the public domain.  You may do
%%% anything you like with them, including modifying them and selling
%%% the binaries without source for ridiculous amounts of money without
%%% saying who made them originally.
%%%
%%% However, I would be happy if you release your works with complete
%%% source for free use.
%%%----------------------------------------------------------------------
-module(reshd).
-author('tab').
-rcs('$Id: reshd.erl,v 1.5 2001/05/04 09:57:48 tab Exp $'). % '

%% API
-export([start/1, start/2]).
-export([stop/1, stop/2]).
-export([build_regname/1, build_regname/2]).

%% exports due to spawns
-export([server_init/3]).
-export([clienthandler_init/3]).

%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------
%% API
%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------

%% ----------------------------------------------------------------------
%% start(PortNumber) -> {ok, UsedPortNumber} | {error, Reason}
%% start(IP, PortNumber) -> {ok, UsedPortNumber} | {error, Reason}
%%   Portnumber = UsedPortNumber = integer(0..65535)
%%   IP = any | {Byte,Byte,Byte,Byte}
%%   Byte = integer(0..255)
%%
%% Start the reshd server to listen for connections on TCP/IP port PortNumber.
%%
%% The special port number 0 means "use any available TCP/IP port".
%% The port that is actually used is returned. If PortNumber != 0, then
%% UsedPortNumber == PortNumber.
%%
%% Optionally, an IP address to bind to can also be specified.
%% The default is that IP any, which means to bind to all ip addresses
%% on the machine.
%%
%% The process that listens for and handles incoming connections is
%% locally registred under the name reshd_<IP>_<UsedPortNumber>.
%% build_regname is used to build the name.
%% ----------------------------------------------------------------------

start(PortNumber) ->
    start(any, PortNumber).
start(IP, PortNumber) ->
    server_start(IP, PortNumber).

%% ----------------------------------------------------------------------
%% stop(PortNumber) -> void()
%% stop(IP, PortNumber) -> void()
%%   Portnumber = UsedPortNumber = integer(0..65535)
%%   IP = any | {Byte,Byte,Byte,Byte}
%%   Byte = integer(0..255)
%%
%% Stops the reshd server and any open connections associated to it.
%% ----------------------------------------------------------------------
stop(PortNumber) ->
    stop(any, PortNumber).
stop(IP, PortNumber) ->
    server_stop(IP, PortNumber).


%% ----------------------------------------------------------------------
%% build_regname(PortNumber) -> atom()
%% build_regname(IP, PortNumber) -> atom()
%%   Portnumber = UsedPortNumber = integer(0..65535)
%%   IP = any | {Byte,Byte,Byte,Byte}
%%   Byte = integer(0..255)
%%
%% Build a name under which the reshd server may be registered.
%% ----------------------------------------------------------------------
build_regname(PortNumber) ->
    build_regname(any, PortNumber).

build_regname(any, PortNumber) ->
    Name = atom_to_list(?MODULE) ++ "_any_" ++ integer_to_list(PortNumber),
    list_to_atom(Name);
build_regname({IP1, IP2, IP3, IP4}, PortNumber) ->
    Name = atom_to_list(?MODULE) ++ "_" ++
        list_to_integer(IP1) ++ "_" ++
        list_to_integer(IP2) ++ "_" ++
        list_to_integer(IP3) ++ "_" ++
        list_to_integer(IP4) ++ "_" ++
        "_" ++ integer_to_list(PortNumber),
    list_to_atom(Name);
build_regname(HostNameOrIP, PortNumber) ->
    Name = atom_to_list(?MODULE) ++
        "_" ++ HostNameOrIP ++ "_" ++
        integer_to_list(PortNumber),
    list_to_atom(Name).


%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------
%% Internal functions: the server part
%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------
server_start(IP, PortNumber) ->
    Server = spawn(?MODULE, server_init, [self(), IP, PortNumber]),
    receive
        {ok, UsedPortNumber} ->
            RegName = build_regname(IP, UsedPortNumber),
            register(RegName, Server),
            {ok, UsedPortNumber};
        {error, {Symptom, Diagnostics}} ->
            {error, {Symptom, Diagnostics}}
    end.

server_stop(IP, PortNumber) ->
    RegName = build_regname(IP, PortNumber),
    case whereis(RegName) of
        undefined ->
            do_nothing;
        Pid ->
            Pid ! stop
    end.

server_init(From, IP, PortNumber) ->
    IPOpt = ip_to_opt(IP),
    ListenOpts = [list,
                  {packet, 0},
                  {active, true}, % this is the default
                  {nodelay, true},
                  {reuseaddr, true}] ++ IPOpt,
    case gen_tcp:listen(PortNumber, ListenOpts) of
        {ok, ServerSocket} ->
            {ok, UsedPortNumber} = inet:port(ServerSocket),
            From ! {ok, UsedPortNumber},
            process_flag(trap_exit, true),
            server_loop(From, ServerSocket);
        {error, Reason} ->
            From ! {error, {listen_failed, Reason}}
    end.


ip_to_opt(any) ->
    [];
ip_to_opt({IP1, IP2, IP3, IP4}=IPNumber) ->
    [{ip, IPNumber}];
ip_to_opt(HostNameOrIPAsString) ->
    case inet:getaddr(HostNameOrIPAsString, inet) of
        {ok, IPNumber} ->
            [{ip, IPNumber}];
        {error, Error} ->
            loginfo("~p: IP lookup failed for ~p: ~p. Binding to any ip.",
                    [?MODULE, HostNameOrIPAsString, Error]),
            []
    end.


server_loop(From, ServerSocket) ->
    server_loop(From, ServerSocket, []).

server_loop(From, ServerSocket, Clients) ->
    case gen_tcp:accept(ServerSocket, 250) of
        {ok, ClientSocket} ->
            ClientHandler = clienthandler_start(From, self(), ClientSocket),
            gen_tcp:controlling_process(ClientSocket, ClientHandler),
            server_loop(From, ServerSocket, [ClientHandler | Clients]);
        {error, timeout} ->
            %% Check for signals now and then
            receive
                stop ->
                    lists:foreach(fun(Client) -> Client ! stop end, Clients),
                    done;
                {client_stop, Client} ->
                    RemainingClients = [C || C <- Clients, C /= Client],
                    server_loop(From, ServerSocket, RemainingClients);
                {'EXIT', Client, Reason} ->
                    RemainingClients = [C || C <- Clients, C /= Client],
                    server_loop(From, ServerSocket, RemainingClients);
                Unexpected ->
                    loginfo("~p:server_loop: unexpected message:~p",
                            [?MODULE, Unexpected]),
                    server_loop(From, ServerSocket, Clients)
            after 0 ->
                    server_loop(From, ServerSocket, Clients)
            end;
        {error, Reason} ->
            logerror("~p:server_loop: Error: accepting on ~p: ~p.",
                     [?MODULE, ServerSocket, Reason])
    end.


%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------
%% The client handler part -- handles a user of the reshd.
%% ----------------------------------------------------------------------
%% ----------------------------------------------------------------------
clienthandler_start(From, Server, ClientSocket) ->
    spawn_link(?MODULE, clienthandler_init, [From, Server, ClientSocket]).

-record(io_request,
        {
          prompt,
          mod, fn, args,
          from, reply_as
         }).
         

clienthandler_init(From, Server, ClientSocket) ->
    %% Announce ourself as group leader.
    %% This causes all calls to io:format(...) and such alike
    %% to send their output to us.
    group_leader(self(), self()),

    %% Next, start the shell
    %% and link to it, so we know when it exits.
    process_flag(trap_exit, true),
    Reshd = shell:start(true),
    link(Reshd),

    %% Go ahead and take care of user input!
    R = (catch clienthandler_loop(idle, Reshd, Server, ClientSocket)),
    exit(Reshd, kill).

clienthandler_loop(State, Reshd, Server, ClientSocket) ->
    receive
        {tcp, _Socket, Input} ->
            NativeInput = nl_network_to_native(Input),
            case handle_input(ClientSocket, State, NativeInput) of
                {ok, NewState} ->
                    clienthandler_loop(NewState, Reshd, Server, ClientSocket);
                close ->
                    gen_tcp:close(ClientSocket)
            end;

        {tcp_closed, Socket} ->
            Server ! {client_stop, self()},
            done;

        {tcp_error, Socket, Reason} ->
            Server ! {client_stop, self()},
            done;

        stop ->
            gen_tcp:close(ClientSocket),
            done;

        {io_request, From, ReplyAs, Req} ->
            case handle_io_request(ClientSocket, State, From, ReplyAs, Req) of
                {ok, NewState} ->
                    clienthandler_loop(NewState, Reshd, Server, ClientSocket);
                close ->
                    gen_tcp:close(ClientSocket)
            end;

        {'EXIT', Reshd, normal} ->
            gen_tcp:close(ClientSocket);

        {'EXIT', Reshd, _OtherReason} ->
            gen_tcp:close(ClientSocket);

        Other ->
            clienthandler_loop(State, Reshd, Server, ClientSocket)
    end.


%% Returns:
%%   {ok, NewState} |
%%   close
handle_input(ClientSocket, State, Input) ->
    case State of
        idle ->
            {ok, {pending_input, Input}};
        {pending_input, PendingInput} ->
            NewInput = PendingInput ++ Input,
            {ok, {pending_input, NewInput}};
        {pending_request, Cont, [FirstReq | RestReqs] = Requests} ->
            #io_request{prompt = Prompt,
                        mod = Mod,
                        fn = Fun,
                        args = Args} = FirstReq,
            case catch apply(Mod, Fun, [Cont, Input|Args]) of
                {more, NewCont} ->
                    print_prompt(ClientSocket, Prompt),
                    {ok, {pending_request, NewCont, Requests}};
                {done, Result, []} ->
                    #io_request{from = From,
                                reply_as = ReplyAs} = FirstReq,
                    From ! {io_reply, ReplyAs, Result},
                    case length(RestReqs) of
                        0 ->
                            {ok, idle};
                        N ->
                            [#io_request{prompt = NextPrompt}|_] = RestReqs,
                            print_prompt(ClientSocket, NextPrompt),
                            InitCont = init_cont(),
                            {ok, {pending_request, InitCont, RestReqs}}
                    end;
                {done, Result, RestChars} ->
                    #io_request{from = From,
                                reply_as = ReplyAs} = FirstReq,
                    From ! {io_reply, ReplyAs, Result},
                    case length(RestReqs) of
                        0 ->
                            {ok, {pending_input, RestChars}};
                        N ->
                            InitCont = init_cont(),
                            TmpState = {pending_request, InitCont, RestReqs},
                            handle_input(ClientSocket, RestChars, TmpState)
                    end;
                Other ->
                    logerror("~p:handle_input: Unexpected result: ~p~n",
                             [?MODULE, Other]),
                    close
            end
    end.


%% Returns:
%%   {ok, NewState} |
%%   close
handle_io_request(ClientSocket, State, From, ReplyAs, IoRequest) ->
    case IoRequest of
        {put_chars, Mod, Fun, Args} ->
            Text = case catch apply(Mod, Fun, Args) of
                      {'EXIT', Reason} -> "";
                      Txt -> Txt
                   end,
            NWText = nl_native_to_network(lists:flatten(Text)),
            gen_tcp:send(ClientSocket, NWText),
            From ! {io_reply, ReplyAs, ok},
            {ok, State};

        {put_chars, Text} ->
            NWText = nl_native_to_network(lists:flatten(Text)),
            gen_tcp:send(ClientSocket, Text),
            From ! {io_reply, ReplyAs, ok},
            {ok, State};

        {get_until, Prompt, Mod, Fun, Args} ->
            NewReq = #io_request{prompt = Prompt,
                                 mod = Mod,
                                 fn = Fun,
                                 args = Args,
                                 from = From,
                                 reply_as = ReplyAs},
            case State of
                {pending_request, Cont, PendingReqs} ->
                    NewState = {pending_request, Cont, PendingReqs++[NewReq]},
                    {ok, NewState};

                idle ->
                    print_prompt(ClientSocket, Prompt),
                    InitContinuation = init_cont(),
                    NewState = {pending_request, InitContinuation, [NewReq]},
                    {ok, NewState};

                {pending_input, Input} ->
                    InitContinuation = init_cont(),
                    TmpState = {pending_request, InitContinuation, [NewReq]},
                    handle_input(ClientSocket, TmpState, Input)
            end;

        UnexpectedIORequest ->
            loginfo("~p:handle_io_request: Unexpected IORequest:~p~n",
                    [?MODULE, UnexpectedIORequest]),
            From ! {io_reply, ReplyAs, ok},
            {ok, State}
    end.
   

init_cont() ->
    [].

print_prompt(ClientSocket, Prompt) ->
    PromptText = case Prompt of
                     {IoFun, PromptFmtStr, PromptArgs} ->
                         io_lib:IoFun(PromptFmtStr, PromptArgs);
                     {IoFun, PromptFmtStr} ->
                         io_lib:IoFun(PromptFmtStr, [])
                 end,
    NWPromptText = nl_native_to_network(lists:flatten(PromptText)),
    gen_tcp:send(ClientSocket, NWPromptText).

%% Convert network newline (cr,lf) to native (\n)
nl_network_to_native(Input) ->
    nl_network_to_native(Input, "").


nl_network_to_native("\r\n" ++ Rest, Acc) ->
    nl_network_to_native(Rest, [$\n | Acc]);
nl_network_to_native([C | Rest], Acc) ->
    nl_network_to_native(Rest, [C | Acc]);
nl_network_to_native("", Acc) ->
    lists:reverse(Acc).

                                   

%% Convert native newline \n to network (cr,lf)
nl_native_to_network(Input) ->
    nl_native_to_network(Input, "").


nl_native_to_network("\n" ++ Rest, Acc) ->
    %% Here we put \r\n in reversed order.
    %% It'll be put in correct order by the lists:reverse() call
    %% in the last clause.
    nl_native_to_network(Rest, [$\n, $\r | Acc]);
nl_native_to_network([C | Rest], Acc) ->
    nl_native_to_network(Rest, [C | Acc]);
nl_native_to_network("", Acc) ->
    lists:reverse(Acc).


loginfo(FmtStr, Args) ->
    %% FIXME: Invent a way to log errors.
    %% Can't use the error_log module since someone may
    %% add a log handler that does io:format. Then there
    %% will be a deadlock, I think, if this is function
    %% is called from within code that handles the client.
    fixme.
logerror(FmtStr, Args) ->
    %% See loginfo/2.
    fixme.
-------------- next part --------------
;;; resh.el -- emacs support for connecting to a reshd
;;;            (remote erlang shell daemon)
;;
;; $Id: resh.el,v 1.5 2001/05/04 09:57:34 tab Exp $
;;
;; Author: Tomas Abrahamsson <tab>

;;; COPYRIGHT

;; These programs are released into the public domain.  You may do
;; anything you like with them, including modifying them and selling
;; the binaries without source for ridiculous amounts of money without
;; saying who made them originally.
;;
;; However, I would be happy if you release your works with complete
;; source for free use.

;;; Installation:

;; Either:
;;
;;   (autoload 'resh "resh" "Start a connection to an erlang reshd" t)
;;
;; or:
;;
;;   (load-library "resh")
;;   (resh-install) ; installs keymaps
;;
;; The difference is that in the second case, the resh is bound to
;; C-c c immediately, while in the first case no key bindings are
;; installed until you have typed M-x resh for the first time.

;;; Code

(require 'erlang)

;; User definable variables
;; vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
(defvar resh-default-host "localhost"
  "*Default hostname for `resh'.")

(defvar resh-default-port nil
  "*Default port (an integer) for `resh'.")
;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
;; End of user definable variables


(defvar resh-host-history nil
  "Host history for `resh'")

(defvar resh-port-history nil
  "Port history for `resh'")

(defvar resh-buff-history nil
  "Buffer name history for `resh'")

(defvar resh-current-host nil
  "Buffer-local variable, used for reconnection.")

(defvar resh-current-port nil
  "Buffer-local variable, used for reconnection.")

(defvar resh-is-installed nil
  "Whether resh is installed or not")

(defvar resh-auto-install-enabled t
  "Whether resh should autoinstall upon call to resh")


;;;###autoload
(defun resh (host port &optional reconnecting wanted-buffer-name)
  "Run an inferior remote Erlang shell.

The command line history can be accessed with  M-p  and  M-n.
The history is saved between sessions.

Entry to this mode calls the functions in the variables
`comint-mode-hook' and `erlang-shell-mode-hook' with no arguments.

The following commands imitate the usual Unix interrupt and
editing control characters:
\\{erlang-shell-mode-map}"
  (interactive
   ;; Handling of interactive calling
   (let* ((init-prompt "Remote erlang shell to")
          (host-hist 'resh-host-history)
          (port-hist 'resh-port-history)
          (buff-hist 'resh-buff-history)
          (host-prompt (concat init-prompt ": "))
          (remote-host (read-string host-prompt resh-default-host host-hist))
          (port-prompt (concat init-prompt " " remote-host " port: "))
          (default-port (cond ((null resh-default-port) nil)
                              ((stringp resh-default-port) resh-default-port)
                              ((numberp resh-default-port)
                               (int-to-string resh-default-port))
                              (t nil)))
          (remote-port-str (read-string port-prompt default-port port-hist))
          (remote-port (cond ((string= "" remote-port-str)
                              (error "Not port number \"%s\"" remote-port-str))
                             (t (string-to-int remote-port-str))))
          (buffer-prompt (concat init-prompt " "
                                 remote-host ":" remote-port-str
                                 ", buffer name: "))
          (buffer-name (if current-prefix-arg
                           (read-string buffer-prompt nil buff-hist)
                         nil)))
     (list remote-host remote-port nil buffer-name)))

  (if (and (not resh-is-installed) resh-auto-install-enabled)
      (resh-install))

  (require 'comint)

  (let* ((proc-name (resh-buffer-name inferior-erlang-process-name host port))
         (erl-buffer (make-comint proc-name (cons host port)))
         (erl-process (get-buffer-process erl-buffer))
         (erl-buffer-name (if wanted-buffer-name
                              wanted-buffer-name
                            (resh-buffer-name inferior-erlang-buffer-name
                                              host port))))

    ;; Say no query needed if erl-process is running when Emacs is exited.
    (process-kill-without-query erl-process)

    ;; Switch to buffer in other or this window
    ;; the `erlang-inferior-shell-split-window' is a local extension
    ;; to the erlang mode.
    (if (and (boundp 'erlang-inferior-shell-split-window)
             erlang-inferior-shell-split-window)
        (switch-to-buffer-other-window erl-buffer)
      (switch-to-buffer erl-buffer))

    ;; comint settings
    (if (and (not (eq system-type 'windows-nt))
             (eq inferior-erlang-shell-type 'newshell))
        (setq comint-process-echoes nil))

    ;; Set buffer name and run erlang-shell-mode unless we are reconnecting
    (if reconnecting
        nil
      (condition-case nil
          ;; `rename-buffer' takes only one argument in Emacs 18.
          (rename-buffer erl-buffer-name t)
        (error (rename-buffer erl-buffer-name)))
      ;; remember the host/port so we can reconnect.
      (make-variable-buffer-local 'resh-current-host)
      (make-variable-buffer-local 'resh-current-port)
      (setq resh-current-host host)
      (setq resh-current-port port)
      (erlang-shell-mode))))

(defun resh-buffer-name (base host port)
  (let* ((host-port (concat host ":" (int-to-string port))))
    (if (string= (substring base -1) "*")
        (concat (substring base 0 -1) "-" host-port "*")
      (concat base "-" host-port))))

(defun resh-reconnect ()
  "Try to reconnect to a remote Erlang shell daemon."
  (interactive)
  (resh resh-current-host resh-current-port t))

(defun resh-set-inferior-erlang-buffer ()
  "Set current buffer to the inferior erlang buffer."
  (interactive)
  (setq inferior-erlang-buffer (current-buffer))
  (message "This buffer is now set to the current inferior erlang buffer"))


;;;###autoload
(defun resh-install ()
  (interactive)
  (if (not resh-is-installed)
      (progn
        (if (not (member 'resh-install-erl-keys erlang-mode-hook))
            (add-hook 'erlang-mode-hook 'resh-install-erl-keys))
        (if (not (member 'resh-install-erl-shell-keys erlang-shell-mode-hook))
            (add-hook 'erlang-shell-mode-hook 'resh-install-erl-shell-keys))
        (setq resh-is-installed t))))

(defun resh-install-erl-keys ()
  (local-set-key "\C-cc" 'resh-erlang))

(defun resh-install-erl-shell-keys ()
  (local-set-key "\C-cc" 'resh-erlang)
  (local-set-key "\C-cs" 'resh-set-inferior-erlang-buffer)
  ;; The reconnection is not fully working...
  ;;(local-set-key "\C-cr" 'resh-reconnect)
  )


(provide 'resh)