Blob Blame History Raw
/*
 * lftp - file transfer program
 *
 * Copyright (c) 1996-2013 by Alexander V. Lukyanov (lav@yars.free.net)
 *
 * 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/>.
 */

#include <config.h>

#include "modconfig.h"

#include "trio.h"
#include <sys/types.h>
#include <sys/stat.h> // for mkdir()
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <locale.h>
#include <ctype.h>

#include "xstring.h"
#include "xmalloc.h"
#include "alias.h"
#include "CmdExec.h"
#include "SignalHook.h"
#include "GetPass.h"
#include "History.h"
#include "log.h"
#include "DummyProto.h"
#include "ResMgr.h"
#include "LsCache.h"
#include "IdNameCache.h"
#include "LocalDir.h"
#include "ConnectionSlot.h"
#include "misc.h"
#include "ArgV.h"
#include "attach.h"

#include "configmake.h"

#include "lftp_rl.h"
#include "complete.h"

CDECL_BEGIN
#include <glob.h>
CDECL_END

#define top_exec CmdExec::top

void  hook_signals()
{
   SignalHook::DoCount(SIGHUP);
   SignalHook::Ignore(SIGTTOU);
   ProcWait::Signal(true);
}

ResDecl res_save_cwd_history
   ("cmd:save-cwd-history","yes",ResMgr::BoolValidate,ResMgr::NoClosure);
ResDecl res_save_rl_history
   ("cmd:save-rl-history","yes",ResMgr::BoolValidate,ResMgr::NoClosure);
ResDecl res_stifle_rl_history
   ("cmd:stifle-rl-history","500",ResMgr::UNumberValidate,ResMgr::NoClosure);

class ReadlineFeeder : public CmdFeeder, private ResClient
{
   bool tty:1;
   bool ctty:1;
   bool add_newline:1;
   int eof_count;
   xstring_c cmd_buf;
   xstring_c for_history;

   static bool readline_inited;
   void readline_init()
   {
      if(readline_inited)
	 return;
      readline_inited=true;
      lftp_readline_init();
      lftp_rl_read_history();
      if(for_history)
      {
	 lftp_add_history_nodups(for_history);
	 for_history.set(0);
      }
      Reconfig(0);
   }

public:
   ReadlineFeeder(const ArgV *args)
   {
      tty=isatty(0);
      ctty=(tcgetpgrp(0)!=(pid_t)-1);
      add_newline=false;
      eof_count=0;
      if(args && args->count()>1)
	 args->CombineQuotedTo(for_history);
   }
   virtual ~ReadlineFeeder()
   {
      if(readline_inited)
      {
	 if(res_save_cwd_history.QueryBool(0))
	    cwd_history.Save();
	 if(res_save_rl_history.QueryBool(0))
	    lftp_rl_write_history();
      }
   }
   bool IsInteractive() const
   {
      return tty;
   }
   bool RealEOF()
   {
      return !tty || eof_count>3;
   }

   const char *NextCmd(class CmdExec *exec,const char *prompt)
   {
      if(add_newline)
      {
	 add_newline=false;
	 return "\n";
      }

      ::completion_shell=exec;
      ::remote_completion=exec->remote_completion;

      if(tty)
      {
	 readline_init();

	 if(ctty) // controlling terminal
	 {
	    if(!in_foreground_pgrp())
	    {
	       // looks like we are in background. Can't read from tty
	       exec->Timeout(500);
	       return "";
	    }
	 }

	 SignalHook::ResetCount(SIGINT);
	 cmd_buf.set_allocated(lftp_readline(prompt));
	 xmalloc_register_block(cmd_buf.get_non_const());
	 if(cmd_buf && *cmd_buf)
	 {
	    if(exec->csh_history)
	    {
	       char *history_value0=0;
	       int expanded = lftp_history_expand (cmd_buf, &history_value0);
	       if (expanded)
	       {
		  if(history_value0)
		     xmalloc_register_block(history_value0);
		  xstring_ca history_value(history_value0);

		  if (expanded < 0)
		     fprintf (stderr, "%s\n", history_value.get());

		  /* If there was an error, return nothing. */
		  if (expanded < 0 || expanded == 2)	/* 2 == print only */
		  {
		     exec->Timeout(0);  // and retry immediately
		     return "";
		  }

		  cmd_buf.move_here(history_value);
	       }
	    }
	    lftp_add_history_nodups(cmd_buf);
	 }
	 else if(cmd_buf==0 && exec->interactive)
	    puts("exit");

	 if(cmd_buf==0)
	    eof_count++;
	 else
	    eof_count=0;
      }
      else // not a tty
      {
	 if(exec->interactive)
	 {
	    while(*prompt)
	    {
	       char ch=*prompt++;
	       if(ch!=1 && ch!=2)
		  putchar(ch);
	    }
	    fflush(stdout);
	 }
	 cmd_buf.set_allocated(readline_from_file(0));
      }

      ::completion_shell=0;

      if(cmd_buf && last_char(cmd_buf)!='\n')
      {
	 exec->Timeout(0);
	 add_newline=true;
      }
      return cmd_buf;
   }
   void clear()
      {
	 if(!tty)
	    return;
	 lftp_rl_clear();
      }
   void Reconfig(const char *) {
      lftp_rl_history_stifle(res_stifle_rl_history.Query(0));
   }
};
bool ReadlineFeeder::readline_inited;

