/*
 *  innduct
 *  tailing reliable realtime streaming feeder for inn
 *  cli.c - command and control connections
 *
 *  Copyright Ian Jackson <ijackson@chiark.greenend.org.uk>
 *  and contributors; see LICENCE.txt.
 *  SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "innduct.h"

/*========== command and control (CLI) connections ==========*/

static int cli_master;

typedef struct CliConn CliConn;
struct CliConn {
  void (*destroy)(CliConn*);
  int fd;
  oop_read *rd;
  FILE *out;
  union {
    struct sockaddr sa;
    struct sockaddr_un un;
  } sa;
  socklen_t salen;
};

static const oop_rd_style cli_rd_style= {
  OOP_RD_DELIM_STRIP, '\n',
  OOP_RD_NUL_FORBID,
  OOP_RD_SHORTREC_FORBID
};

static void cli_destroy(CliConn *cc) {
  cc->destroy(cc);
}

static void cli_checkouterr(CliConn *cc /* may destroy*/) {
  if (ferror(cc->out) | fflush(cc->out)) {
    info("CTRL%d write error %s", cc->fd, strerror(errno));
    cli_destroy(cc);
  }
}

static void cli_prompt(CliConn *cc /* may destroy*/) {
  fprintf(cc->out, "%s| ", sitename);
  cli_checkouterr(cc);
}

struct CliCommand {
  const char *cmd;
  void (*f)(CliConn *cc, const CliCommand *ccmd,
	    const char *arg, size_t argsz);
  void *xdata;
  int xval;
};

static const CliCommand cli_commands[];

#define CCMD(wh)						\
  static void ccmd_##wh(CliConn *cc, const CliCommand *c,	\
			const char *arg, size_t argsz)

CCMD(help) {
  fputs("commands:\n", cc->out);
  const CliCommand *ccmd;
  for (ccmd=cli_commands; ccmd->cmd; ccmd++)
    fprintf(cc->out, " %s\n", ccmd->cmd);
  fputs("NB: permissible arguments are not shown above."
	"  Not all commands listed are safe.  See innduct(8).\n", cc->out);
}

CCMD(flush) {
  int ok= trigger_flush_ok("manual request");
  if (!ok) fprintf(cc->out,"already flushing (state is %s)\n", sms_names[sms]);
}

CCMD(stop) {
  preterminate();
  notice("terminating (CTRL%d)",cc->fd);
  raise_default(SIGTERM);
  abort();
}

CCMD(logstats) { showstats(); }

CCMD(dump);
CCMD(dumphere);

/* messing with our head: */
CCMD(period) { period(); }
CCMD(setintarg) { *(int*)c->xdata= atoi(arg); }
CCMD(setint) { *(int*)c->xdata= c->xval; }
CCMD(setint_period) { *(int*)c->xdata= c->xval; period(); }

static const CliCommand cli_commands[]= {
  { "h",             ccmd_help      },
  { "flush",         ccmd_flush     },
  { "stop",          ccmd_stop      },
  { "logstats",      ccmd_logstats  },
  { "dump q",        ccmd_dump, 0,0 },
  { "dump a",        ccmd_dump, 0,1 },
  { "show",          ccmd_dumphere  },

  { "p",             ccmd_period    },

#define POKES(cmd,func)							\
  { cmd "flush",     func,           &until_flush,             1 },	\
  { cmd "conn",      func,           &until_connect,           0 },	\
  { cmd "blscan",    func,           &until_backlog_nextscan,  0 },
POKES("next ", ccmd_setint)
POKES("prod ", ccmd_setint_period)

  { "pretend flush", ccmd_setintarg, &simulate_flush             },
  { "wedge blscan",  ccmd_setint,    &until_backlog_nextscan, -1 },
  { 0 }
};

