// Copyright (C) 2002 Graydon Hoare <graydon@pobox.com>
//               2006 Timothy Brownawell <tbrownaw@gmail.com>
//
// This program is made available under the GNU GPL version 2.0 or
// greater. See the accompanying file COPYING for details.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE.

#include "base.hh"
#include "cmd.hh"

#include "automate_ostream_demuxed.hh"
#include "merge_content.hh"
#include "netcmd.hh"
#include "network/connection_info.hh"
#include "file_io.hh"
#include "globish.hh"
#include "keys.hh"
#include "key_store.hh"
#include "cert.hh"
#include "revision.hh"
#include "uri.hh"
#include "vocab_cast.hh"
#include "platform-wrapped.hh"
#include "app_state.hh"
#include "maybe_workspace_updater.hh"
#include "project.hh"
#include "work.hh"
#include "database.hh"
#include "roster.hh"
#include "vocab_cast.hh"
#include "ui.hh"

#include <fstream>

using std::ifstream;
using std::ofstream;
using std::map;
using std::set;
using std::string;
using std::vector;

using boost::shared_ptr;

static const var_key default_server_key(var_domain("database"),
                                        var_name("default-server"));
static const var_key default_include_pattern_key(var_domain("database"),
                                                 var_name("default-include-pattern"));
static const var_key default_exclude_pattern_key(var_domain("database"),
                                                 var_name("default-exclude-pattern"));

static void
find_key(options & opts,
         database & db,
         key_store & keys,
         lua_hooks & lua,
         project_t & project,
         netsync_connection_info const & info,
         bool need_key = true)
{
  utf8 host(info.client.unparsed);
  if (!info.client.uri.host.empty())
    host = utf8(info.client.uri.host, origin::user);

  cache_netsync_key(opts, db, keys, lua, project, host,
                    info.client.include_pattern,
                    info.client.exclude_pattern,
                    need_key ? KEY_REQUIRED : KEY_OPTIONAL);
}