#define args	  (parent->args)
#define exit_code (parent->exit_code)
#define output	  (parent->output)
#define session	  (parent->session)
#define eprintf	  parent->eprintf
CMD(history)
{
   enum { READ, WRITE, CLEAR, LIST } mode = LIST;
   const char *fn = NULL;
   static struct option history_options[]=
   {
      {"read",required_argument,0,'r'},
      {"write",required_argument,0,'w'},
      {"clear",no_argument,0,'c'},
      {"list",required_argument,0,'l'},
      {0,0,0,0}
   };

   exit_code=0;
   int opt;
   while((opt=args->getopt_long("+r:w:cl",history_options,0))!=EOF) {
      switch(opt) {
      case 'r':
	 mode = READ;
	 fn = optarg;
	 break;
      case 'w':
	 mode = WRITE;
	 fn = optarg;
	 break;
      case 'c':
	 mode = CLEAR;
	 break;
      case 'l':
	 mode = LIST;
	 break;
      case '?':
	 eprintf(_("Try `help %s' for more information.\n"),args->a0());
	 return 0;
      }
   }

   int cnt = 16;
   if(const char *arg = args->getcurr()) {
      if(!strcasecmp(arg, "all"))
	 cnt = -1;
      else if(isdigit((unsigned char)arg[0]))
	 cnt = atoi(arg);
      else {
	 eprintf(_("%s: %s - not a number\n"), args->a0(), args->getcurr());
	 exit_code=1;
	 return 0;
      }
   }

   switch(mode) {
   case READ:
      if(int err = lftp_history_read(fn)) {
	 eprintf("%s: %s: %s\n", args->a0(), fn, strerror(err));
	 exit_code=1;
      }
      break;

   case WRITE:
      if(int err = lftp_history_write(fn)) {
	 eprintf("%s: %s: %s\n", args->a0(), fn, strerror(err));
	 exit_code=1;
      }
      break;

   case LIST:
      lftp_history_list(cnt);
      break;
   case CLEAR:
      lftp_history_clear();
      break;
   }

   return 0;
}
CMD(attach)
{
   const char *pid_s=args->getarg(1);
   if(!pid_s) {
      xstring& path=AcceptTermFD::get_sock_path(1);
      path.rtrim('1');
      path.append('*');
      glob_t g;
      glob(path, 0, NULL, &g);
      for(size_t i=0; i<g.gl_pathc; i++) {
	 const char *sock_path=g.gl_pathv[i];
	 pid_s=strrchr(sock_path,'-');
	 if(!pid_s)
	    continue;
	 pid_s++;
	 int p=atoi(pid_s);
	 if(p<=1) {
	    pid_s=0;
	    continue;
	 }
	 if(kill(p,0)==-1) {
	    if(errno==ESRCH) {
	       eprintf("%s: removing stale socket `%s'.\n",args->a0(),sock_path);
	       if(unlink(sock_path)==-1)
		  eprintf("%s: unlink(%s): %s\n",args->a0(),sock_path,strerror(errno));
	    }
	    pid_s=0;
	    continue;
	 }
	 pid_s=alloca_strdup(pid_s);
	 break;
      }
      globfree(&g);
      if(!pid_s) {
	 eprintf("%s: no backgrounded lftp processes found.\n",args->a0());
	 return 0;
      }
   }
   int pid=atoi(pid_s);
   SMTaskRef<SendTermFD> term_sender(new SendTermFD(pid));
   while(!term_sender->Done()) {
      SMTask::Schedule();
      SMTask::Block();
   }
   exit_code=0;
   if(term_sender->Failed()) {
      eprintf("%s\n",term_sender->ErrorText());
      exit_code=1;
   }
   return 0;
}
#undef args
#undef exit_code
#undef output
#undef session
#undef eprintf


