/*
 * shell.c
 *
 * Copyright 2004, Minoru Murashima. All rights reserved.
 * Distributed under the terms of the BSD License.
 *
 * シェルユーザーインターフェース。
 */


#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<dirent.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<termios.h>
#include<limits.h>
#include<stropts.h>
/****************************/
extern void sys_test();
/******************************/


enum{
	ARG_SIZE=128,			/* 引数バッファサイズ。 */
	SHELL_ARGS_MAX=10,		/* シェルが扱う引数の最大数。 */
};


/*****************************************************************************************
 *
 * Error handler.
 *
 ******************************************************************************************/


/*
 * PUBLIC
 * エラーの表示。
 */
static void errstr(const char *mes)
{
	fprintf(stderr,"%s\n",mes);
}


/*****************************************************************************************
 *
 * Prompt.
 *
 ******************************************************************************************/

enum{
	CRNT_DIR_BUF_SIZE=128,	/* Size of current directory path buffer. */
};


/* PRIVATE */
static char crntDir[CRNT_DIR_BUF_SIZE];		/* Current directory buffer. */


/*
 * PRIVATE
 * カレントディレクトリバッファ変換。
 */
static inline void crntDirToPrompt()
{
	char *p,*q;


	for(p=q=crntDir+1;*p!='\0';++p)
	{
		if(*p=='/')q=p+1;
	}
	if(p==&crntDir[1])crntDir[0]='/';
	else memcpy(crntDir,q,(uint)p-(uint)q+1);
}


/*
 * PUBLIC
 * Init prompt.
 * return : 0 or error=-1
 */
static int initPrompt()
{
	/* Get current directory. */
	if(getcwd(crntDir,CRNT_DIR_BUF_SIZE)==NULL)
	{
		fprintf(stderr,"shell : Failed getcwd\n");
		return -1;
	}

	crntDirToPrompt();

	return 0;
}


/*****************************************************************************************
 *
 * Job control.
 *
 ******************************************************************************************/


/*
 * PUBLIC
 * ジョブリスト。
 */
static pid_t volatile job[CHILD_MAX];
static int jobCnt;


/*
 * return : 0 or max=1
 */
static int chkJobNum()
{
	if(jobCnt>0)
	{
		while(getpgid(job[jobCnt-1])==-1)
			job[--jobCnt]=0;

		if(jobCnt==CHILD_MAX)return 1;
	}
	return 0;
}


/*
 * ジョブの登録。
 */
static void addJob(pid_t pid)
{
	job[jobCnt++]=pid;
}


/*
 * ジョブの削除。
 */
static void delJob()
{
	int i;


	for(i=0;i<jobCnt;++i)
		if(getpgid(job[i])==-1)
		{
			memcpy((void*)&job[i],(void*)&job[i+1],(jobCnt-i-1)*sizeof(pid_t));
			job[--jobCnt]=0;
			--i;
		}
}


/*
 * fg
 * 引数は '%n(nは数字)' のみ対応。
 */
static void fg(char **arg)
{
	int num;
	pid_t grp;


	/* 引数の確認。 */
	if(arg[0]==NULL)
	{
		if((num=jobCnt-1)<0)return errstr("No job!");
		while((grp=getpgid(job[num]))<0)
		{
			job[num]=0;
			if(--num<0)return errstr("No job!");
		}
		jobCnt=num+1;
	}
	else if(*arg[0]=='%')
	{
		if(((num=atoi(&arg[0][1]))==0)||(num>CHILD_MAX))
			return errstr("Argument is wrong!");
		num=jobCnt-num;
		if((job[num]==0)||((grp=getpgid(job[num]))==-1))
		{
			memcpy((void*)&job[num],(void*)&job[num+1],(jobCnt-num-1)*sizeof(pid_t));
			job[--jobCnt]=0;
			return errstr("Argument is wrong!");
		}
	}
	else return errstr("Argument is wrong!");

	/* フォアグラウンドグループの変更。 */
	if(tcsetpgrp(0,grp)==-1)
		return perror("Failed tcsetpgrp()");
	kill(-grp,SIGCONT);					/* フォアグラウンドグループにSIGCONTを送る。 */

	/* ウェイト。 */
	waitpid(job[num],NULL,0);

	if((grp=getpgrp())!=tcgetpgrp(0))
		tcsetpgrp(0,grp);			/* 端末フォアグラウンドグループを戻す。 */
}


/*****************************************************************************************
 *
 * Execute file.
 *
 ******************************************************************************************/


/*
 * PUBLIC
 * Execute file.
 * parameters : arguments
 */
static void execFile(char **arg)
{
	int pid;
	int backgrnd=0;		/* バックグラウンド実行フラグ。 */
	int state=0;
	int i;


	/* ジョブ数をチェック。 */
	if(chkJobNum()==1)
		return errstr("shell : Number of child processes is max!");

	/* 引数に'&'を探す。 */
	for(i=1;arg[i]!=NULL;++i)
		if(*arg[i]=='&')
		{
			arg[i]=NULL;
			backgrnd=1;
			break;
		}

	/* 実行。 */
	pid=fork();
    if(pid==0)
	{
		if(backgrnd)setpgid(0,0);
		if(execve(arg[0],arg,NULL)!=0)
			exit(1);
	}
	else if(pid<0)
	{
		perror("Failed fork! error");
		return;
	}

	addJob(pid);
	if(backgrnd==0)
	{
		pid_t grp;

		waitpid(pid,&state,0);

		if((grp=getpgrp())!=tcgetpgrp(0))
			tcsetpgrp(0,grp);			/* 端末フォアグラウンドグループを戻す。 */

		/* 子プロセス終了状態の確認。 */
		if(WIFEXITED(state))
		{
			if(WEXITSTATUS(state)==1)
				fprintf(stderr,"shell : %s execute error!\n",arg[0]);
			delJob();
		}
	}
}