static void *cli_rd_ok(oop_source *lp, oop_read *oread, oop_rd_event ev,
		       const char *errmsg, int errnoval,
		       const char *data, size_t recszu, void *cc_v) {
  CliConn *cc= cc_v;

  if (!data) {
    info("CTRL%d closed", cc->fd);
    cc->destroy(cc);
    return OOP_CONTINUE;
  }

  if (recszu == 0) goto prompt;
  assert(recszu <= INT_MAX);
  int recsz= recszu;

  const CliCommand *ccmd;
  for (ccmd=cli_commands; ccmd->cmd; ccmd++) {
    int l= strlen(ccmd->cmd);
    if (recsz < l) continue;
    if (recsz > l && data[l] != ' ') continue;
    if (memcmp(data, ccmd->cmd, l)) continue;

    int argl= (int)recsz - (l+1); 
    ccmd->f(cc, ccmd, argl>=0 ? data+l+1 : 0, argl);
    goto prompt;
  }

  fputs("unknown command; h for help\n", cc->out);

 prompt:
  cli_prompt(cc);
  return OOP_CONTINUE;
}

static void *cli_rd_err(oop_source *lp, oop_read *oread, oop_rd_event ev,
			const char *errmsg, int errnoval,
			const char *data, size_t recsz, void *cc_v) {
  CliConn *cc= cc_v;
  
  info("CTRL%d read error %s", cc->fd, errmsg);
  cc->destroy(cc);
  return OOP_CONTINUE;
}

static int cli_conn_startup(CliConn *cc /* may destroy*/,
				const char *how) {
  cc->rd= oop_rd_new_fd(loop, cc->fd, 0,0);
  if (!cc->rd) { warn("oop_rd_new_fd cli failed"); return -1; }

  int er= oop_rd_read(cc->rd, &cli_rd_style, MAX_CLI_COMMAND,
		      cli_rd_ok, cc,
		      cli_rd_err, cc);
  if (er) { errno= er; syswarn("oop_rd_read cli failed"); return -1; }

  info("CTRL%d %s ready", cc->fd, how);
  cli_prompt(cc);
  return 0;
}

static void cli_stdio_destroy(CliConn *cc) {
  if (cc->rd) {
    oop_rd_cancel(cc->rd);
    errno= oop_rd_delete_tidy(cc->rd);
    if (errno) syswarn("oop_rd_delete tidy failed (no-nonblock stdin?)");
  }
  free(cc);
}

void cli_stdio(void) {
  NEW_DECL(CliConn *,cc);
  cc->destroy= cli_stdio_destroy;

  cc->fd= 0;
  cc->out= stdout;
  int r= cli_conn_startup(cc,"stdio");
  if (r) cc->destroy(cc);
}

static void cli_accepted_destroy(CliConn *cc) {
  if (cc->rd) {
    oop_rd_cancel(cc->rd);
    oop_rd_delete_kill(cc->rd);
  }
  if (cc->out) { fclose(cc->out); cc->fd=0; }
  close_perhaps(&cc->fd);
  free(cc);
}

static void *cli_master_readable(oop_source *lp, int master,
				 oop_event ev, void *u) {
  NEW_DECL(CliConn *,cc);
  cc->destroy= cli_accepted_destroy;

  cc->salen= sizeof(cc->sa);
  cc->fd= accept(master, &cc->sa.sa, &cc->salen);
  if (cc->fd<0) { syswarn("error accepting cli connection"); goto x; }

  cc->out= fdopen(cc->fd, "w");
  if (!cc->out) { syswarn("error fdopening accepted cli connection"); goto x; }

  int r= cli_conn_startup(cc, "accepted");
  if (r) goto x;

  return OOP_CONTINUE;

 x:
  cc->destroy(cc);
  return OOP_CONTINUE;
}

#define NOCLI(...) do{						\
    syswarn("no cli listener, because failed to " __VA_ARGS__);	\
    goto nocli;							\
  }while(0)