static void sig_term(int sig)
{
   printf(_("[%u] Terminated by signal %d. %s\n"),(unsigned)getpid(),sig,SMTask::now.IsoDateTime());
   if(top_exec) {
      top_exec->KillAll();
      alarm(30);
      while(Job::NumberOfJobs()>0) {
	 SMTask::Schedule();
	 SMTask::Block();
      }
   }
   exit(1);
}

static void detach()
{
   SignalHook::Ignore(SIGINT);
   SignalHook::Ignore(SIGHUP);
   SignalHook::Ignore(SIGTSTP);

   const char *home=get_lftp_data_dir();
   if(home)
   {
      xstring& log=xstring::get_tmp(home);
      if(access(log,F_OK)==-1)
	 log.append("_log");
      else
	 log.append("/log");

      int fd=open(log,O_WRONLY|O_APPEND|O_CREAT,0600);
      if(fd>=0)
      {
	 dup2(fd,1); // stdout
	 dup2(fd,2); // stderr
	 if(fd!=1 && fd!=2)
	    close(fd);
      }
      const char *c="debug";
      ResMgr::Set("log:show-pid",c,"yes");
      ResMgr::Set("log:show-time",c,"yes");
      ResMgr::Set("log:show-ctx",c,"yes");
   }
   close(0);	  // close stdin.
   open("/dev/null",O_RDONLY); // reopen it, just in case.

#ifdef HAVE_SETSID
   setsid();	  // start a new session.
#endif

   SignalHook::Handle(SIGTERM,sig_term);
}

static void move_to_background()
{
   // notify jobs
   Job::lftpMovesToBackground_ToAll();
   // wait they do something, but no more than 1 sec.
   SMTask::RollAll(TimeInterval(1,0));
   // if all jobs terminated, don't really move to bg.
   if(Job::NumberOfJobs()==0)
      return;

   top_exec->AtBackground();
   top_exec->WaitDone();
   if(Job::NumberOfJobs()==0)
      return;

   fflush(stdout);
   fflush(stderr);

   pid_t pid=fork();
   switch(pid)
   {
   case(0): // child
   {
      pid=getpid();
      detach();
      printf(_("[%u] Started.  %s\n"),(unsigned)pid,SMTask::now.IsoDateTime());
      SMTaskRef<AcceptTermFD> term_acceptor(new AcceptTermFD());
      for(;;)
      {
	 SMTask::Schedule();
	 if(Job::NumberOfJobs()==0)
	    break;
	 SMTask::Block();
	 if(term_acceptor->Accepted()) {
	    hook_signals();
	    top_exec->SetInteractive();
	    top_exec->SetStatusLine(new StatusLine(1));
	    top_exec->SetCmdFeeder(new ReadlineFeeder(0));
	    for(;;)
	    {
	       SMTask::Schedule();
	       if(top_exec->Done() || term_acceptor->Detached()) {
		  if(Job::NumberOfJobs()>0) {
		     printf(_("[%u] Detaching from the terminal to complete transfers...\n"),(unsigned)pid);
		  } else if(top_exec->Done()) {
		     printf(_("[%u] Exiting and detaching from the terminal.\n"),(unsigned)pid);
		  }
		  fflush(stdout);
		  term_acceptor->Detach();
		  detach();
		  printf(_("[%u] Detached from terminal. %s\n"),(unsigned)pid,SMTask::now.IsoDateTime());
		  break;
	       }
	       SMTask::Block();
	    }
	 }
      }
      top_exec->AtExitBg();
      top_exec->AtTerminate();
      top_exec->WaitDone();
      printf(_("[%u] Finished. %s\n"),(unsigned)pid,SMTask::now.IsoDateTime());
      break;
   }
   default: // parent
      printf(_("[%u] Moving to background to complete transfers...\n"),
	       (unsigned)pid);
      fflush(stdout);
      _exit(0);
   case(-1):
      perror("fork()");
   }
}