static void
build_client_connection_info(options & opts,
                             lua_hooks & lua,
                             database & db,
                             key_store & keys,
                             project_t & project,
                             netsync_connection_info & info,
                             bool address_given,
                             bool include_or_exclude_given,
                             bool need_key = true)
{
  // Use the default values if needed and available.
  if (!address_given)
    {
      E(db.var_exists(default_server_key), origin::user,
        F("no server given and no default server set"));
      var_value addr_value;
      db.get_var(default_server_key, addr_value);
      info.client.unparsed = typecast_vocab<utf8>(addr_value);
      L(FL("using default server address: %s") % info.client.unparsed);
    }
  parse_uri(info.client.unparsed(), info.client.uri, origin::user);

  var_key server_include(var_domain("server-include"),
                         typecast_vocab<var_name>(info.client.unparsed));
  var_key server_exclude(var_domain("server-exclude"),
                         typecast_vocab<var_name>(info.client.unparsed));

  if (info.client.uri.query.empty() && !include_or_exclude_given)
    {
      if (db.var_exists(server_include))
        {
          var_value pattern_value;
          db.get_var(server_include, pattern_value);
          info.client.include_pattern = globish(pattern_value(), origin::user);
          L(FL("using default branch include pattern: '%s'")
            % info.client.include_pattern);
          if (db.var_exists(server_exclude))
            {
              db.get_var(server_exclude, pattern_value);
              info.client.exclude_pattern = globish(pattern_value(), origin::user);
            }
          else
            info.client.exclude_pattern = globish();
          L(FL("excluding: %s") % info.client.exclude_pattern);
        }
      else
        {
          // No include/exclude given anywhere, use the defaults.
          E(db.var_exists(default_include_pattern_key), origin::user,
            F("no branch pattern given and no default pattern set"));
          var_value pattern_value;
          db.get_var(default_include_pattern_key, pattern_value);
          info.client.include_pattern = globish(pattern_value(), origin::user);
          L(FL("using default branch include pattern: '%s'")
            % info.client.include_pattern);
          if (db.var_exists(default_exclude_pattern_key))
            {
              db.get_var(default_exclude_pattern_key, pattern_value);
              info.client.exclude_pattern = globish(pattern_value(), origin::user);
            }
          else
            info.client.exclude_pattern = globish();
          L(FL("excluding: %s") % info.client.exclude_pattern);
        }
    }
  else if(!info.client.uri.query.empty())
    {
      E(!include_or_exclude_given, origin::user,
        F("Include/exclude pattern was given both as part of the URL and as a separate argument."));

      // Pull include/exclude from the query string
      char const separator = '/';
      char const negate = '-';
      string const & query(info.client.uri.query);
      std::vector<arg_type> includes, excludes;
      string::size_type begin = 0;
      string::size_type end = query.find(separator);
      while (begin < query.size())
        {
          std::string item = query.substr(begin, end);
          if (end == string::npos)
            begin = end;
          else
            {
              begin = end+1;
              if (begin < query.size())
                end = query.find(separator, begin);
            }

          bool is_exclude = false;
          if (item.size() >= 1 && item.at(0) == negate)
            {
              is_exclude = true;
              item.erase(0, 1);
            }
          else if (item.find("include=") == 0)
            {
              item.erase(0, string("include=").size());
            }
          else if (item.find("exclude=") == 0)
            {
              is_exclude = true;
              item.erase(0, string("exclude=").size());
            }

          if (is_exclude)
            excludes.push_back(arg_type(urldecode(item, origin::user),
                                        origin::user));
          else
            includes.push_back(arg_type(urldecode(item, origin::user),
                                        origin::user));
        }
      info.client.include_pattern = globish(includes);
      info.client.exclude_pattern = globish(excludes);
    }

  // Maybe set the default values.
  if (!db.var_exists(default_server_key)
      || opts.set_default)
    {
      P(F("setting default server to %s") % info.client.unparsed());
      db.set_var(default_server_key,
                 typecast_vocab<var_value>(info.client.unparsed));
    }
  if (!db.var_exists(default_include_pattern_key)
      || opts.set_default)
    {
      P(F("setting default branch include pattern to '%s'")
        % info.client.include_pattern);
      db.set_var(default_include_pattern_key,
                 typecast_vocab<var_value>(info.client.include_pattern));
    }
  if (!db.var_exists(default_exclude_pattern_key)
      || opts.set_default)
    {
      P(F("setting default branch exclude pattern to '%s'")
        % info.client.exclude_pattern);
      db.set_var(default_exclude_pattern_key,
                 typecast_vocab<var_value>(info.client.exclude_pattern));
    }
  if (!db.var_exists(server_include)
      || opts.set_default)
    {
      P(F("setting default include pattern for server '%s' to '%s'")
        % info.client.unparsed % info.client.include_pattern);
      db.set_var(server_include,
                 typecast_vocab<var_value>(info.client.include_pattern));
    }
  if (!db.var_exists(server_exclude)
      || opts.set_default)
    {
      P(F("setting default exclude pattern for server '%s' to '%s'")
        % info.client.unparsed % info.client.exclude_pattern);
      db.set_var(server_exclude,
                 typecast_vocab<var_value>(info.client.exclude_pattern));
    }

  info.client.use_argv =
    lua.hook_get_netsync_connect_command(info.client.uri,
                                         info.client.include_pattern,
                                         info.client.exclude_pattern,
                                         global_sanity.debug_p(),
                                         info.client.argv);
  opts.use_transport_auth = lua.hook_use_transport_auth(info.client.uri);
  if (opts.use_transport_auth)
    {
      find_key(opts, db, keys, lua, project, info, need_key);
    }

  info.client.connection_type = netsync_connection_info::netsync_connection;
}

static void
extract_client_connection_info(options & opts,
                               lua_hooks & lua,
                               database & db,
                               key_store & keys,
                               project_t & project,
                               args_vector const & args,
                               netsync_connection_info & info,
                               bool need_key = true)
{
  bool have_address = false;
  bool have_include_exclude = false;
  if (args.size() >= 1)
    {
      have_address = true;
      info.client.unparsed = idx(args, 0);
    }
  if (args.size() >= 2 || opts.exclude_given)
    {
      E(args.size() >= 2, origin::user, F("no branch pattern given"));

      have_include_exclude = true;
      info.client.include_pattern = globish(args.begin() + 1, args.end());
      info.client.exclude_pattern = globish(opts.exclude_patterns);
    }
  build_client_connection_info(opts, lua, db, keys, project,
                               info, have_address, have_include_exclude,
                               need_key);
}

