mirror of https://github.com/mabam/CAP.git
692 lines
14 KiB
C
692 lines
14 KiB
C
/*
|
|
* Copyright (c) 1990, The Regents of the University of California.
|
|
* Edward Moy, Workstation Software Support Group, Workstation Support Services,
|
|
* Information Systems and Technology.
|
|
*
|
|
* Permission is granted to any individual or institution to use, copy,
|
|
* or redistribute this software so long as it is not sold for profit,
|
|
* provided that this notice and the original copyright notices are
|
|
* retained. The University of California makes no representations about the
|
|
* suitability of this software for any purpose. It is provided "as is"
|
|
* without express or implied warranty.
|
|
*/
|
|
|
|
#include <netat/appletalk.h>
|
|
#ifdef NEEDFCNTLDOTH
|
|
#include <fcntl.h>
|
|
#endif NEEDFCNTLDOTH
|
|
#include <sys/param.h>
|
|
#ifndef _TYPES
|
|
# include <sys/types.h> /* sometimes needed for sys/file.h */
|
|
#endif _TYPES
|
|
#include <sys/file.h>
|
|
#ifdef USEDIRENT
|
|
#include <dirent.h>
|
|
#else USEDIRENT
|
|
#include <sys/dir.h>
|
|
#endif USEDIRENT
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/ioctl.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#ifdef USESTRINGDOTH
|
|
#include <string.h>
|
|
#else USESTRINGDOTH
|
|
#include <strings.h>
|
|
#endif USESTRINGDOTH
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
|
|
#include <netinet/in.h>
|
|
|
|
#define ASYNC 1
|
|
#define BDSSIZE 512
|
|
#define BITMASK ((1 << NBITSPERFIELD) - 1)
|
|
#define HOLDTIME 60
|
|
#define MACID 4
|
|
#define MORESIZE (sizeof(moremsg) - 1)
|
|
#define NBDS 8
|
|
#define NBITSPERFIELD (NUSERBITS / (NFIELDS - 1))
|
|
#define NFIELDS 6
|
|
#define NUSERBITS 32
|
|
#define SYNC 0
|
|
|
|
struct Queue {
|
|
time_t q_time; /* modification time */
|
|
char q_name[MAXNAMLEN+1]; /* control file name */
|
|
};
|
|
|
|
AddrBlock addr;
|
|
ABusRecord abrecord;
|
|
BDS bds[NBDS];
|
|
char bdsbuffer[NBDS * BDSSIZE]; /* bds buffer */
|
|
int bdsfull; /* bds buffer full */
|
|
char *bdsptr; /* current position in bds buffer */
|
|
int colwidth[NFIELDS]; /* column widths */
|
|
char current[40]; /* current file being printed */
|
|
char empty[] = ""; /* empty string */
|
|
EntityName entity; /* Entity name of register naming */
|
|
int holdtime = HOLDTIME; /* minimum time between looking at queue */
|
|
long lasttime; /* last time queue looked at */
|
|
char line[BUFSIZ]; /* line buffer */
|
|
char *lock = "lock"; /* lock file name */
|
|
char moremsg[] = " (More ...)\r";
|
|
char *myname; /* name of program */
|
|
char *printer; /* name of LW spooler */
|
|
int rank; /* order to be printed (-1=none, 0=active) */
|
|
struct stat statbuf;
|
|
char *status = "status"; /* status file name */
|
|
char temp[512]; /* temp buffer for bds */
|
|
char type[] = "Printer Queue"; /* AppleTalk type */
|
|
char zone[] = "*"; /* AppleTalk zone */
|
|
|
|
char *pgetstr();
|
|
char *malloc();
|
|
|
|
main(argc, argv)
|
|
int argc;
|
|
char **argv;
|
|
{
|
|
register ABusRecord *abr = &abrecord;
|
|
register OSErr err;
|
|
register int i, j, k;
|
|
int sock;
|
|
long l;
|
|
void cleanup();
|
|
|
|
if(myname = rindex(*argv, '/'))
|
|
myname++;
|
|
else
|
|
myname = *argv;
|
|
for(argc--, argv++ ; argc > 0 && **argv == '-' ; argc--, argv++) {
|
|
switch((*argv)[1]) {
|
|
case 'l': /* alternate lock file */
|
|
if((*argv)[2])
|
|
lock = &(*argv)[2];
|
|
else if(argc < 2)
|
|
Usage(); /* never returns */
|
|
else {
|
|
argc--;
|
|
lock = *++argv;
|
|
}
|
|
break;
|
|
case 's': /* alternate status file */
|
|
if((*argv)[2])
|
|
status = &(*argv)[2];
|
|
else if(argc < 2)
|
|
Usage(); /* never returns */
|
|
else {
|
|
argc--;
|
|
status = *++argv;
|
|
}
|
|
break;
|
|
case 't': /* new hold time */
|
|
if((*argv)[2])
|
|
holdtime = atoi(&(*argv)[2]);
|
|
else if(argc < 2)
|
|
Usage(); /* never returns */
|
|
else {
|
|
argc--;
|
|
holdtime = atoi(*++argv);
|
|
}
|
|
if(holdtime < 0)
|
|
Usage();
|
|
break;
|
|
default:
|
|
Usage(); /* never returns */
|
|
}
|
|
}
|
|
if(argc < 2)
|
|
Usage(); /* never returns */
|
|
printer = *argv++;
|
|
if (chdir(*argv) < 0)
|
|
fatal("cannot chdir to spooling directory");
|
|
disassociate();
|
|
for(i = 0 ; i < NBDS ; i++)
|
|
bds[i].buffPtr = &bdsbuffer[i * BDSSIZE];
|
|
signal(SIGHUP, cleanup);
|
|
signal(SIGQUIT, cleanup);
|
|
signal(SIGINT, cleanup);
|
|
signal(SIGTERM, cleanup);
|
|
abInit(TRUE);
|
|
nbpInit();
|
|
sock = 0;
|
|
if((err = ATPOpenSocket(&addr, &sock)) != noErr)
|
|
fatal("Error opening ATP socket (%d)\n", err);
|
|
if((err = nbp_register(printer, type, zone, sock)) != noErr)
|
|
fatal("Error registering name (%d)\n", err);
|
|
for( ; ; ) {
|
|
abr->proto.atp.atpSocket = sock;
|
|
abr->proto.atp.atpReqCount = BDSSIZE;
|
|
abr->proto.atp.atpDataPtr = line;
|
|
if((err = ATPGetRequest(abr, SYNC)) != noErr)
|
|
fatal("Error setting up ATPGetRequest (%d)\n", err);
|
|
if(abr->abResult != 1) {
|
|
time(&l);
|
|
if(l - lasttime >= holdtime) {
|
|
lasttime = l;
|
|
bdsptr = bdsbuffer;
|
|
bdsfull = FALSE;
|
|
tobds("\"%s\" Printer Queue %s",
|
|
printer, ctime(&lasttime));
|
|
displayq();
|
|
*bdsptr = 0;
|
|
k = bdsptr - bdsbuffer;
|
|
abr->proto.atp.atpNumBufs =
|
|
abr->proto.atp.atpBDSSize = j =
|
|
(k + (BDSSIZE - 1)) / BDSSIZE;
|
|
for(i = 0 ; i < j ; i++) {
|
|
bds[i].buffSize = k > BDSSIZE ?
|
|
BDSSIZE : k;
|
|
k -= BDSSIZE;
|
|
}
|
|
l = 0;
|
|
for(i = (NFIELDS - 2) ; i >= 0 ; i--)
|
|
l = (l << NBITSPERFIELD) |
|
|
(colwidth[i] > BITMASK ? BITMASK :
|
|
colwidth[i]);
|
|
/*
|
|
* We shouldn't have to do this, but we do!
|
|
*/
|
|
bds[0].userBytes = htonl(l);
|
|
}
|
|
abr->proto.atp.fatpEOM = TRUE;
|
|
abr->proto.atp.atpRspBDSPtr = bds;
|
|
if((err = ATPSndRsp(abr, SYNC)) != noErr)
|
|
fprintf(stderr,
|
|
"%s: Error sending response (%d)\n",
|
|
myname, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
disassociate()
|
|
{
|
|
int i;
|
|
|
|
if (fork())
|
|
_exit(0); /* kill parent */
|
|
for (i=0; i < 3; i++) close(i); /* kill */
|
|
(void)open("/",0);
|
|
(void)dup2(0,1);
|
|
(void)dup2(0,2);
|
|
#ifndef POSIX
|
|
#ifdef TIOCNOTTY
|
|
if ((i = open("/dev/tty",2)) > 0) {
|
|
(void)ioctl(i, TIOCNOTTY, (caddr_t)0);
|
|
(void)close(i);
|
|
}
|
|
#endif TIOCNOTTY
|
|
#else POSIX
|
|
(void) setsid();
|
|
#endif POSIX
|
|
}
|
|
|
|
void
|
|
cleanup()
|
|
{
|
|
NBPRemove(&entity);
|
|
exit(1);
|
|
}
|
|
|
|
Usage()
|
|
{
|
|
fprintf(stderr,
|
|
"Usage: %s [-l lockfile] [-s statusfile] [-t holdtime] name spooldir\n",
|
|
myname);
|
|
exit(1);
|
|
}
|
|
|
|
/*
|
|
* Put unformated text into BDS buffers.
|
|
*/
|
|
writebds(buf, size)
|
|
char *buf;
|
|
{
|
|
bcopy(buf, bdsptr, size);
|
|
bdsptr += size;
|
|
}
|
|
|
|
/*
|
|
* Put formatted text into BDS buffers.
|
|
*/
|
|
tobds(fmt, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
|
|
char *fmt;
|
|
{
|
|
register int len, bdslen;
|
|
|
|
if(bdsfull)
|
|
return;
|
|
sprintf(temp, fmt, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
|
|
arg8, arg9);
|
|
if((bdslen = bdsptr - bdsbuffer) + (len = strlen(temp)) >
|
|
NBDS * BDSSIZE) {
|
|
bdsfull = TRUE;
|
|
while(NBDS * BDSSIZE - bdslen < MORESIZE) {
|
|
if(*--bdsptr != '\r')
|
|
bdsptr++;
|
|
while(*--bdsptr != '\r')
|
|
{}
|
|
bdsptr++;
|
|
bdslen = bdsptr - bdsbuffer;
|
|
}
|
|
strcpy(bdsptr, moremsg);
|
|
bdsptr += MORESIZE;
|
|
return;
|
|
}
|
|
strcpy(bdsptr, temp);
|
|
bdsptr += len;
|
|
}
|
|
|
|
/*
|
|
* Display the current state of the queue. Format = 1 if long format.
|
|
*/
|
|
displayq()
|
|
{
|
|
register struct Queue *q;
|
|
register int i, nitems, fd;
|
|
struct Queue **Queue;
|
|
FILE *fp;
|
|
|
|
rank = -1;
|
|
for(i = 0 ; i < (NFIELDS - 1) ; i++)
|
|
colwidth[i] = 0;
|
|
if ((nitems = Getq(&Queue)) < 0)
|
|
fatal("cannot examine spooling area\n");
|
|
if (stat(lock, &statbuf) >= 0) {
|
|
if (statbuf.st_mode & 0100) {
|
|
tobds("Warning: \"%s\" is down: ", printer);
|
|
fd = open(status, O_RDONLY);
|
|
if (fd >= 0) {
|
|
OSLockFileforRead(fd);
|
|
while ((i = read(fd, line, BUFSIZ)) > 0)
|
|
writebds(line, i);
|
|
OSUnlockFile(fd);
|
|
(void) close(fd);
|
|
} else
|
|
writebds("\n", 1);
|
|
}
|
|
if (statbuf.st_mode & 010)
|
|
tobds("Warning: \"%s\" queue is turned off\n", printer);
|
|
}
|
|
|
|
if (nitems) {
|
|
fp = fopen(lock, "r");
|
|
if (fp == NULL)
|
|
warn();
|
|
else {
|
|
register char *cp;
|
|
|
|
/* get daemon pid */
|
|
cp = current;
|
|
while ((*cp = getc(fp)) != EOF && *cp != '\n')
|
|
cp++;
|
|
*cp = '\0';
|
|
i = atoi(current);
|
|
if (i <= 0 || kill(i, 0) < 0)
|
|
warn();
|
|
else {
|
|
/* read current file name */
|
|
cp = current;
|
|
while ((*cp = getc(fp)) != EOF && *cp != '\n')
|
|
cp++;
|
|
*cp = '\0';
|
|
/*
|
|
* Print the status file.
|
|
*/
|
|
fd = open(status, O_RDONLY);
|
|
if (fd >= 0) {
|
|
OSLockFileforRead(fd);
|
|
while ((i = read(fd, line, BUFSIZ)) > 0)
|
|
writebds(line, i);
|
|
OSUnlockFile(fd);
|
|
(void) close(fd);
|
|
} else
|
|
writebds("\n", 1);
|
|
}
|
|
(void) fclose(fp);
|
|
}
|
|
/*
|
|
* Now, examine the control files and print out the jobs to
|
|
* be done for each user.
|
|
*/
|
|
for (i = 0; i < nitems; i++) {
|
|
q = Queue[i];
|
|
inform(q);
|
|
free(q);
|
|
}
|
|
free(Queue);
|
|
} else if (nitems == 0)
|
|
tobds("no entries\n");
|
|
}
|
|
|
|
/*
|
|
* Print a warning message if there is no daemon present.
|
|
*/
|
|
warn()
|
|
{
|
|
tobds("Warning: print spooler not running\n");
|
|
current[0] = '\0';
|
|
}
|
|
|
|
inform(q)
|
|
register struct Queue *q;
|
|
{
|
|
register int copies, pc;
|
|
register char *cp;
|
|
register long size;
|
|
int mac;
|
|
FILE *cfp;
|
|
char *alias;
|
|
char *class;
|
|
char cstr[128]; /* 'H' */
|
|
char fstr[128]; /* [a-z] */
|
|
char jstr[128]; /* 'J' */
|
|
char nstr[128]; /* 'N' */
|
|
char ustr[128]; /* 'P' */
|
|
char *job;
|
|
char *user;
|
|
|
|
/*
|
|
* There's a chance the control file has gone away
|
|
* in the meantime; if this is the case just keep going
|
|
*/
|
|
if ((cfp = fopen(q->q_name, "r")) == NULL)
|
|
return;
|
|
|
|
alias = class = user = job = empty;
|
|
*cstr = *fstr = *jstr = *nstr = *ustr = '\0';
|
|
pc = -1;
|
|
size = -1L;
|
|
mac = FALSE;
|
|
if (rank < 0)
|
|
rank = 0;
|
|
if (strcmp(q->q_name, current) != 0)
|
|
rank++;
|
|
copies = 0;
|
|
while (getline(cfp)) {
|
|
switch (line[0]) {
|
|
case 'H':
|
|
strcpy(cstr, line + 1);
|
|
continue;
|
|
case 'J':
|
|
strcpy(jstr, line + 1);
|
|
continue;
|
|
case 'P':
|
|
strcpy(ustr, line + 1);
|
|
continue;
|
|
default:
|
|
if(line[0] < 'a' || line[0] > 'z')
|
|
continue;
|
|
if(copies == 0)
|
|
strcpy(fstr, line + 1);
|
|
copies++;
|
|
continue;
|
|
case 'N':
|
|
if(*fstr && stat(fstr, &statbuf) >= 0)
|
|
size = copies * statbuf.st_size;
|
|
strcpy(nstr, line + 1);
|
|
continue;
|
|
}
|
|
}
|
|
fclose(cfp);
|
|
if(strcmp(ustr, "root") == 0 && strncmp(jstr, "MacUser: ", 9) == 0) {
|
|
mac = TRUE;
|
|
cp = jstr + 9;
|
|
for( ; ; ) {
|
|
if((cp = index(cp, ' ')) == NULL) {
|
|
job = "Untitled";
|
|
pc = pagecount(jstr + 9);
|
|
break;
|
|
}
|
|
if(strncmp(cp, " Job: ", 6) == 0) {
|
|
*cp = 0;
|
|
job = cp + 6;
|
|
pc = pagecount(job);
|
|
break;
|
|
}
|
|
cp++;
|
|
}
|
|
strcpy(ustr, jstr + 9);
|
|
if((cp = rindex(ustr, '!')) && strlen(cp) == (MACID + 1)) {
|
|
*cp++ = 0;
|
|
class = cp;
|
|
if(cp = rindex(ustr, ' ')) {
|
|
*cp++ = 0;
|
|
alias = ustr;
|
|
user = cp;
|
|
} else
|
|
user = ustr;
|
|
} else {
|
|
user = ustr;
|
|
class = "Macintosh";
|
|
}
|
|
} else {
|
|
if(*jstr)
|
|
job = jstr;
|
|
else if(strcmp(nstr, " ") == 0)
|
|
job = "(standard input)";
|
|
else
|
|
job = nstr;
|
|
user = ustr;
|
|
class = cstr;
|
|
if((copies = strlen(class)) >= 13 &&
|
|
strcmp(cp = &class[copies - 13], ".berkeley.edu") == 0)
|
|
*cp = 0;
|
|
}
|
|
cp = bdsptr;
|
|
prank(rank);
|
|
if(pc >= 0)
|
|
tobds("%s\t%s\t%s\t%s\t%d Page%s\n", alias, user, job, class,
|
|
pc, pc == 1 ? "" : "s");
|
|
else if(size >= 0L)
|
|
tobds("%s\t%s\t%s\t%s\t%ld byte%s\n", alias, user, job, class,
|
|
size, size == 1 ? "" : "s");
|
|
else
|
|
tobds("%s\t%s\t%s\t%s\t?? bytes\n", alias, user, job, class);
|
|
*bdsptr = 0;
|
|
measurecols(cp);
|
|
}
|
|
|
|
measurecols(cp)
|
|
register char *cp;
|
|
{
|
|
register char *tp;
|
|
register int i, j;
|
|
|
|
for(i = 0 ; i < (NFIELDS - 1) ; i++) {
|
|
if((tp = index(cp, '\t')) == NULL) {
|
|
if((j = strlen(cp)) > colwidth[i])
|
|
colwidth[i] = j;
|
|
break;
|
|
}
|
|
if((j = tp - cp) > colwidth[i])
|
|
colwidth[i] = j;
|
|
cp = tp + 1;
|
|
}
|
|
}
|
|
|
|
pagecount(str)
|
|
register char *str;
|
|
{
|
|
for( ; ; ) {
|
|
if((str = index(str, ' ')) == NULL)
|
|
return(-1);
|
|
if(strncmp(str, " Pages: ", 8) == 0) {
|
|
*str = 0;
|
|
str += 8;
|
|
return(atoi(str));
|
|
}
|
|
str++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Print the job's rank in the queue,
|
|
* update col for screen management
|
|
*/
|
|
prank(n)
|
|
{
|
|
char line[100];
|
|
static char *r[] = {
|
|
"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"
|
|
};
|
|
|
|
if (n == 0) {
|
|
tobds("active\t");
|
|
return;
|
|
}
|
|
if ((n/10) == 1)
|
|
tobds("%dth\t", n);
|
|
else
|
|
tobds("%d%s\t", n, r[n%10]);
|
|
}
|
|
|
|
/*
|
|
* fatal error
|
|
*/
|
|
fatal(fmt, arg1, arg2, arg3)
|
|
char *fmt;
|
|
{
|
|
fprintf(stderr, "%s: ", myname);
|
|
fprintf(stderr, fmt, arg1, arg2, arg3);
|
|
exit(1);
|
|
}
|
|
|
|
/*
|
|
* register the specified entity
|
|
*
|
|
*/
|
|
nbp_register(sobj, stype, szone, skt)
|
|
char *sobj, *stype, *szone;
|
|
int skt;
|
|
{
|
|
nbpProto nbpr; /* nbp proto */
|
|
NBPTEntry nbpt[1]; /* table of entity names */
|
|
int err;
|
|
|
|
strcpy((char *)entity.objStr.s, sobj);
|
|
strcpy((char *)entity.typeStr.s, stype);
|
|
strcpy((char *)entity.zoneStr.s, szone);
|
|
|
|
|
|
nbpr.nbpAddress.skt = skt;
|
|
nbpr.nbpRetransmitInfo.retransInterval = 4;
|
|
nbpr.nbpRetransmitInfo.retransCount = 3;
|
|
nbpr.nbpBufPtr = nbpt;
|
|
nbpr.nbpBufSize = sizeof(nbpt);
|
|
nbpr.nbpDataField = 1; /* max entries */
|
|
nbpr.nbpEntityPtr = &entity;
|
|
|
|
err = NBPRegister(&nbpr,FALSE); /* try synchronous */
|
|
return(err);
|
|
}
|
|
|
|
getline(cfp)
|
|
FILE *cfp;
|
|
{
|
|
register int linel = 0;
|
|
register char *lp = line;
|
|
register c;
|
|
|
|
while ((c = getc(cfp)) != '\n') {
|
|
if (c == EOF)
|
|
return(0);
|
|
if (c == '\t') {
|
|
do {
|
|
*lp++ = ' ';
|
|
linel++;
|
|
} while ((linel & 07) != 0);
|
|
continue;
|
|
}
|
|
*lp++ = c;
|
|
linel++;
|
|
}
|
|
*lp++ = '\0';
|
|
return(linel);
|
|
}
|
|
|
|
/*
|
|
* Scan the current directory and make a list of daemon files sorted by
|
|
* creation time.
|
|
* Return the number of entries and a pointer to the list.
|
|
*/
|
|
Getq(namelist)
|
|
struct Queue *(*namelist[]);
|
|
{
|
|
#ifdef USEDIRENT
|
|
register struct dirent *d;
|
|
#else USEDIRENT
|
|
register struct direct *d;
|
|
#endif USEDIRENT
|
|
register struct Queue *q, **Queue;
|
|
register int nitems;
|
|
int arraysz, compar();
|
|
DIR *dirp;
|
|
|
|
if ((dirp = opendir(".")) == NULL)
|
|
return(-1);
|
|
if (fstat(dirp->dd_fd, &statbuf) < 0)
|
|
goto errdone;
|
|
|
|
/*
|
|
* Estimate the array size by taking the size of the directory file
|
|
* and dividing it by a multiple of the minimum size entry.
|
|
*/
|
|
arraysz = (statbuf.st_size / 24);
|
|
Queue = (struct Queue **)malloc(arraysz * sizeof(struct Queue *));
|
|
if (Queue == NULL)
|
|
goto errdone;
|
|
|
|
nitems = 0;
|
|
while ((d = readdir(dirp)) != NULL) {
|
|
#ifdef __hpux
|
|
if (d->d_name[0] != 'c' || d->d_name[1] != 'A')
|
|
#else /* __hpux */
|
|
if (d->d_name[0] != 'c' || d->d_name[1] != 'f')
|
|
#endif /* __hpux */
|
|
continue; /* daemon control files only */
|
|
if (stat(d->d_name, &statbuf) < 0)
|
|
continue; /* Doesn't exist */
|
|
q = (struct Queue *)malloc(sizeof(time_t)+strlen(d->d_name)+1);
|
|
if (q == NULL)
|
|
goto errdone;
|
|
q->q_time = statbuf.st_mtime;
|
|
strcpy(q->q_name, d->d_name);
|
|
/*
|
|
* Check to make sure the array has space left and
|
|
* realloc the maximum size.
|
|
*/
|
|
if (++nitems > arraysz) {
|
|
Queue = (struct Queue **)realloc((char *)Queue,
|
|
(statbuf.st_size/12) * sizeof(struct Queue *));
|
|
if (Queue == NULL)
|
|
goto errdone;
|
|
}
|
|
Queue[nitems-1] = q;
|
|
}
|
|
closedir(dirp);
|
|
if (nitems)
|
|
qsort(Queue, nitems, sizeof(struct Queue *), compar);
|
|
*namelist = Queue;
|
|
return(nitems);
|
|
|
|
errdone:
|
|
closedir(dirp);
|
|
return(-1);
|
|
}
|
|
|
|
/*
|
|
* Compare modification times.
|
|
*/
|
|
compar(p1, p2)
|
|
register struct Queue **p1, **p2;
|
|
{
|
|
if ((*p1)->q_time < (*p2)->q_time)
|
|
return(-1);
|
|
if ((*p1)->q_time > (*p2)->q_time)
|
|
return(1);
|
|
return(0);
|
|
}
|