void cli_init(void) {
  union {
    struct sockaddr sa;
    struct sockaddr_un un;
  } sa;

  memset(&sa,0,sizeof(sa));
  int maxlen= sizeof(sa.un.sun_path);

  if (!path_cli) {
    info("control command line disabled");
    return;
  }

  int pathlen= strlen(path_cli);
  if (pathlen > maxlen) {
    warn("no cli listener, because cli socket path %s too long (%d>%d)",
	 path_cli, pathlen, maxlen);
    return;
  }

  if (path_cli_dir) {
    int r= mkdir(path_cli_dir, 0700);
    if (r && errno!=EEXIST)
      NOCLI("create cli socket directory %s", path_cli_dir);
  }

  int r= unlink(path_cli);
  if (r && errno!=ENOENT)
    NOCLI("remove old cli socket %s", path_cli);

  cli_master= socket(PF_UNIX, SOCK_STREAM, 0);
  if (cli_master<0) NOCLI("create new cli master socket");

  int sl= pathlen + offsetof(struct sockaddr_un, sun_path);
  sa.un.sun_family= AF_UNIX;
  memcpy(sa.un.sun_path, path_cli, pathlen);

  r= bind(cli_master, &sa.sa, sl);
  if (r) NOCLI("bind to cli socket path %s", sa.un.sun_path);

  r= listen(cli_master, 5);
  if (r) NOCLI("listen to cli master socket");

  xsetnonblock(cli_master, 1);

  loop->on_fd(loop, cli_master, OOP_READ, cli_master_readable, 0);
  info("cli ready, listening on %s", path_cli);

  return;

 nocli:
  xclose_perhaps(&cli_master, "cli master",0);
  return;
}

/*========== dumping state ==========*/

static void dump_article_list(FILE *f, const CliCommand *c,
			      const ArticleList *al) {
  fprintf(f, " count=%d\n", al->count);
  if (!c->xval) return;
  
  int i; Article *art;
  for (i=0, art=LIST_HEAD(*al); art; i++, art=LIST_NEXT(art)) {
    fprintf(f," #%05d %-11s", i, artstate_names[art->state]);
    DUMPV("%p", art->,ipf);
    DUMPV("%d", art->,missing);
    DUMPV("%lu", (unsigned long)art->,offset);
    DUMPV("%d", art->,blanklen);
    DUMPV("%d", art->,midlen);
    fprintf(f, " %s %s\n", TokenToText(art->token), art->messageid);
  }
}

static void dump_counts_events(FILE *f, const Counts *counts) {
  DUMPV("%d", counts->,events[read_ok]);
  DUMPV("%d", counts->,events[read_blank]);
  DUMPV("%d", counts->,events[read_err]);
  DUMPV("%d", counts->,events[nooffer_missing]);
}

static void dump_counts_results(FILE *f, const Counts *counts,
				const char *wh1, const char *wh2) {
  ArtState state; const char *const *statename;
  for (state=0, statename=artstate_names; *statename; state++,statename++) {
#define RC_DUMP_FMT(x) " " #x "=%d"
#define RC_DUMP_VAL(x) ,counts->results[state][RC_##x]
    fprintf(f,"%s%s counts %-11s"
	    RESULT_COUNTS(RC_DUMP_FMT,RC_DUMP_FMT) "\n",
	    wh1,wh2, *statename
	    RESULT_COUNTS(RC_DUMP_VAL,RC_DUMP_VAL));
  }
}

static void dump_input_file(FILE *f, const CliCommand *c,
			    InputFile *ipf, const char *wh) {
  char *dipf= dbg_report_ipf(ipf);
  fprintf(f,"input %s %s", wh, dipf);
  free(dipf);
  if (ipf) dump_counts_events(f, &ipf->counts);
  fprintf(f,"\n");
  if (ipf) {
    dump_counts_results(f, &ipf->counts, "input ",wh);
    fprintf(f,"input %s queue", wh);
    dump_article_list(f,c,&ipf->queue);
  }
}