CMD_AUTOMATE_NO_STDIO(remote_stdio,
                      N_("[ADDRESS[:PORTNUMBER]]"),
                      N_("Opens an 'automate stdio' connection to a remote server"),
                      "",
                      options::opts::max_netsync_version |
                      options::opts::min_netsync_version |
                      options::opts::set_default)
{
  if (args.size() > 1)
    throw usage(execid);

  app.opts.non_interactive = true;

  if (app.opts.dbname.empty())
    {
      W(F("No database given; assuming '%s' database. This means that we can't\n"
          "verify the server key, because we have no record of what it should be.")
          % memory_db_identifier);
      app.opts.dbname_type = memory_db;
    }

  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;

  if (args.size() == 1)
    {
      info.client.unparsed = idx(args, 0);
    }
  else
    {
      E(db.var_exists(default_server_key), origin::user,
        F("no server given and no default server set"));
      var_value addr_value;
      db.get_var(default_server_key, addr_value);
      info.client.unparsed = typecast_vocab<utf8>(addr_value);
      L(FL("using default server address: %s") % info.client.unparsed);
    }

  parse_uri(info.client.unparsed(), info.client.uri, origin::user);

  if (!db.var_exists(default_server_key) || app.opts.set_default)
    {
      P(F("setting default server to %s") % info.client.unparsed());
      db.set_var(default_server_key,
                 typecast_vocab<var_value>(info.client.unparsed));
    }

  info.client.use_argv =
    app.lua.hook_get_netsync_connect_command(info.client.uri,
                                             info.client.include_pattern,
                                             info.client.exclude_pattern,
                                             global_sanity.debug_p(),
                                             info.client.argv);
  app.opts.use_transport_auth = app.lua.hook_use_transport_auth(info.client.uri);
  if (app.opts.use_transport_auth)
    {
      find_key(app.opts, db, keys, app.lua, project, info, true);
    }

  info.client.connection_type = netsync_connection_info::automate_connection;

  info.client.set_input_stream(std::cin);
  automate_ostream os(output, app.opts.automate_stdio_size);
  info.client.set_output_stream(os);

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_and_sink_role, info);
}

// shamelessly copied and adapted from option.cc
static void
parse_options_from_args(args_vector & args,
                        std::vector<std::pair<std::string, arg_type> > & opts)
{
  bool seen_dashdash = false;
  for (args_vector::size_type i = 0; i < args.size(); )
    {
      string name;
      arg_type arg;

      if (idx(args,i)() == "--" || seen_dashdash)
        {
          if (!seen_dashdash)
            {
              seen_dashdash = true;
            }
          ++i;
          continue;
        }
      else if (idx(args,i)().substr(0,2) == "--")
        {
          string::size_type equals = idx(args,i)().find('=');
          bool has_arg;
          if (equals == string::npos)
            {
              name = idx(args,i)().substr(2);
              has_arg = false;
            }
          else
            {
              name = idx(args,i)().substr(2, equals-2);
              has_arg = true;
            }

          if (has_arg)
            {
              arg = arg_type(idx(args,i)().substr(equals+1), origin::user);
            }
        }
      else if (idx(args,i)().substr(0,1) == "-")
        {
          name = idx(args,i)().substr(1,1);
          bool has_arg = idx(args,i)().size() > 2;

          if (has_arg)
            {
              arg = arg_type(idx(args,i)().substr(2), origin::user);
            }
        }
      else
        {
          ++i;
          continue;
        }

      opts.push_back(std::pair<std::string, arg_type>(name, arg));
      args.erase(args.begin() + i);
    }
}