/*****************************************************************************************
 *
 * Do builtin command.
 *
 ******************************************************************************************/

typedef struct{
	const char *name;
	void (*cmd)(char**);
}COMMAND;


/*
 * PRIVATE
 * 文字列の最後が'/'なら'\0'を上書きする。
 * NOTE!
 *  最初のNULLをチェックしない。
 * parameters : 文字列
 */
static char *takeOutSlash(char *str)
{
	char *p;


	p=str;
	while(*++p!='\0');
	if((--p!=str)&&(*p=='/'))*p='\0';

	return str;
}


/*
 * PRIVATE
 * Builtin command.
 */
/* cd */
static void cd(char **arg)
{
	if(arg[0]==NULL)return;
	if(chdir(takeOutSlash(arg[0]))==0)initPrompt();
	else perror("Failed cd");
}

/* reboot */
static void _reboot(char **arg)
{
	if(reboot(REBOOT_REBOOT)==-1)fprintf(stderr,"shell : Failed halt\n");
}

/* halt */
static void halt(char **arg)
{
	if(reboot(REBOOT_POWEROFF)==-1)perror("Failed halt!");
}

/* ls */
/*static void ls(char **arg)
{
	char crtDir[]={'.','\0'};
	int len;
	DIR *dir;
	struct dirent *dirent;
	struct stat sbuf;


	if(arg[0]=='\0')arg[0]=crtDir;

	if((dir=opendir(takeOutSlash(arg[0])))==NULL)
		return perror("Failed ls!");

	strcat(arg[0],"/");
	len=strlen(arg[0]);
	while((dirent=readdir(dir))!=NULL)
	{
		strcpy(&arg[0][len],dirent->d_name);
		stat(arg[0],&sbuf);
		if(S_ISDIR(sbuf.st_mode))
			fprintf(stderr,"%s/\n",dirent->d_name);
		else fprintf(stderr,"%s\n",dirent->d_name);
	}

	if(closedir(dir)==-1)
		perror("Failed closedir!");
}
*/

/*
 * PRIVATE
 * Cmmand structure
 * 配列はコマンド名の順に並べる。
 */
static COMMAND command[]={
	{"cd",cd},
	{"fg",fg},
	{"halt",halt},
/*	{"ls",ls},*/
	{"reboot",_reboot},
/***********************************/
	{"test",sys_test},
/************************************/
};


enum{
	CMD_NUM=sizeof(command)/sizeof(COMMAND),		/* Number of builtin commands. */
};


/*
 * PUBLIC
 * Execute command.
 * parameters : arguments
 */
static void execCommand(char *arg[])
{
	int low,high,middle;


	/* 二分検索。 */
	for(low=0,high=CMD_NUM-1;low<=high;)
	{
		middle=(low+high)/2;
		if(strcmp(arg[0],command[middle].name) > 0)low=middle+1;
		else high=middle-1;
	}
	if((low>=CMD_NUM)||(strcmp(arg[0],command[low].name)!=0))
		fprintf(stderr,"shell : Command ""%s"" not found\n",arg[0]);
	else command[low].cmd(&arg[1]);
}


/*****************************************************************************************
 *
 * Parse command line.
 *
 ******************************************************************************************/

/*
 * PUBLIC
 * 入力されたコマンドラインを区切って引数ポインタバッファを設定する。
 * 最後の引数ポインタはNULL文字をポインタを代入する。
 * parameters : command line buffer,arguments pointer buffer
 */
static void parseCommandLine(char *line,char *arg[])
{
	char*p,*q;
	int i;


	p=line;
	for(;*p==' ';++p);
	for(i=0;i<SHELL_ARGS_MAX-1;++i)
	{
		if((q=strchr(p,' '))!=NULL)
		{
			*q='\0';
			arg[i]=p;
			while(*++q==' ');
			p=q;
		}
		else
		{
			if(*p=='\0')arg[i]=NULL;
			else
			{
				arg[i]=p;
				arg[i+1]=NULL;
			}
			break;
		}
	}
}


/*****************************************************************************************
 *
 * Main.
 *
 ******************************************************************************************/

int main()
{
	char buf[ARG_SIZE];
	char *arg[SHELL_ARGS_MAX];
	int size;

	/* セッション開始。 */
	if(setsid()<0)
	{
		perror("Failed setsid");
		return 0;
	}

	/* 制御端末の取得。 */
	if(ioctl(0,TIOCSCTTY)<0)
	{
		perror("failed ioctl");
		return 0;
	}

	/* プロンプトの表示。 */
	if(initPrompt()!=0)
	{
		fprintf(stderr,"shell : Fialed init prompt!\n");
		return 0;
	}

	for(;;)
	{
		fprintf(stderr,"%s>",crntDir);
		
		/* 端末からの入力受付。 */
		if((size=read(STDIN_FILENO,buf,ARG_SIZE-1))>1)
		{
			buf[size-1]='\0';

			/* トークンに分割。 */
			parseCommandLine(buf,arg);
			
			/* Execute command line. */
			if(arg[0]!=NULL)
			{
				if(strchr(arg[0],'/')!=NULL)execFile(arg);
				else execCommand(arg);
			}
		}
		else if(size<0)
		{
			perror("shell : failed read standard in");
			break;
		}
	}

	return 0;
}