int lftp_slot(int count,int key)
{
   if(!top_exec)
      return 0;
   char slot[2];
   slot[0]=key;
   slot[1]=0;
   top_exec->ChangeSlot(slot);
   lftp_rl_set_prompt(top_exec->MakePrompt());
   return 0;
}

void  source_if_exist(const char *rc)
{
   if(access(rc,R_OK)!=-1)
   {
      top_exec->FeedCmd("source ");
      top_exec->FeedCmd(rc);
      top_exec->FeedCmd("\n");
   }
}

static void tty_clear()
{
   if(top_exec)
      top_exec->pre_stdout();
}

// look for the option, remove it and return true if found
static bool pick_option(int& argc,char **argv,const char *option)
{
   for(int i=1; i<argc; i++) {
      if(!strcmp(argv[i],option)) {
	 // remove the option, move trailing NULL too.
	 memmove(argv+i,argv+i+1,(argc-i)*sizeof(*argv));
	 argc--;
	 return true;
      }
      if(!strcmp(argv[i],"--"))	 // end of options
	 break;
   }
   return false;
}

char *program_name;

int   main(int argc,char **argv)
{
   program_name=argv[0];

#ifdef SOCKS4
   SOCKSinit(program_name);
#endif

   setlocale (LC_ALL, "");
   setlocale (LC_NUMERIC, "C");
   bindtextdomain (PACKAGE, LOCALEDIR);
   textdomain (PACKAGE);

   CmdExec::RegisterCommand("history",cmd_history,
	 N_("history -w file|-r file|-c|-l [cnt]"),
	 N_(" -w <file> Write history to file.\n"
	 " -r <file> Read history from file; appends to current history.\n"
	 " -c  Clear the history.\n"
	 " -l  List the history (default).\n"
	 "Optional argument cnt specifies the number of history lines to list,\n"
	 "or \"all\" to list all entries.\n"));
   CmdExec::RegisterCommand("attach",cmd_attach,"attach [PID]",
      N_("Attach the terminal to specified backgrounded lftp process.\n"));

   top_exec=new CmdExec(0,0);
   hook_signals();
   top_exec->SetStatusLine(new StatusLine(1));
   Log::global=new Log("debug");
   Log::global->SetCB(tty_clear);

   source_if_exist(SYSCONFDIR"/lftp.conf");

   if(!pick_option(argc,argv,"--norc")) {
      const char *home=getenv("HOME");
      if(home)
	 source_if_exist(dir_file(home,".lftprc"));
      home=get_lftp_config_dir();
      if(home)
	 source_if_exist(dir_file(home,"rc"));
   }

   top_exec->WaitDone();
   top_exec->SetTopLevel();
   top_exec->Fg();

   Ref<ArgV> args(new ArgV(argc,argv));
   args->setarg(0,"lftp");

   lftp_feeder=new ReadlineFeeder(args);

   top_exec->ExecParsed(args.borrow());
   top_exec->WaitDone();
   int exit_code=top_exec->ExitCode();

   top_exec->AtExit();
   top_exec->WaitDone();

   if(Job::NumberOfJobs()>0)
   {
      top_exec->SetInteractive(false);
      move_to_background();
   }
   else
   {
      top_exec->AtExitFg();
      top_exec->AtTerminate();
      top_exec->WaitDone();
   }
   top_exec->KillAll();
   top_exec=0;

   Job::Cleanup();
   ConnectionSlot::Cleanup();
   SessionPool::ClearAll();
   FileAccess::ClassCleanup();
   ProcWait::DeleteAll();
   IdNameCacheCleanup();
   SignalHook::Cleanup();
   Log::Cleanup();
   SMTask::Cleanup();
   return exit_code;
}