CMD_AUTOMATE_NO_STDIO(remote,
                      N_("COMMAND [ARGS]"),
                      N_("Executes COMMAND on a remote server"),
                      "",
                      options::opts::remote_stdio_host |
                      options::opts::max_netsync_version |
                      options::opts::min_netsync_version |
                      options::opts::set_default)
{
  E(args.size() >= 1, origin::user,
    F("wrong argument count"));

  if (app.opts.dbname.empty())
    {
      W(F("No database given; assuming '%s' database. This means that we can't\n"
          "verify the server key, because we have no record of what it should be.")
          % memory_db_identifier);
      app.opts.dbname_type = memory_db;
    }

  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;

  if (app.opts.remote_stdio_host_given)
    {
      info.client.unparsed = app.opts.remote_stdio_host;
    }
  else
    {
      E(db.var_exists(default_server_key), origin::user,
        F("no server given and no default server set"));
      var_value addr_value;
      db.get_var(default_server_key, addr_value);
      info.client.unparsed = typecast_vocab<utf8>(addr_value);
      L(FL("using default server address: %s") % info.client.unparsed);
    }

  parse_uri(info.client.unparsed(), info.client.uri, origin::user);

  if (!db.var_exists(default_server_key) || app.opts.set_default)
    {
      P(F("setting default server to %s") % info.client.unparsed());
      db.set_var(default_server_key,
                 typecast_vocab<var_value>(info.client.unparsed));
    }

  info.client.use_argv =
    app.lua.hook_get_netsync_connect_command(info.client.uri,
                                             info.client.include_pattern,
                                             info.client.exclude_pattern,
                                             global_sanity.debug_p(),
                                             info.client.argv);
  app.opts.use_transport_auth = app.lua.hook_use_transport_auth(info.client.uri);
  if (app.opts.use_transport_auth)
    {
      find_key(app.opts, db, keys, app.lua, project, info, true);
    }

  args_vector cleaned_args(args);
  std::vector<std::pair<std::string, arg_type> > opts;
  parse_options_from_args(cleaned_args, opts);

  std::stringstream ss;
  if (opts.size() > 0)
    {
      ss << 'o';
      for (unsigned int i=0; i < opts.size(); ++i)
        {
          ss << opts.at(i).first.size()  << ':' << opts.at(i).first;
          ss << opts.at(i).second().size() << ':' << opts.at(i).second();
        }
      ss << 'e' << ' ';
    }

  ss << 'l';
  for (args_vector::size_type i=0; i<cleaned_args.size(); ++i)
  {
      std::string arg = idx(cleaned_args, i)();
      ss << arg.size() << ':' << arg;
  }
  ss << 'e';

  L(FL("stdio input: %s") % ss.str());

  info.client.set_input_stream(ss);
  automate_ostream_demuxed os(output, std::cerr, app.opts.automate_stdio_size);
  info.client.set_output_stream(os);

  info.client.connection_type = netsync_connection_info::automate_connection;

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_and_sink_role, info);

  E(os.get_error() == 0, origin::network,
    F("received remote error code %d") % os.get_error());
}

CMD(push, "push", "", CMD_REF(network),
    N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
    N_("Pushes branches to a netsync server"),
    N_("This will push all branches that match the pattern given in PATTERN "
       "to the netsync server at the address ADDRESS."),
    options::opts::max_netsync_version | options::opts::min_netsync_version |
    options::opts::set_default | options::opts::exclude |
    options::opts::key_to_push)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys,
                                 project, args, info);

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_role, info);
}

CMD_AUTOMATE(push, N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
             N_("Pushes branches to a netsync server"),
             "",
              options::opts::max_netsync_version |
              options::opts::min_netsync_version |
              options::opts::set_default | options::opts::exclude |
              options::opts::key_to_push)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys, project, args, info);

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_role, info);
}

CMD(pull, "pull", "", CMD_REF(network),
    N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
    N_("Pulls branches from a netsync server"),
    N_("This pulls all branches that match the pattern given in PATTERN "
       "from the netsync server at the address ADDRESS."),
    options::opts::max_netsync_version | options::opts::min_netsync_version |
    options::opts::set_default | options::opts::exclude |
    options::opts::maybe_auto_update)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  maybe_workspace_updater updater(app, project);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys, project,
                                 args, info, false);

  if (!keys.have_signing_key())
    P(F("doing anonymous pull; use -kKEYNAME if you need authentication"));

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, sink_role, info);

  updater.maybe_do_update();
}

CMD_AUTOMATE(pull, N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
             N_("Pulls branches from a netsync server"),
             "",
             options::opts::max_netsync_version |
             options::opts::min_netsync_version |
             options::opts::set_default | options::opts::exclude)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys, project,
                                 args, info, false);

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, sink_role, info);
}