static void dumpinfo(const CliCommand *c, FILE *f) {
  int i;
  fprintf(f,"general");
  DUMPV("%s", sms_names,[sms]);
  DUMPV("%d", ,until_flush);
  DUMPV("%ld", (long),self_pid);
  DUMPV("%p", , defer);
  DUMPV("%d", , until_connect);
  DUMPV("%d", , until_backlog_nextscan);
  DUMPV("%d", , simulate_flush);
  fprintf(f,"\nnocheck");
  DUMPV("%#.10f", , accept_proportion);
  DUMPV("%d", , nocheck);
  DUMPV("%d", , nocheck_reported);
  fprintf(f,"\n");

  fprintf(f,"special");
  DUMPV("%ld", (long),connecting_child);
  DUMPV("%d", , connecting_fdpass_sock);
  DUMPV("%d", , cli_master);
  fprintf(f,"\n");

  fprintf(f,"lowvol");
  DUMPV("%d", , lowvol_circptr);
  DUMPV("%d", , lowvol_total);
  fprintf(f,":");
  for (i=0; i<lowvol_periods; i++) {
    fprintf(f," ");
    if (i==lowvol_circptr) fprintf(f,"*");
    fprintf(f,"%d",lowvol_perperiod[i]);
  }
  fprintf(f,"\n");

  fprintf(f,"filemon ");
  filemon_method_dump_info(f);

  dump_input_file(f,c, main_input_file,     "main"    );
  dump_input_file(f,c, flushing_input_file, "flushing");
  dump_input_file(f,c, backlog_input_file,  "backlog" );
  if (backlog_counts_report) {
    fprintf(f,"completed backlogs");
    dump_counts_events(f, &backlog_counts);
    fprintf(f,"\n");
    dump_counts_results(f, &backlog_counts, "completed backlogs","");
  }

  fprintf(f,"conns count=%d\n", conns.count);

  Conn *conn;
  FOR_CONN(conn) {

    fprintf(f,"C%d",conn->fd);
    DUMPV("%p",conn->,rd);             DUMPV("%d",conn->,max_queue);
    DUMPV("%d",conn->,stream);         DUMPV("\"%s\"",conn->,quitting);
    DUMPV("%d",conn->,since_activity);
    fprintf(f,"\n");

    fprintf(f,"C%d waiting", conn->fd); dump_article_list(f,c,&conn->waiting);
    fprintf(f,"C%d priority",conn->fd); dump_article_list(f,c,&conn->priority);
    fprintf(f,"C%d sent",    conn->fd); dump_article_list(f,c,&conn->sent);

    fprintf(f,"C%d xmit xmitu=%d\n", conn->fd, conn->xmitu);
    for (i=0; i<conn->xmitu; i++) {
      const struct iovec *iv= &conn->xmit[i];
      const XmitDetails *xd= &conn->xmitd[i];
      char *dinfo;
      switch (xd->kind) {
      case xk_Const:    dinfo= masprintf("Const");                 break;
      case xk_Artdata:  dinfo= masprintf("A%p", xd->info.sm_art);  break;
      default:
	abort();
      }
      fprintf(f," #%03d %-11s l=%zd %s\n", i, dinfo, iv->iov_len,
	      sanitise(iv->iov_base, iv->iov_len));
      free(dinfo);
    }
  }

  fprintf(f,"paths");
  DUMPV("%s", , feedfile);
  DUMPV("%s", , path_cli);
  DUMPV("%s", , path_lock);
  DUMPV("%s", , path_flushing);
  DUMPV("%s", , path_defer);
  DUMPV("%s", , path_dump);
  DUMPV("%s", , globpat_backlog);
  fprintf(f,"\n");
}

CCMD(dump) {
  fprintf(cc->out, "dumping state to %s\n", path_dump);
  FILE *f= fopen(path_dump, "w");
  if (!f) { fprintf(cc->out, "failed: open: %s\n", strerror(errno)); return; }
  dumpinfo(c,f);
  if (!!ferror(f) + !!fclose(f)) {
    fprintf(cc->out, "failed: write: %s\n", strerror(errno));
    return;
  }
}

CCMD(dumphere) {
  dumpinfo(c,cc->out);
  fprintf(cc->out, ".\n");
}
