## Copyright (C) 2020-2023 Olaf Till <i7tiol@t-online.de>
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 3 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn{Function File} {} pserver ()
## @deftypefnx{Function File} {} pserver (@var{options})
## @deftypefnx{Function File} {} pserver ("kill")
## Starts or stops a server of the parallel cluster.
##
## @var{options}: structure of options; field @code{use_tls} is
## @code{true} by default (TLS with SRP authentication); if set to
## @code{false}, there will be no encryption or authentication. Field
## @code{auth_file} can be set to an alternative path to the file with
## authentication information (see below). Fields @code{cmd_port}
## (default: 12502) and @code{data_port} (default: 12501) can be set
## to change the ports of the command channel and the data channel,
## respectively.
##
## If called as @code{pserver ("kill")}, the server will be stopped by
## sending it a signal, taking its process id from its pid-file
## '/tmp/.octave-<hostname>.pid'. Otherwise the server will be
## started.
##
## The servers exectuable file (@code{octave-pserver}) is searched for
## by first assuming the directory structure of a regular package
## installation, then by searching Octaves function search path for
## it, and then by the called shell in its shell search path.
##
## If a directory path corresponding to the current directory of the
## client exists on the server machine, it will be used as the servers
## current directory for the respective client (multiple clients are
## possible). Otherwise, @code{/tmp} will be used. The Octave
## functions the server is supposed to call and the files it possibly
## has to access must be available at the server machine. This can
## e.g. be achieved by having the server machine mount a network file
## system (which is outside the scope of this package documentation).
##
## If a connection is accepted from a client, the server collects a
## network identifier and the names of all server machines of the
## network from the client. Then, connections are automatically
## established between all machines of the network. Data exchange will
## be possible between all machines (client or server) in both
## directions. Commands can only be sent from the client to any
## server.
##
## The opaque variable holding the network connections, in the same
## order as in the corresponding variable returned by @code{pconnect},
## is accessible under the variable name @code{sockets} at the server
## side. Do not overwrite or clear this variable. The own server
## machine will also be contained at some index position of this
## variable, but will not correspond to a real connection. See
## @code{pconnect} for further information.
##
## The client and the server must both use or both not use TLS. If TLS
## is switched off, different measures must be taken to protect ports
## of the command and data channels at the servers and the client
## against unauthorized access, e.g. by a firewall or by physical
## isolation of the network.
##
## For using TLS, authorization data must be present at the server
## machine. These data can conveniently be generated by
## @code{parallel_generate_srp_data}; the helptext of the latter
## function documents the expected location of these data.
##
## The SRP password will be sent over the encrypted TLS channel from
## the client to each server, to avoid permanently storing passwords
## at the server for server-to-server data connections. Due to
## inevitable usage of external libraries, memory with sensitive data
## can, however, be on the swap device even after shutdown of the
## application.
##
## The server executable file @code{octave-pserver} is installed and
## runs at GNU/Linux, but not at some other operating systems like
## Windows and macOS.
##
## @seealso{pconnect, reval, psend, precv, sclose, parallel_generate_srp_data, select_sockets}
## @end deftypefn

function pserver (arg)

  if ((nargs = nargin ()) > 1
      || (nargs == 1 && ! strcmp (arg, "kill")))
    print_usage ();
  endif

  if (nargs == 1 && strcmp (arg, "kill"))

    hostname = strtrim (call_system ("hostname", ""));

    pid  = str2num (call_system
                      ("cat", sprintf ("/tmp/.octave-%s.pid", hostname)));

    if (kill (pid, 15))

      error ("could not kill server");

    endif

  else

    octave_binary = fullfile (OCTAVE_HOME, "/bin/octave-cli");

    tp_octave_binary = cstrcat (octave_binary, "-", OCTAVE_VERSION ());

    if (! isempty (stat (tp_octave_binary)))

      octave_binary = tp_octave_binary;

    endif

    optstr = sprintf ("--octave-binary='%s'", octave_binary);

    if (nargs == 1)

      if (isfield (arg, "use_tls") && ! arg.use_tls)

        optstr = sprintf ("%s --no-auth", optstr);

      endif

      if (isfield (arg, "auth_file"))

        optstr = sprintf ("%s --auth-file='%s'", optstr, arg.auth_file);

      endif

      if (isfield (arg, "cmd_port"))

        optstr = sprintf ("%s --cmd-port=%i", optstr, arg.cmd_port);

      endif

      if (isfield (arg, "data_port"))

        optstr = sprintf ("%s --data-port=%i", optstr, arg.data_port);

      endif

    endif

    server_binary =  get_server_binary ();

    call_system (server_binary, optstr);

  endif

endfunction

function fpath = get_server_binary ()

  fname = "octave-pserver";

  if (__isfile__ (tp = fullfile (fileparts (mfilename ("fullpath")),
                                 "bin/",
                                 fname)))

    ## regular installation directories

    fpath = tp;

  elseif (__isfile__ (tp = fullfile ("/usr/libexec", 
                                     fname)))

    ## Debian specific: installed in /usr/libexec

    fpath = tp;

  elseif (! isempty (tp = search_path (fname)))

    ## in Octaves search path

    fpath = tp

  else

    ## fallback, rely on system search path

    fpath = fname;

  endif

endfunction

function output = call_system (program, argstring)

  [status, output] = system ([program, " ", argstring]);

  if (status == 127)

    error ("unable to find program '%s'", program);

  elseif (status)

    error ("program %s returned error code %i", program, status);

  endif

endfunction

function full_path = search_path (file)

  full_path = "";

  for dir = strsplit (path (), ":");

    if (__isfile__ (tp = fullfile (dir{1}, file)))

      full_path = tp;

      break;

    endif

  endfor

endfunction

%!xtest
%! pserver ();
%! conns = pconnect ({"localhost"});
%! assert (res = netcellfun (conns, @ (x, y) x * y, {1, 2, 3, 4}, {2, 3, 4, 5}), [2, 6, 12, 20]);
%! pserver ("kill");