CMD(sync, "sync", "", CMD_REF(network),
    N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
    N_("Synchronizes branches with a netsync server"),
    N_("This synchronizes branches that match the pattern given in PATTERN "
       "with the netsync server at the address ADDRESS."),
    options::opts::max_netsync_version | options::opts::min_netsync_version |
    options::opts::set_default | options::opts::exclude |
    options::opts::key_to_push | options::opts::maybe_auto_update)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  maybe_workspace_updater updater(app, project);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys,
                                 project, args, info);

  if (app.opts.set_default && workspace::found)
    {
      // Write workspace options, including key; this is the simplest way to
      // fix a "found multiple keys" error reported by sync.
      workspace::set_options(app.opts, app.lua);
    }

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_and_sink_role, info);

  updater.maybe_do_update();
}

CMD_AUTOMATE(sync, N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
             N_("Synchronizes branches with a netsync server"),
             "",
             options::opts::max_netsync_version | options::opts::min_netsync_version |
             options::opts::set_default | options::opts::exclude |
             options::opts::key_to_push)
{
  database db(app);
  key_store keys(app);
  project_t project(db);

  netsync_connection_info info;
  extract_client_connection_info(app.opts, app.lua, db, keys, project, args, info);

  if (app.opts.set_default && workspace::found)
  {
    // Write workspace options, including key; this is the simplest way to
    // fix a "found multiple keys" error reported by sync.
    workspace::set_options(app.opts, app.lua);
  }

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, source_and_sink_role, info);
}

CMD(clone, "clone", "", CMD_REF(network),
    N_("ADDRESS[:PORTNUMBER] BRANCH [DIRECTORY]"),
    N_("Checks out a revision from a remote database into a directory"),
    N_("If a revision is given, that's the one that will be checked out.  "
       "Otherwise, it will be the head of the branch supplied.  "
       "If no directory is given, the branch name will be used as directory"),
    options::opts::max_netsync_version | options::opts::min_netsync_version |
    options::opts::revision)
{
  if (args.size() < 2 || args.size() > 3 || app.opts.revision_selectors.size() > 1)
    throw usage(execid);

  revision_id ident;
  system_path workspace_dir;
  netsync_connection_info info;
  info.client.unparsed = idx(args, 0);

  branch_name branchname = typecast_vocab<branch_name>(idx(args, 1));

  E(!branchname().empty(), origin::user,
    F("you must specify a branch to clone"));

  bool target_is_current_dir = false;
  if (args.size() == 2)
    {
      // No checkout dir specified, use branch name for dir.
      workspace_dir = system_path(branchname(), origin::user);
    }
  else
    {
      // Checkout to specified dir.
      workspace_dir = system_path(idx(args, 2));
      if (idx(args, 2) == utf8("."))
        target_is_current_dir = true;
    }

  if (!target_is_current_dir)
    {
      require_path_is_nonexistent
        (workspace_dir,
         F("clone destination directory '%s' already exists")
         % workspace_dir);
    }

  system_path _MTN_dir = workspace_dir / path_component("_MTN");

  require_path_is_nonexistent
    (_MTN_dir, F("bookkeeping directory already exists in '%s'")
     % workspace_dir);

  directory_cleanup_helper remove_on_fail(
    target_is_current_dir ? _MTN_dir : workspace_dir
  );

  // remember the initial working dir so that relative file://
  // db URIs will work
  system_path start_dir(get_current_working_dir(), origin::system);

  database_path_helper helper(app.lua);
  helper.maybe_set_default_alias(app.opts);

  database db(app);
  db.create_if_not_exists();
  db.ensure_open();

  // this is actually stupid, but app.opts.branch must be set here
  // otherwise it will not be written into _MTN/options, in case
  // a revision is chosen which has multiple branch certs
  app.opts.branch = branchname;
  workspace::create_workspace(app.opts, app.lua, workspace_dir);
  app.opts.branch = branch_name();

  key_store keys(app);
  project_t project(db);

  info.client.include_pattern = globish(branchname(), origin::user);
  info.client.exclude_pattern = globish(app.opts.exclude_patterns);

  build_client_connection_info(app.opts, app.lua, db, keys, project,
                               info, true, true, false);

  if (!keys.have_signing_key())
    P(F("doing anonymous pull; use -kKEYNAME if you need authentication"));

  // make sure we're back in the original dir so that file: URIs work
  change_current_working_dir(start_dir);

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       client_voice, sink_role, info);

  change_current_working_dir(workspace_dir);

  transaction_guard guard(db, false);

  if (app.opts.revision_selectors.empty())
    {
      set<revision_id> heads;
      project.get_branch_heads(branchname, heads,
                               app.opts.ignore_suspend_certs);
      E(!heads.empty(), origin::user,
        F("branch '%s' is empty") % branchname);
      if (heads.size() > 1)
        {
          P(F("branch %s has multiple heads:") % branchname);
          for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
            P(i18n_format("  %s")
              % describe_revision(app.opts, app.lua, project, *i));
          P(F("choose one with '%s clone -r<id> SERVER BRANCH'") % prog_name);
          E(false, origin::user, F("branch %s has multiple heads") % branchname);
        }
      ident = *(heads.begin());
    }
  else if (app.opts.revision_selectors.size() == 1)
    {
      // use specified revision
      complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 0)(), ident);

      E(project.revision_is_in_branch(ident, branchname),
        origin::user,
        F("revision %s is not a member of branch %s")
          % ident % branchname);
    }

  roster_t empty_roster, current_roster;

  L(FL("checking out revision %s to directory %s")
    % ident % workspace_dir);
  db.get_roster(ident, current_roster);

  workspace work(app);
  revision_t workrev;
  make_revision_for_workspace(ident, cset(), workrev);
  work.put_work_rev(workrev);

  cset checkout;
  make_cset(empty_roster, current_roster, checkout);

  content_merge_checkout_adaptor wca(db);
  work.perform_content_update(empty_roster, current_roster, checkout, wca, false);

  work.maybe_update_inodeprints(db);
  guard.commit();
  remove_on_fail.commit();
}

