elks-enhanced
public
Read
Owner: themaster
Branch: master
Commits: 6893
Updated: 2026-04-19 00:15
Git CLI clone URL
git clone https://www.xt-emporium.com/git/elks-enhanced.git
Fullscreen desktop URL
Code
Commits
History
Branches
Bug Reports
Discussions
Compare
Settings
elks-enhanced
/
elkscmd
/
screen
/
screen.c
File editor
/* * Copyright (c) 1987,1988 Oliver Laumann, Technical University of Berlin. * Not derived from licensed software. * * Permission is granted to freely use, copy, modify, and redistribute this * software, provided that no attempt is made to gain profit from it, the * author is not construed to be liable for any results of using the * software, alterations are clearly marked as such, and this notice is not * modified. * * This code was modified for use with ELKS and the bcc compiler (which only * supports K&R) by Riley Williams <rhw@MemAlpha.cx> with help from the Linux * ELKS development team on SourceForge.Net. It is based on the screen 2.0a * code as released on 19th October 1988. */ static char ScreenVersion[] = "screen 2.0a.2 (ELKS) 30-Apr-2020"; #include <ctype.h> #include <dirent.h> #include <errno.h> #include <fcntl.h> #include <limits.h> #include <pwd.h> #include <signal.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <sys/wait.h> #include <termcap.h> #include <termios.h> #include <time.h> #include <unistd.h> #ifdef UTMP #include <utmp.h> #endif #ifdef SUNLOADAV #include <sys/param.h> #endif #include "screen.h" struct mode OldMode, NewMode; #ifdef GETTTYENT #include <ttyent.h> #else static struct ttyent { char *ty_name; } *getttyent(); static char *tt, *ttnext; static char ttys[] = "/etc/ttys"; #endif #define MAXWIN 10 #define MSGWAIT 5 #define Ctrl(c) ((c)&037) static int SigHandler(void); static int DoWait(void); static void CheckWindows(void); static int ProcessInput(char *buf, int len); static void SwitchWindow(int n); static int SetCurrWindow(int n); static int NextWindow(void); static int PreviousWindow(void); static int FreeWindow(struct win *wp); static int ShowWindows(void); static int MakeServerSocket(void); static int MakeClientSocket(int err); static int SendCreateMsg(int s, int ac, char **av, int aflag); static int SendErrorMsg(char *fmt,...); static void ReceiveMsg(int s); static int ExecCreate(struct msg *mp); static void ReadRc(char *fn); static int Parse(char *fn, char *buf, char **args); static char **SaveArgs(int argc, char **argv); static int MakeNewEnv(void); static int IsSymbol(char *e, char *s); static char *Filename(char *s); static int IsNum(char *s, int base); static int RemoveUtmp(int slot); static int SetUtmp(char *name); static int InitUtmp(void); static int MoreWindows(void); static int MakeWindow(char *prog, char **args, int aflag, int StartAt, char *dir); static int GetSockName(void); static int Kill(int pid, int sig); static void Attacher(void); static int Attach(int how); static void Detach(int suspend); static int SetTTY(int fd, struct mode *mp); static int GetTTY(int fd, struct mode *mp); static int ShowInfo(void); static void screen_execvpe(char *prog, char **args, char **env); static void WriteFile(int dump); static void KillWindow(struct win **pp); static void Finit(int signum); static int InitKeytab(void); static int enableRawMode(int fd); static void disableRawMode(int fd); static void brktty(void); static void freetty(void); extern char *blank, Term[]; extern int rows, cols; extern int ISO2022; extern int status; extern time_t TimeDisplayed; extern char AnsiVersion[]; extern int flowctl; extern char *MakeTermcap(int aflag); extern char *getlogin(void); static void AttacherFinit(int signum); static void SigChld(int signum); static void SigHup(int signum); static char *MakeBellMsg(int n); static char *GetTtyName(void); static char PtyName[32], TtyName[32]; static char *ShellProg; static char *ShellArgs[2]; static char inbuf[IOSIZE]; static int inlen; static int ESCseen; static int GotSignal; static char DefaultShell[] = "/bin/sh"; static char DefaultPath[] = ":/bin:/usr/bin"; static int TtyMode = 0622; static char SockPath[PATH_MAX]; static char SockDir[] = "scr"; static char *SockNamePtr, *SockName; static int ServerSocket; static char *NewEnv[MAXARGS]; static char Esc = Ctrl('a'); static char MetaEsc = 'a'; static char *home; static int HasWindow; #ifdef UTMP static int utmp, utmpf; static char UtmpName[] = "/etc/utmp"; static char *LoginName; #endif static char *BellString = "Bell in window %"; static int mflag, nflag, fflag, rflag; static char HostName[MAXSTR]; static int Detached; static int AttacherPid; /* Non-Zero in child if we have an attacher */ static int DevTty; #ifdef LOADAV static char KmemName[] = "/dev/kmem"; #ifdef sequent static char UnixName[] = "/dynix"; #else static char UnixName[] = "/vmunix"; #endif #ifdef alliant static char AvenrunSym[] = "_Loadavg"; #else static char AvenrunSym[] = "_avenrun"; #endif static struct nlist nl[2]; static int avenrun, kmemf; #ifdef SUNLOADAV long loadav[3]; #else #ifdef alliant long loadav[4]; #else double loadav[3]; #endif #endif #endif static struct win *curr, *other; static int CurrNum, OtherNum; static struct win *wtab[MAXWIN]; #define KEY_IGNORE 0 #define KEY_HARDCOPY 1 #define KEY_SUSPEND 2 #define KEY_SHELL 3 #define KEY_NEXT 4 #define KEY_PREV 5 #define KEY_KILL 6 #define KEY_REDISPLAY 7 #define KEY_WINDOWS 8 #define KEY_VERSION 9 #define KEY_OTHER 10 #define KEY_0 11 #define KEY_1 12 #define KEY_2 13 #define KEY_3 14 #define KEY_4 15 #define KEY_5 16 #define KEY_6 17 #define KEY_7 18 #define KEY_8 19 #define KEY_9 20 #define KEY_XON 21 #define KEY_XOFF 22 #define KEY_INFO 23 #define KEY_TERMCAP 24 #define KEY_QUIT 25 #define KEY_DETACH 26 #define KEY_CREATE 27 #define KEY_HELP 28 struct key { int type; char **args; } ktab[256]; char *KeyNames[] = { "hardcopy", "suspend", "shell", "next", "prev", "kill", "redisplay", "windows", "version", "other", "select0", "select1", "select2", "select3", "select4", "select5", "select6", "select7", "select8", "select9", "xon", "xoff", "info", "termcap", "quit", "detach", "help", 0 }; static int SetMode(struct mode *op, struct mode *np); static int rawmode = 0; static struct termios orig_termios; int main(int ac, char **av) { int n, len; struct win **pp, *p; char *ap; int s; fd_set r_readfd, w_writefd, e_errfd; int aflag = 0; int lflag = 0; struct timeval tv; time_t now; char buf[IOSIZE], *myname = (ac == 0) ? "screen" : av[0]; char rc[PATH_MAX]; struct stat st; while (ac > 0) { ap = *++av; if (--ac > 0 && *ap == '-') { switch (ap[1]) { case 'c': /* Compatibility with older versions. */ break; case 'a': aflag = 1; break; case 'l': lflag = 1; break; case 'm': mflag = 1; break; case 'n': nflag = 1; break; case 'f': fflag = 1; break; case 'r': rflag = 1; if (ap[2]) { SockName = ap + 2; if (ac != 1) goto help; } else if (--ac == 1) { SockName = *++av; } else if (ac != 0) goto help; break; case 'e': if (ap[2]) { ap += 2; } else { if (--ac == 0) goto help; ap = *++av; } if (strlen(ap) != 2) Msg(0, "Two characters are required with -e option."); Esc = ap[0] - 0x40; /* a control character is difficult * to enter with an editor */ MetaEsc = ap[1]; break; default: help: exit_with_usage(myname); } } else break; } if (nflag && fflag) Msg(0, "-f and -n are conflicting options."); if ((ShellProg = getenv("SHELL")) == 0) ShellProg = DefaultShell; ShellArgs[0] = ShellProg; if (ac == 0) { ac = 1; av = ShellArgs; } if ((home = getenv("HOME")) == 0) Msg(0, "$HOME is undefined."); sprintf(SockPath, "%s/%s", home, SockDir); /* SockDir = "scr" */ if (stat(SockPath, &st) == -1) { if (errno == ENOENT) { if (mkdir(SockPath, 0700) == -1) Msg(errno, "Cannot make directory %s", SockPath); chown(SockPath, getuid(), getgid()); } else { Msg(errno, "Cannot get status of %s", SockPath); } } else { if ((st.st_mode & S_IFMT) != S_IFDIR) Msg(0, "%s is not a directory.", SockPath); if ((st.st_mode & 0777) != 0700) Msg(0, "Directory %s must have mode 700.", SockPath); if (st.st_uid != getuid()) Msg(0, "You are not the owner of %s.", SockPath); } gethostname(HostName, MAXSTR); if ((ap = strchr(HostName, '.'))) *ap = '\0'; strcat(SockPath, "/"); SockNamePtr = SockPath + strlen(SockPath); #if 1 //def BSDJOBS if ((DevTty = open("/dev/tty", O_RDWR | O_NDELAY)) == -1) Msg(errno, "/dev/tty"); #endif if (rflag) { Attach(MSG_ATTACH); Attacher(); /* NOTREACHED */ } if (GetSockName()) { s = MakeClientSocket(1); SendCreateMsg(s, ac, av, aflag); close(s); exit(0); } if (rflag) { switch (fork()) { case -1: Msg(errno, "fork"); /* NOTREACHED */ case 0: break; default: Attacher(); /* NOTREACHED */ } } AttacherPid = getppid(); ServerSocket = s = MakeServerSocket(); InitTerm(); if (enableRawMode(STDIN_FILENO) == -1) return 0; if (fflag) flowctl = 1; else if (nflag) flowctl = 0; MakeNewEnv(); GetTTY(0, &OldMode); InitUtmp(); if (lflag) { Msg(0, "ShellProg: %s, SockPath: %s, HostName: %s, SockName:%s", ShellProg, SockPath, HostName, SockName); } #ifdef LOADAV InitKmem(); #endif signal(SIGHUP, SigHup); signal(SIGINT, Finit); signal(SIGQUIT, Finit); signal(SIGTERM, Finit); #ifdef BSDJOBS signal(SIGTTIN, SIG_IGN); #ifdef SIGTTOU signal(SIGTTOU, SIG_IGN); #endif #endif InitKeytab(); if ((n = MakeWindow(*av, av, aflag, 0, (char *)0)) == -1) { SetTTY(0, &OldMode); FinitTerm(); disableRawMode(STDIN_FILENO); Kill(AttacherPid, SIGHUP); Msg(0, "MakeWindow failed"); exit(1); } sprintf(rc, "%.*s/.screenrc", PATH_MAX - 11, home); /* TODO better limit for PATH_MAX */ ReadRc(rc); /* need to allocate memory for win0 first */ SetCurrWindow(n); HasWindow = 1; SetMode(&OldMode, &NewMode); SetTTY(0, &NewMode); signal(SIGCHLD, SigChld); tv.tv_usec = 0; while (1) { if (status) { time(&now); if (now - TimeDisplayed < MSGWAIT) { tv.tv_sec = MSGWAIT - (now - TimeDisplayed); } else RemoveStatus(curr); } /* * check for I/O on all available I/O descriptors */ FD_ZERO(&r_readfd); FD_ZERO(&w_writefd); FD_ZERO(&e_errfd); if (inlen) FD_SET(curr->ptyfd, &w_writefd); else FD_SET(0, &r_readfd); for (pp = wtab; pp < wtab + MAXWIN; ++pp) { if (!(p = *pp)) continue; if ((*pp)->active && status) continue; if ((*pp)->outlen > 0) continue; FD_SET((*pp)->ptyfd, &r_readfd); } FD_SET(s, &r_readfd); fflush(stdout); if (GotSignal && !status) { SigHandler(); continue; } if ((select(32, &r_readfd, &w_writefd, &e_errfd, (status) ? &tv : (struct timeval *)0)) < 0) { //debug1("Bad select - errno %d\n", errno); #if 0 if (errno != EINTR) { HasWindow = 0; Msg(errno, "select"); Finit(); } else { errno = 0; if ((!GotSignal || status) && !InterruptPlease) continue; } #endif } if (GotSignal && !status) { SigHandler(); continue; } if (FD_ISSET(s, &r_readfd)) { RemoveStatus(curr); ReceiveMsg(s); } if (FD_ISSET(0, &r_readfd)) { RemoveStatus(curr); if (ESCseen) { inbuf[0] = Esc; inlen = read(0, inbuf + 1, IOSIZE - 1) + 1; ESCseen = 0; } else { inlen = read(0, inbuf, IOSIZE); } if (inlen > 0) inlen = ProcessInput(inbuf, inlen); if (inlen > 0) continue; } if (GotSignal && !status) { SigHandler(); continue; } if (FD_ISSET(curr->ptyfd, &w_writefd) && inlen > 0) { if ((len = write(curr->ptyfd, inbuf, inlen)) > 0) { inlen -= len; bcopy(inbuf + len, inbuf, inlen); } } if (GotSignal && !status) { SigHandler(); continue; } for (pp = wtab; pp < wtab + MAXWIN; ++pp) { if (!(p = *pp)) continue; if (p->outlen) { WriteString(p, p->outbuf, p->outlen); } else if (FD_ISSET(p->ptyfd, &r_readfd)) { if ((len = read(p->ptyfd, buf, IOSIZE)) == -1) { if (errno == EWOULDBLOCK) len = 0; } if (len > 0) WriteString(p, buf, len); } if (p->bell) { p->bell = 0; Msg(0, MakeBellMsg(pp - wtab)); } } if (GotSignal && !status) SigHandler(); } /* while (1) */ /* NOTREACHED */ } /* main */ static int SigHandler(void) { while (GotSignal) { GotSignal = 0; DoWait(); } return 0; } static void SigChld(int signum) { GotSignal = 1; } static void SigHup(int signum) { Detach(0); } static int DoWait(void) { int pid; struct win **pp; int wstat; /* removed stuff ELKS does not support */ return 0; } static void KillWindow(struct win **pp) { if (*pp == curr) curr = 0; if (*pp == other) other = 0; FreeWindow(*pp); *pp = 0; } static void CheckWindows(void) { struct win **pp; /* * If the current window disappeared and the "other" window is still * there, switch to the "other" window, else switch to the window with * the lowest index. If there current window is still there, but the * "other" window vanished, "SetCurrWindow" is called in order to assign * a new value to "other". If no window is alive at all, exit. */ if (!curr && other) { SwitchWindow(OtherNum); return; } if (curr && !other) { SetCurrWindow(CurrNum); return; } for (pp = wtab; pp < wtab + MAXWIN; ++pp) { if (*pp) { if (!curr) SwitchWindow(pp - wtab); return; } } Finit(0); } static void Finit(int signum) { struct win *p, **pp; for (pp = wtab; pp < wtab + MAXWIN; ++pp) { if ((p = *pp)) FreeWindow(p); } SetTTY(0, &OldMode); FinitTerm(); disableRawMode(STDIN_FILENO); printf("\r\n[screen is terminating]\n"); Kill(AttacherPid, SIGHUP); exit(0); } static int InitKeytab(void) { int i; ktab['h'].type = ktab[Ctrl('h')].type = KEY_HARDCOPY; ktab['z'].type = ktab[Ctrl('z')].type = KEY_SUSPEND; ktab['c'].type = ktab[Ctrl('c')].type = KEY_SHELL; ktab[' '].type = ktab[Ctrl(' ')].type = ktab['n'].type = ktab[Ctrl('n')].type = KEY_NEXT; ktab['-'].type = ktab['p'].type = ktab[Ctrl('p')].type = KEY_PREV; ktab['k'].type = ktab[Ctrl('k')].type = KEY_KILL; ktab['l'].type = ktab[Ctrl('l')].type = KEY_REDISPLAY; ktab['w'].type = ktab[Ctrl('w')].type = KEY_WINDOWS; ktab['v'].type = ktab[Ctrl('v')].type = KEY_VERSION; ktab['q'].type = ktab[Ctrl('q')].type = KEY_XON; ktab['s'].type = ktab[Ctrl('s')].type = KEY_XOFF; ktab['t'].type = ktab[Ctrl('t')].type = KEY_INFO; ktab['.'].type = KEY_TERMCAP; ktab[Ctrl('\\')].type = ktab['K'].type = KEY_QUIT; ktab['d'].type = ktab[Ctrl('d')].type = KEY_DETACH; ktab['?'].type = KEY_HELP; ktab[Esc].type = KEY_OTHER; for (i = 0; i <= 9; i++) ktab[i + '0'].type = KEY_0 + i; return 0; } static int ProcessInput(char *buf, int len) { int n, k; char *s, *p; struct win **pp; for (s = p = buf; len > 0; len--, s++) { if (*s == Esc) { if (len > 1) { len--; s++; k = ktab[*s].type; if (*s == MetaEsc) { *p++ = Esc; } else if (k >= KEY_0 && k <= KEY_9) { p = buf; SwitchWindow(k - KEY_0); } else switch (ktab[*s].type) { case KEY_TERMCAP: p = buf; WriteFile(0); break; case KEY_HARDCOPY: p = buf; WriteFile(1); break; case KEY_SUSPEND: p = buf; /* Detach (1); */ Msg(0, "Detach disabled"); break; case KEY_SHELL: p = buf; if ((n = MakeWindow(ShellProg, ShellArgs, 0, 0, (char *)0)) != -1) { SwitchWindow(n); DoESC('c', 0); /* clear screen */ ShowWindows(); } break; case KEY_NEXT: p = buf; if (MoreWindows()) SwitchWindow(NextWindow()); break; case KEY_PREV: p = buf; if (MoreWindows()) SwitchWindow(PreviousWindow()); break; case KEY_KILL: p = buf; FreeWindow(wtab[CurrNum]); if (other == curr) other = 0; curr = wtab[CurrNum] = 0; CheckWindows(); ShowWindows(); break; case KEY_QUIT: for (pp = wtab; pp < wtab + MAXWIN; ++pp) if (*pp) FreeWindow(*pp); Finit(0); /* NOTREACHED */ case KEY_DETACH: p = buf; /* Detach (0); */ Msg(0, "Detach disabled"); break; case KEY_REDISPLAY: p = buf; Activate(wtab[CurrNum]); break; case KEY_WINDOWS: p = buf; ShowWindows(); break; case KEY_VERSION: p = buf; Msg(0, "%s %s", ScreenVersion, AnsiVersion); break; case KEY_HELP: display_help(); break; case KEY_INFO: p = buf; ShowInfo(); break; case KEY_OTHER: p = buf; if (MoreWindows()) SwitchWindow(OtherNum); break; case KEY_XON: *p++ = Ctrl('q'); break; case KEY_XOFF: *p++ = Ctrl('s'); break; case KEY_CREATE: p = buf; if ((n = MakeWindow(ktab[*s].args[0], ktab[*s].args, 0, 0, (char *)0)) != -1) { SwitchWindow(n); } break; } } else ESCseen = 1; } else *p++ = *s; } return p - buf; } static void SwitchWindow(int n) { if (!wtab[n]) return; SetCurrWindow(n); Activate(wtab[n]); } static int SetCurrWindow(int n) { /* * If we come from another window, this window becomes the "other" * window: */ if (curr) { curr->active = 0; other = curr; OtherNum = CurrNum; } CurrNum = n; curr = wtab[n]; curr->active = 1; /* * If the "other" window is currently undefined (at program start or * because it has died), or if the "other" window is equal to the one * just selected, we try to find a new one: */ if (other == 0 || other == curr) { OtherNum = NextWindow(); other = wtab[OtherNum]; } return 0; } static int NextWindow(void) { struct win **pp; for (pp = wtab + CurrNum + 1; pp != wtab + CurrNum; ++pp) { if (pp == wtab + MAXWIN) pp = wtab; if (*pp) break; } return pp - wtab; } static int PreviousWindow(void) { struct win **pp; for (pp = wtab + CurrNum - 1; pp != wtab + CurrNum; --pp) { if (pp < wtab) pp = wtab + MAXWIN - 1; if (*pp) break; } return pp - wtab; } static int MoreWindows(void) { struct win **pp; int n; for (n = 0, pp = wtab; pp < wtab + MAXWIN; ++pp) if (*pp) ++n; if (n <= 1) Msg(0, "No other window."); return n > 1; } static int FreeWindow(struct win *wp) { int i; RemoveUtmp(wp->slot); chmod(wp->tty, 0666); chown(wp->tty, 0, 0); close(wp->ptyfd); for (i = 0; i < rows; ++i) { free(wp->image[i]); free(wp->attr[i]); free(wp->font[i]); } free(wp->image); free(wp->attr); free(wp->font); free(wp); return 0; } int MakeWindow(char *prog, char **args, int aflag, int StartAt, char *dir) { struct win **pp, *p; char **cp; int n, f; int tf; //int mypid; char ebuf[10]; pp = wtab + StartAt; do { if (*pp == 0) break; if (++pp == wtab + MAXWIN) pp = wtab; } while (pp != wtab + StartAt); if (*pp) { Msg(0, "No more windows."); return -1; } n = pp - wtab; sprintf(PtyName, "/dev/ptyp%d", n); f = open(PtyName, O_RDWR); sprintf(TtyName, "/dev/ttyp%d", n); if (f == -1) { Msg(0, "No more PTYs."); return -1; } fcntl(f, F_SETFL, O_NDELAY); if ((p = *pp = (struct win *)malloc(sizeof(struct win))) == 0) { nomem: Msg(0, "Out of memory."); return -1; } /* allocate memory */ if ((p->image = (char **)malloc(rows * sizeof(char *))) == 0) goto nomem; for (cp = p->image; cp < p->image + rows; ++cp) { if ((*cp = malloc(cols)) == 0) goto nomem; //bclear(*cp, cols); bzero(*cp, cols); } if ((p->attr = (char **)malloc(rows * sizeof(char *))) == 0) goto nomem; for (cp = p->attr; cp < p->attr + rows; ++cp) { if ((*cp = malloc(cols)) == 0) goto nomem; bzero(*cp, cols); } if ((p->font = (char **)malloc(rows * sizeof(char *))) == 0) goto nomem; if ((p->tabs = malloc(cols + 1)) == 0) /* +1 because 0 <= x <= cols */ goto nomem; #if 0 for (cp = p->font; cp < p->font + rows; ++cp) { if ((*cp = malloc(cols)) == 0) goto nomem; bzero(*cp, cols); } #endif ResetScreen(p); p->aflag = aflag; p->active = 0; p->bell = 0; p->outlen = 0; p->ptyfd = f; strncpy(p->cmd, Filename(args[0]), MAXSTR - 1); p->cmd[MAXSTR - 1] = '\0'; strncpy(p->tty, TtyName, MAXSTR - 1); chown(TtyName, getuid(), getgid()); chmod(TtyName, TtyMode); p->slot = SetUtmp(TtyName); switch (p->wpid = fork()) { case -1: Msg(errno, "fork"); free((char *)p); return -1; case 0: signal(SIGHUP, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGTTIN, SIG_DFL); #ifdef SIGTTOU signal(SIGTTOU, SIG_DFL); #endif setuid(getuid()); setgid(getgid()); if (dir && chdir(dir) == -1) { SendErrorMsg("Cannot chdir to %s: %s", dir, strerror(errno)); exit(1); } //mypid = getpid(); //ioctl(DevTty, TIOCNOTTY, (char *)0); freetty(); if ((tf = open(TtyName, O_RDWR)) == -1) { SendErrorMsg("Cannot open %s: %s", TtyName, strerror(errno)); exit(1); } dup2(tf, 0); dup2(tf, 1); dup2(tf, 2); for (f = OPEN_MAX - 1; f > 2; f--) close(f); //ioctl(0, TIOCSPGRP, &mypid); //setpgrp(0, mypid); //not supported by ELKS - just dummy /* posix setsid() in brktty() from freetty() already made us leader */ SetTTY(0, &OldMode); NewEnv[2] = MakeTermcap(aflag); sprintf(ebuf, "WINDOW=%d", n); NewEnv[3] = ebuf; screen_execvpe(prog, args, NewEnv); SendErrorMsg("Cannot exec %s: %s", prog, strerror(errno)); exit(1); } return n; } static void screen_execvpe(char *prog, char **args, char **env) { char *path, *p; char buf[MAXLINE * 2]; char *shargs[MAXARGS + 1]; int i, eaccess = 0; if (prog[0] == '/') path = ""; else if ((path = getenv("PATH")) == 0) path = DefaultPath; do { p = buf; while (*path && *path != ':') *p++ = *path++; if (p > buf) *p++ = '/'; strcpy(p, prog); if (*path) ++path; execve(buf, args, env); switch (errno) { case ENOEXEC: shargs[0] = DefaultShell; shargs[1] = buf; for ((i = 1); (shargs[i + 1] = args[i]); ++i) ; execve(DefaultShell, shargs, env); return; case EACCES: eaccess = 1; break; case ENOMEM: case E2BIG: case ETXTBSY: return; } } while (*path); if (eaccess) errno = EACCES; } static void WriteFile(int dump) { /* * dump==0: create .termcap, dump==1: hardcopy */ int i, j, k; char *p; FILE *f; char fn[PATH_MAX]; int pid, s; if (dump) sprintf(fn, "hardcopy.%d", CurrNum); else sprintf(fn, "%s/%s/termcap", home, SockDir); switch (pid = fork()) { case -1: Msg(errno, "fork"); return; case 0: setuid(getuid()); setgid(getgid()); if ((f = fopen(fn, "w")) == NULL) exit(1); if (dump) { for (i = 0; i < rows; ++i) { p = curr->image[i]; for (k = cols - 1; k >= 0 && p[k] == ' '; --k); for (j = 0; j <= k; ++j) putc(p[j], f); putc('\n', f); } Msg(0, "Text screen written to file \"%s\".\n\r", fn); } else { if ((p = strchr(MakeTermcap(curr->aflag), '='))) { fputs(++p, f); putc('\n', f); } Msg(0, "Termcap file \"%s\" written to disk.\n\r", fn); } fclose(f); exit(0); default: while ((i = wait(&s)) != pid) if (i == -1) return; if ((s >> 8) & 0377) Msg(0, "Cannot open \"%s\".", fn); else Msg(0, "%s written to \"%s\".", dump ? "Screen image" : "Termcap entry", fn); } } static int ShowWindows(void) { char buf[1024]; char *s; struct win **pp, *p; for (s = buf, pp = wtab; pp < wtab + MAXWIN; ++pp) { if ((p = *pp) == 0) continue; if (s - buf + 5 + strlen(p->cmd) > cols - 1) break; if (s > buf) { *s++ = 'W'; *s++ = 'i'; *s++ = 'n'; *s++ = ':'; } *s++ = pp - wtab + '0'; if (p == curr) *s++ = '*'; /* indicate current window */ else if (p == other) *s++ = ' '; /* blank for other */ *s++ = '-'; strcpy(s, p->cmd); s += strlen(s); strcpy(s, ", "); s += 2; } Msg(0, "Win:%s", buf); return 0; } static int ShowInfo(void) { char buf[1024], *p; struct win *wp = curr; int i; struct tm *tp; time_t now; time(&now); tp = localtime(&now); sprintf(buf, "%2d:%2.2d:%2.2d %s", tp->tm_hour, tp->tm_min, tp->tm_sec, HostName); #ifdef LOADAV if (avenrun && GetAvenrun()) { p = buf + strlen(buf); #ifdef SUNLOADAV sprintf(p, " %2.2f %2.2f %2.2f", (double)loadav[0] / FSCALE, (double)loadav[1] / FSCALE, (double)loadav[2] / FSCALE); #else #ifdef alliant sprintf(p, " %2.2f %2.2f %2.2f %2.2f", (double)loadav[0] / 100, (double)loadav[1] / 100, (double)loadav[2] / 100, (double)loadav[3] / 100); #else sprintf(p, " %2.2f %2.2f %2.2f", loadav[0], loadav[1], loadav[2]); #endif #endif } #endif p = buf + strlen(buf); sprintf(p, " (%d,%d) %cflow %cins %corg %cwrap %cpad", wp->y, wp->x, flowctl ? '+' : '-', wp->insert ? '+' : '-', wp->origin ? '+' : '-', wp->wrap ? '+' : '-', wp->keypad ? '+' : '-'); if (ISO2022) { p = buf + strlen(buf); sprintf(p, " G%1d [", wp->LocalCharset); for (i = 0; i < 4; i++) p[i + 5] = wp->charsets[i] ? wp->charsets[i] : 'B'; p[9] = ']'; p[10] = '\0'; } Msg(0, buf); return 0; } static int SetTTY(int fd, struct mode *mp) { #ifdef BSD_TTY ioctl(fd, TIOCSETP, &mp->m_ttyb); ioctl(fd, TIOCSETC, &mp->m_tchars); ioctl(fd, TIOCSLTC, &mp->m_ltchars); ioctl(fd, TIOCLSET, &mp->m_lmode); ioctl(fd, TIOCSETD, &mp->m_ldisc); #endif return 0; } static int GetTTY(int fd, struct mode *mp) { #ifdef BSD_TTY ioctl(fd, TIOCGETP, &mp->m_ttyb); ioctl(fd, TIOCGETC, &mp->m_tchars); ioctl(fd, TIOCGLTC, &mp->m_ltchars); ioctl(fd, TIOCLGET, &mp->m_lmode); ioctl(fd, TIOCGETD, &mp->m_ldisc); #endif return 0; } static int SetMode(struct mode *op, struct mode *np) { *np = *op; #ifdef BSD_TTY np->m_ttyb.sg_flags &= ~(CRMOD | ECHO); np->m_ttyb.sg_flags |= CBREAK; np->m_tchars.t_intrc = -1; np->m_tchars.t_quitc = -1; if (!flowctl) { np->m_tchars.t_startc = -1; np->m_tchars.t_stopc = -1; } np->m_ltchars.t_suspc = -1; np->m_ltchars.t_dsuspc = -1; np->m_ltchars.t_flushc = -1; np->m_ltchars.t_lnextc = -1; #endif return 0; } static char * GetTtyName(void) { char *p; int n; for (p = 0, n = 0; n <= 2 && !(p = ttyname(n)); n++) ; if (!p || *p == '\0') Msg(0, "screen must run on a tty."); return p; } static int Attach(int how) { int s, lasts = 0, found = 0; DIR *dirp; struct dirent *dp; struct msg m; char last[MAXNAMLEN + 1]; if (SockName) { if ((lasts = MakeClientSocket(0)) == -1) { if (how == MSG_CONT) Msg(0, "This screen has already been continued from elsewhere."); else Msg(0, "There is no screen to be resumed from %s.", SockName); } } else { if ((dirp = opendir(SockPath)) == NULL) Msg(0, "Cannot open %s", SockPath); while ((dp = readdir(dirp)) != NULL) { SockName = dp->d_name; if (SockName[0] == '.') /* . and .. */ continue; if ((s = MakeClientSocket(0)) != -1) { if (found == 0) { strcpy(last, SockName); lasts = s; } else { if (found == 1) { printf("There are detached screens on:\n"); printf(" %s\n", last); close(lasts); } printf(" %s\n", SockName); close(s); } found++; } } if (found == 0) Msg(0, "There is no screen to be resumed."); if (found > 1) Msg(0, "Type \"screen -r ttyX.scr\" to resume one of them."); closedir(dirp); strcpy(SockNamePtr, last); SockName = SockNamePtr; } m.type = how; strcpy(m.m.attach.tty, GetTtyName()); m.m.attach.apid = getpid(); if (write(lasts, (char *)&m, sizeof(m)) != sizeof(m)) Msg(errno, "write"); return 0; } static void AttacherFinit(int signum) { exit(0); } static void ReAttach(int signum) { Attach(MSG_CONT); } static void pause(void) { select(1, NULL, NULL, NULL, NULL); } static void Attacher(void) { signal(SIGHUP, AttacherFinit); signal(SIGCONT, ReAttach); while (1) pause(); } static void Detach(int suspend) { struct win **pp; if (Detached) return; signal(SIGHUP, SIG_IGN); SetTTY(0, &OldMode); FinitTerm(); disableRawMode(STDIN_FILENO); if (suspend) { Kill(AttacherPid, SIGTSTP); } else { for (pp = wtab; pp < wtab + MAXWIN; ++pp) if (*pp) RemoveUtmp((*pp)->slot); printf("\n[detached]\n"); Kill(AttacherPid, SIGHUP); AttacherPid = 0; } close(0); close(1); close(2); //ioctl(DevTty, TIOCNOTTY, (char *)0); freetty(); Detached = 1; do { ReceiveMsg(ServerSocket); } while (Detached); if (!suspend) for (pp = wtab; pp < wtab + MAXWIN; ++pp) if (*pp) (*pp)->slot = SetUtmp((*pp)->tty); signal(SIGHUP, SigHup); } static int Kill(int pid, int sig) { if (pid != 0) kill(pid, sig); return 0; } static int GetSockName(void) { int client; static char buf[2 * MAXSTR]; if (!mflag && (SockName = getenv("STY")) != 0 && *SockName != '\0') { client = 1; setuid(getuid()); setgid(getgid()); } else { sprintf(buf, "%s.scr", Filename(GetTtyName())); SockName = buf; client = 0; } return client; } static int MakeServerSocket(void) { int s; struct sockaddr_un a; char *p; if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) Msg(errno, "socket"); a.sun_family = AF_UNIX; strcpy(SockNamePtr, SockName); strcpy(a.sun_path, SockPath); if (connect(s, (struct sockaddr *)&a, strlen(SockPath) + 1) != -1) { p = Filename(SockPath); Msg(0, "You have already a screen running on %s.\n\ If it has been detached, try \"screen -r\".", p); /* NOTREACHED */ } unlink(SockPath); if (bind(s, (struct sockaddr *)&a, strlen(SockPath) + 1) == -1) Msg(errno, "bind"); chown(SockPath, getuid(), getgid()); if (listen(s, 5) == -1) Msg(errno, "listen"); return s; } static int MakeClientSocket(int err) { int s; struct sockaddr_un a; if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) Msg(errno, "socket"); a.sun_family = AF_UNIX; strcpy(SockNamePtr, SockName); strcpy(a.sun_path, SockPath); if (connect(s, (struct sockaddr *)&a, strlen(SockPath) + 1) == -1) { if (err) { Msg(errno, "connect: %s", SockPath); } else { close(s); return -1; } } return s; } static int SendCreateMsg(int s, int ac, char **av, int aflag) { struct msg m; char *p; int len, n; m.type = MSG_CREATE; p = m.m.create.line; for (n = 0; ac > 0 && n < MAXARGS - 1; ++av, --ac, ++n) { len = strlen(*av) + 1; if (p + len >= m.m.create.line + MAXLINE) break; strcpy(p, *av); p += len; } m.m.create.nargs = n; m.m.create.aflag = aflag; if (getcwd(m.m.create.dir, sizeof(m.m.create.dir)) == 0) Msg(0, "%s", m.m.create.dir); if (write(s, (char *)&m, sizeof(m)) != sizeof(m)) Msg(errno, "write"); return 0; } static int SendErrorMsg(char *fmt,...) { int s; struct msg m; s = MakeClientSocket(1); m.type = MSG_ERROR; static va_list ap; va_start(ap, fmt); vsprintf(m.m.message, fmt, ap); va_end(ap); write(s, (char *)&m, sizeof(m)); close(s); sleep(1); return 0; } static void ReceiveMsg(int s) { int ns; struct sockaddr_un a; int left, len = sizeof(a); struct msg m; char *p; if ((ns = accept(s, (struct sockaddr *)&a, (void *)&len)) == -1) { Msg(errno, "accept"); return; } p = (char *)&m; left = sizeof(m); while (left > 0 && (len = read(ns, p, left)) > 0) { p += len; left -= len; } close(ns); if (len == -1) Msg(errno, "read"); if (left > 0) return; switch (m.type) { case MSG_CREATE: if (!Detached) ExecCreate(&m); break; case MSG_CONT: if (m.m.attach.apid != AttacherPid || !Detached) break; /* Intruder Alert */ /* FALLTHROUGH */ case MSG_ATTACH: if (Detached) { if (kill(m.m.attach.apid, 0) == 0 && open(m.m.attach.tty, O_RDWR) == 0) { dup(0); dup(0); AttacherPid = m.m.attach.apid; Detached = 0; GetTTY(0, &OldMode); SetMode(&OldMode, &NewMode); SetTTY(0, &NewMode); Activate(wtab[CurrNum]); } } else { Kill(m.m.attach.apid, SIGHUP); Msg(0, "Not detached."); } break; case MSG_ERROR: Msg(0, "%s", m.m.message); break; default: Msg(0, "Invalid message (type %d).", m.type); } } static int ExecCreate(struct msg *mp) { char *args[MAXARGS]; int n; char **pp = args, *p = mp->m.create.line; for (n = mp->m.create.nargs; n > 0; --n) { *pp++ = p; p += strlen(p) + 1; } *pp = 0; if ((n = MakeWindow(mp->m.create.line, args, mp->m.create.aflag, 0, mp->m.create.dir)) != -1) SwitchWindow(n); return 0; } static void ReadRc(char *fn) { FILE *f; char *p, **pp, **ap; int argc, num, c; char buf[256]; char *args[MAXARGS]; int key; ap = args; if (access(fn, R_OK) == -1) return; if ((f = fopen(fn, "r")) == NULL) return; while (fgets(buf, 256, f) != NULL) { if ((p = strrchr(buf, '\n'))) *p = '\0'; if ((argc = Parse(fn, buf, ap)) == 0) continue; if (strcmp(ap[0], "escape") == 0) { p = ap[1]; if (argc < 2 || strlen(p) != 2) Msg(0, "%s: two characters required after escape.", fn); Esc = *p++ - 0x40; /* a control character is difficult to enter * with an editor */ MetaEsc = *p; } else if (strcmp(ap[0], "chdir") == 0) { p = argc < 2 ? home : ap[1]; if (chdir(p) == -1) Msg(errno, "%s", p); } else if (strcmp(ap[0], "mode") == 0) { if (argc != 2) { Msg(0, "%s: mode: one argument required.", fn); } else if (!IsNum(ap[1], 7)) { Msg(0, "%s: mode: octal number expected.", fn); } else sscanf(ap[1], "%o", &TtyMode); } else if (strcmp(ap[0], "bell") == 0) { if (argc != 2) { Msg(0, "%s: bell: one argument required.", fn); } else { if ((BellString = malloc(strlen(ap[1]) + 1)) == 0) Msg(0, "Out of memory."); strcpy(BellString, ap[1]); } } else if (strcmp(ap[0], "screen") == 0) { num = 0; if (argc > 1 && IsNum(ap[1], 10)) { num = atoi(ap[1]); if (num < 0 || num > MAXWIN - 1) Msg(0, "%s: illegal screen number %d.", fn, num); --argc; ++ap; } if (argc < 2) { ap[1] = ShellProg; argc = 2; } ap[argc] = 0; MakeWindow(ap[1], ap + 1, 0, num, (char *)0); } else if (strcmp(ap[0], "bind") == 0) { p = ap[1]; if (argc < 2 || *p == '\0') Msg(0, "%s: key expected after bind.", fn); if (p[1] == '\0') { key = *p; } else if (p[0] == '^' && p[1] != '\0' && p[2] == '\0') { c = p[1]; if (isupper(c)) p[1] = tolower(c); key = Ctrl(c); } else if (IsNum(p, 7)) { sscanf(p, "%o", &key); } else { Msg(0, "%s: bind: character, ^x, or octal number expected.", fn); } if (argc < 3) { ktab[key].type = 0; } else { for (pp = KeyNames; *pp; ++pp) if (strcmp(ap[2], *pp) == 0) break; if (*pp) { ktab[key].type = pp - KeyNames + 1; } else { ktab[key].type = KEY_CREATE; ktab[key].args = SaveArgs(argc - 2, ap + 2); } } } else Msg(0, "%s: unknown keyword \"%s\".", fn, ap[0]); } fclose(f); } static int Parse(char *fn, char *buf, char **args) { char *p = buf, **ap = args; int delim, argc = 0; argc = 0; for (;;) { while (*p && (*p == ' ' || *p == '\t')) ++p; if (*p == '\0' || *p == '#') return argc; if (argc > MAXARGS - 1) Msg(0, "%s: too many tokens.", fn); delim = 0; if (*p == '"' || *p == '\'') { delim = *p; *p = '\0'; ++p; } ++argc; *ap = p; ++ap; while (*p && !(delim ? *p == delim : (*p == ' ' || *p == '\t'))) ++p; if (*p == '\0') { if (delim) Msg(0, "%s: Missing quote.", fn); else return argc; } *p++ = '\0'; } } static char ** SaveArgs(int argc, char **argv) { char **ap, **pp; if ((pp = ap = (char **)malloc((argc + 1) * sizeof(char **))) == 0) Msg(0, "Out of memory."); while (argc--) { if ((*pp = malloc(strlen(*argv) + 1)) == 0) Msg(0, "Out of memory."); strcpy(*pp, *argv); ++pp; ++argv; } *pp = 0; return ap; } static int MakeNewEnv(void) { char **op, **np = NewEnv; static char buf[MAXSTR]; if (strlen(SockName) > MAXSTR - 5) SockName = "?"; sprintf(buf, "STY=%s", SockName); *np++ = buf; *np++ = Term; np += 2; for (op = environ; *op; ++op) { if (np == NewEnv + MAXARGS - 1) break; if (!IsSymbol(*op, "TERM") && !IsSymbol(*op, "TERMCAP") && !IsSymbol(*op, "STY")) *np++ = *op; } *np = 0; return 0; } static int IsSymbol(char *e, char *s) { char *p; int n; for (p = e; *p && *p != '='; ++p); if (*p) { *p = '\0'; n = strcmp(e, s); *p = '='; return n == 0; } return 0; } void Msg(int err, char *fmt,...) { static va_list ap; char buf[1024]; char *p = buf; va_start(ap, fmt); vsprintf(p, fmt, ap); va_end(ap); if (err) { p += strlen(p); sprintf(p, ": Error %d, %s", err, strerror(err)); } if (HasWindow) { MakeStatus(buf, curr); } else { printf("%s\r\n", buf); } } int bclear(char *p, int n) { bcopy(blank, p, n); return 0; } static char * Filename(char *s) { char *p; p = s + strlen(s) - 1; while (p >= s && *p != '/') --p; return ++p; } static int IsNum(char *s, int base) { for (base += '0'; *s; ++s) if (*s < '0' || *s > base) return 0; return 1; } static char * MakeBellMsg(int n) { static char buf[MAXSTR]; char *p = buf, *s; for (s = BellString; *s && p < buf + MAXSTR - 1; s++) *p++ = (*s == '%') ? n + '0' : *s; *p = '\0'; return buf; } #ifdef UTMP static int InitUtmp(void) { struct passwd *p; if ((utmpf = open(UtmpName, O_WRONLY)) == -1) { if (errno != EACCES) Msg(errno, UtmpName); return; } if ((LoginName = getlogin()) == 0 || LoginName[0] == '\0') { if ((p = getpwuid(getuid())) == 0) return; LoginName = p->pw_name; } utmp = 1; } static int SetUtmp(char *name) { char *p; struct ttyent *tp; int slot = 1; struct utmp u; if (!utmp) return 0; if (p = strrchr(name, '/')) ++p; else p = name; setttyent(); while ((tp = getttyent()) != NULL && strcmp(p, tp->ty_name) != 0) ++slot; if (tp == NULL) return 0; strncpy(u.ut_line, p, 8); strncpy(u.ut_user, LoginName, 8); u.ut_host[0] = '\0'; time(&u.ut_time); lseek(utmpf, (long)(slot * sizeof(u)), 0); write(utmpf, (char *)&u, sizeof(u)); return slot; } static int RemoveUtmp(int slot) { struct utmp u; if (slot) { bzero((char *)&u, sizeof(u)); lseek(utmpf, (long)(slot * sizeof(u)), 0); write(utmpf, (char *)&u, sizeof(u)); } } #else static int InitUtmp(void) { return 0; } static int SetUtmp(char *name) { return 0; } static int RemoveUtmp(int slot) { return 0; } #endif #ifndef GETTTYENT static void setttyent(void) { struct stat s; int f; char *p, *ep; if (ttnext) { ttnext = tt; return; } if ((f = open(ttys, O_RDONLY)) == -1 || fstat(f, &s) == -1) Msg(errno, ttys); if ((tt = malloc(s.st_size + 1)) == 0) Msg(0, "Out of memory."); if (read(f, tt, s.st_size) != s.st_size) Msg(errno, ttys); close(f); for (p = tt, ep = p + s.st_size; p < ep; ++p) if (*p == '\n') *p = '\0'; *p = '\0'; ttnext = tt; } static struct ttyent * getttyent(void) { static struct ttyent t; if (*ttnext == '\0') return NULL; t.ty_name = ttnext + 2; ttnext += strlen(ttnext) + 1; return &t; } #endif #ifdef LOADAV static int InitKmem(void) { if ((kmemf = open(KmemName, O_RDONLY)) == -1) return; nl[0].n_name = AvenrunSym; nlist(UnixName, nl); if (nl[0].n_type == 0 || nl[0].n_value == 0) return; avenrun = 1; } static int GetAvenrun(void) { if (lseek(kmemf, nl[0].n_value, 0) == -1) return 0; if (read(kmemf, loadav, sizeof(loadav)) != sizeof(loadav)) return 0; return 1; } #endif #ifndef USEBCOPY #if 0 bcopy(s1, s2, len) register char *s1, *s2; register len; { if (s1 < s2 && s2 < s1 + len) { s1 += len; s2 += len; while (len-- > 0) { --s2 = *--s1; } } else { while (len-- > 0) { s2++ = *s1++; } } } #endif #endif int gethostname(char *host, size_t size) { strncpy(host, "elks", size); host[size - 1] = '\0'; return 0; } static int enableRawMode(int fd) { struct termios raw; if (!isatty(STDIN_FILENO)) goto fatal; #if 0 if (!atexit_registered) { atexit(linenoiseAtExit); atexit_registered = 1; } #endif if (tcgetattr(fd, &orig_termios) == -1) goto fatal; raw = orig_termios; /* modify the original mode */ /* * input modes: no break, no CR to NL, no parity check, no strip char, no * start/stop output control. */ raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); /* output modes - disable post processing */ raw.c_oflag &= ~(OPOST); /* control modes - set 8 bit chars */ raw.c_cflag |= (CS8); /* * local modes - echo nothing, canonical off, no extended functions, no * signal chars (^Z,^C) */ raw.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG); /* * control chars - set return condition: min number of bytes and timer. * We want read to return every single byte, without timeout. */ raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ /* put terminal in raw mode after flushing */ if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) goto fatal; rawmode = 1; return 0; fatal: //errno = ENOTTY; return -1; } static void disableRawMode(int fd) { /* Don't even check the return value as it's too late. */ if (rawmode && tcsetattr(fd, TCSAFLUSH, &orig_termios) != -1) rawmode = 0; } static void brktty(void) { setsid(); /* will break terminal affiliation */ } static void freetty(void) { brktty(); close(0); close(1); close(2); } void exit_with_usage(char *myname) { printf("Use: %s [-opts] [cmd [args]]\n", myname); printf(" or: %s -r [ttyX.scr]\n\nOptions:\n", myname); printf("-a Force all capabilities into each window's termcap\n"); printf("-e xy Change command characters\n"); printf("-f Flow control on\n"); printf("-n Flow control off\n"); printf("-l Do nothing, just list the SockDir\n"); /* printf("-r Reattach to a detached screen process\n"); */ exit(1); } int display_help(void) { /* * Ctrl+a d Detach from the current screen session, and leave it * running.\n\r\ Use screen -r to resume\n\r\ */ printf("\n\r\ Ctrl+a ? Display available commands\n\r\ Ctrl+a v Show the version\n\r\ Ctrl+a w Window list\n\r\ Ctrl+a 0-9 Open window 0-9\n\r\ Ctrl+a c Create a new window (with shell)\n\r\ Ctrl-a n Switch to the Next window\n\r\ Ctrl-a p Switch to the Previous window\n\r\ Ctrl+a l Fully refresh current window\n\r\ Ctrl+a k Kill (Destroy) the current window.\n\r\ Ctrl-a K Kill all windows and terminate screen(quit)\n\r\ Ctrl-a t Show the load average\n\r\ Ctrl-a . Write out a .termcap file\n\r"); return 0; } void dbgmsg(int n) { printf("dbg:%d\n", n); fflush(stdout); sleep(1); }
Commit message
This repository is read-only for this account.
Repository snapshot
Current branch
master
Visibility
public
Your access
Read
Remote
Configured
File activity
View file history