struct pid_file
{
  explicit pid_file(system_path const & p)
    : path(p)
  {
    if (path.empty())
      return;
    require_path_is_nonexistent(path, F("pid file '%s' already exists") % path);
    file.open(path.as_external().c_str());
    E(file.is_open(), origin::system, F("failed to create pid file '%s'") % path);
    file << get_process_id() << '\n';
    file.flush();
  }

  ~pid_file()
  {
    if (path.empty())
      return;
    pid_t pid;
    ifstream(path.as_external().c_str()) >> pid;
    if (pid == get_process_id()) {
      file.close();
      delete_file(path);
    }
  }

private:
  ofstream file;
  system_path path;
};

CMD_NO_WORKSPACE(serve, "serve", "", CMD_REF(network), "",
                 N_("Serves the database to connecting clients"),
                 "",
                 options::opts::max_netsync_version |
                 options::opts::min_netsync_version |
                 options::opts::bind | options::opts::pidfile |
                 options::opts::bind_stdio | options::opts::no_transport_auth)
{
  if (!args.empty())
    throw usage(execid);

  database db(app);
  key_store keys(app);
  project_t project(db);
  pid_file pid(app.opts.pidfile);

  db.ensure_open();

  netsync_connection_info info;
  info.server.addrs = app.opts.bind_uris;

  if (app.opts.use_transport_auth)
    {
      E(app.lua.hook_persist_phrase_ok(), origin::user,
        F("need permission to store persistent passphrase "
          "(see hook persist_phrase_ok())"));

      info.client.include_pattern = globish("*", origin::internal);
      info.client.exclude_pattern = globish("", origin::internal);
      if (!app.opts.bind_uris.empty())
        info.client.unparsed = *app.opts.bind_uris.begin();
      find_key(app.opts, db, keys, app.lua, project, info);
    }
  else if (!app.opts.bind_stdio)
    W(F("The --no-transport-auth option is usually only used "
        "in combination with --stdio"));

  run_netsync_protocol(app, app.opts, app.lua, project, keys,
                       server_voice, source_and_sink_role, info);
}

// Local Variables:
// mode: C++
// fill-column: 76
// c-file-style: "gnu"
// indent-tabs-mode: nil
// End:
// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:
