xref: /titanic_44/usr/src/cmd/rexd/on.c (revision 8eea8e29cc4374d1ee24c25a07f45af132db3499)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License, Version 1.0 only
6  * (the "License").  You may not use this file except in compliance
7  * with the License.
8  *
9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10  * or http://www.opensolaris.org/os/licensing.
11  * See the License for the specific language governing permissions
12  * and limitations under the License.
13  *
14  * When distributing Covered Code, include this CDDL HEADER in each
15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16  * If applicable, add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your own identifying
18  * information: Portions Copyright [yyyy] [name of copyright owner]
19  *
20  * CDDL HEADER END
21  */
22 #ident	"%Z%%M%	%I%	%E% SMI"
23 
24 /*
25  * on - user interface program for remote execution service
26  *
27  * Copyright (c) 1985 Sun Microsystems, Inc.
28  */
29 
30 #define	BSD_COMP
31 
32 #include <ctype.h>
33 #include <errno.h>
34 #include <netdb.h>
35 #include <signal.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <unistd.h>
40 
41 #include <netinet/in.h>
42 #include <rpc/rpc.h>
43 #include <rpc/clnt_soc.h>
44 #include <rpc/key_prot.h>
45 #include <sys/fcntl.h>
46 #include <sys/ioctl.h>
47 #include <sys/param.h>
48 #include <sys/socket.h>
49 #include <sys/sockio.h>
50 #include <sys/stat.h>
51 #include <sys/time.h>
52 
53 
54 #include <sys/ttold.h>
55 
56 
57 #include "rex.h"
58 
59 #include <stropts.h>
60 #include <sys/stream.h>
61 #include <sys/ttcompat.h>
62 
63 
64 #define	bcmp(b1, b2, len)	memcmp(b1, b2, len)
65 #define	bzero(b, len)		memset(b, '\0', len)
66 #define	bcopy(b1, b2, len)	memcpy(b2, b1, len)
67 
68 #define	CommandName "on"	/* given as argv[0] */
69 #define	AltCommandName "dbon"
70 
71 extern int errno;
72 
73 /*
74  * Note - the following must be long enough for at least two portmap
75  * timeouts on the other side.
76  */
77 struct timeval LongTimeout = { 123, 0 };
78 struct timeval testtimeout = { 5, 0 };
79 
80 int Debug = 0;			/* print extra debugging information */
81 int Only2 = 0;			/* stdout and stderr are the same */
82 int Interactive = 0;		/* use a pty on server */
83 int NoInput = 0;		/* don't read standard input */
84 int child = 0;			/* pid of the executed process */
85 int ChildDied = 0;		/* true when above is valid */
86 int HasHelper = 0;		/* must kill helpers (interactive mode) */
87 
88 int InOut;			/* socket for stdin/stdout */
89 int Err;			/* socket for stderr */
90 
91 struct sgttyb OldFlags;		/* saved tty flags */
92 struct sgttyb NewFlags;		/* for stop/continue job control */
93 CLIENT *Client;			/* RPC client handle */
94 struct rex_ttysize WindowSize;	/* saved window size */
95 
96 static int Argc;
97 static char **Argv;		/* saved argument vector (for ps) */
98 static char *LastArgv;		/* saved end-of-argument vector */
99 
100 void	usage(void);
101 void	Die(int stat);
102 void	doaccept(int *fdp);
103 u_short makeport(int *fdp);
104 
105 
106 /*
107  * window change handler - propagate to remote server
108  */
109 void
110 sigwinch(int junk)
111 {
112 	struct	winsize	newsize; /* the modern way to get row and col	*/
113 	struct	rex_ttysize	size;	/* the old way no body */
114 					/* bothered to change */
115 	enum	clnt_stat	clstat;
116 
117 	ioctl(0, TIOCGWINSZ, &newsize);
118 
119 	/*
120 	 * compensate for the struct change
121 	 */
122 	size.ts_lines = (int)newsize.ws_row; /* typecast important! */
123 	size.ts_cols = (int)newsize.ws_col;
124 
125 	if (bcmp(&size, &WindowSize, sizeof (size)) == 0)
126 		return;
127 
128 	WindowSize = size;
129 	if (clstat = clnt_call(Client, REXPROC_WINCH,
130 				xdr_rex_ttysize, (caddr_t)&size, xdr_void,
131 				NULL, LongTimeout)) {
132 		fprintf(stderr, "on (size): ");
133 		clnt_perrno(clstat);
134 		fprintf(stderr, "\r\n");
135 	}
136 }
137 
138 /*
139  * signal handler - propagate to remote server
140  */
141 void
142 sendsig(int sig)
143 {
144 	enum clnt_stat clstat;
145 
146 	if (clstat = clnt_call(Client, REXPROC_SIGNAL,
147 				xdr_int, (caddr_t) &sig, xdr_void,
148 				NULL, LongTimeout)) {
149 		fprintf(stderr, "on (signal): ");
150 		clnt_perrno(clstat);
151 		fprintf(stderr, "\r\n");
152 	}
153 }
154 
155 
156 void
157 cont(int junk)
158 {
159 	/*
160 	 * Put tty modes back the way they were and tell the rexd server
161 	 * to send the command a SIGCONT signal.
162 	 */
163 	if (Interactive) {
164 		ioctl(0, TIOCSETN, &NewFlags);
165 		(void) send(InOut, "", 1, MSG_OOB);
166 	}
167 }
168 
169 /*
170  * oob -- called when the command invoked by the rexd server is stopped
171  *	  with a SIGTSTP or SIGSTOP signal.
172  */
173 void
174 oob(int junk)
175 {
176 	int atmark;
177 	char waste[BUFSIZ], mark;
178 
179 	for (;;) {
180 		if (ioctl(InOut, SIOCATMARK, &atmark) < 0) {
181 			perror("ioctl");
182 			break;
183 		}
184 		if (atmark)
185 			break;
186 		(void) read(InOut, waste, sizeof (waste));
187 	}
188 	(void) recv(InOut, &mark, 1, MSG_OOB);
189 	/*
190 	 * Reset tty modes to something sane and stop myself
191 	 */
192 	if (Interactive) {
193 		ioctl(0, TIOCSETN, &OldFlags);
194 		printf("\r\n");
195 	}
196 	kill(getpid(), SIGSTOP);
197 }
198 
199 
200 
201 void
202 main(int argc, char **argv)
203 {
204 	struct	winsize	newsize; /* the modern way to get row and col	*/
205 	char *rhost, **cmdp;
206 	char curdir[MAXPATHLEN];
207 	char wdhost[MAXHOSTNAMELEN];
208 	char fsname[MAXPATHLEN];
209 	char dirwithin[MAXPATHLEN];
210 	struct rex_start rst;
211 	struct rex_result result;
212 	extern char **environ;
213 	enum clnt_stat clstat;
214 	struct hostent *hp;
215 	struct sockaddr_in server_addr;
216 	int sock = RPC_ANYSOCK;
217 	fd_set selmask, zmask, remmask;
218 	int nfds, cc;
219 	char *chi, *cho;
220 	int trying_authdes;
221 	char netname[MAXNETNAMELEN+1];
222 	char hostname[MAXHOSTNAMELEN+1];
223 	char publickey[HEXKEYBYTES+1];
224 	int i;
225 	char *domain;
226 	static char buf[4096];
227 
228 	/*
229 	 * we check the invoked command name to see if it should
230 	 * really be a host name.
231 	 */
232 	if ((rhost = strrchr(argv[0], '/')) == NULL) {
233 		rhost = argv[0];
234 	} else {
235 		rhost++;
236 	}
237 
238 	/*
239 	 * argv start and extent for setproctitle()
240 	 */
241 	Argc = argc;
242 	Argv = argv;
243 	if (argc > 0)
244 		LastArgv = argv[argc-1] + strlen(argv[argc-1]);
245 	else
246 		LastArgv = NULL;
247 
248 	while (argc > 1 && argv[1][0] == '-') {
249 		switch (argv[1][1]) {
250 		case 'd': Debug = 1;
251 			break;
252 		case 'i': Interactive = 1;
253 			break;
254 		case 'n': NoInput = 1;
255 			break;
256 		default:
257 			printf("Unknown option %s\n", argv[1]);
258 		}
259 		argv++;
260 		argc--;
261 	}
262 
263 	if (strcmp(rhost, CommandName) && strcmp(rhost, AltCommandName)) {
264 		cmdp = &argv[1];
265 		Interactive = 1;
266 	} else {
267 		if (argc < 2)
268 			usage();
269 		rhost = argv[1];
270 		cmdp = &argv[2];
271 	}
272 
273 	/*
274 	 * Can only have one of these
275 	 */
276 	if (Interactive && NoInput)
277 		usage();
278 
279 	if ((hp = gethostbyname(rhost)) == NULL) {
280 		fprintf(stderr, "on: unknown host %s\n", rhost);
281 		exit(1);
282 	}
283 
284 	bcopy(hp->h_addr, (caddr_t)&server_addr.sin_addr, hp->h_length);
285 	server_addr.sin_family = AF_INET;
286 	server_addr.sin_port = 0; /* use pmapper */
287 
288 	if (Debug)
289 		printf("Got the host named %s (%s)\n",
290 			rhost, inet_ntoa(server_addr.sin_addr));
291 	trying_authdes = 1;
292 
293 try_auth_unix:
294 	sock = RPC_ANYSOCK;
295 
296 	if (Debug)
297 		printf("clnt_create: Server_Addr %u Prog %d Vers %d Sock %d\n",
298 			&server_addr, REXPROG, REXVERS, sock);
299 
300 	if ((Client = clnttcp_create(&server_addr, REXPROG, REXVERS, &sock,
301 					0, 0)) == NULL) {
302 		fprintf(stderr, "on: cannot connect to server on %s\n",
303 			rhost);
304 		clnt_pcreateerror("on:");
305 		exit(1);
306 	}
307 
308 	if (Debug)
309 		printf("TCP RPC connection created\n");
310 
311 	if (trying_authdes) {
312 		yp_get_default_domain(&domain);
313 
314 		cho =  hostname;
315 		*cho = 0;
316 		chi =  hp->h_name;
317 
318 		for (i = 0; (*chi && (i < MAXHOSTNAMELEN)); i++)
319 			{
320 				if (isupper(*chi))
321 					*cho = tolower(*chi);
322 				else
323 					*cho = *chi;
324 				cho++;
325 				chi++;
326 			}
327 		*cho = 0;
328 
329 		if (domain != NULL)	{
330 			if (host2netname(netname, hostname, domain) == 0) {
331 				trying_authdes = 0;
332 				if (Debug)
333 					printf("host2netname failed %s\n",
334 						hp->h_name);
335 			}
336 			/* #ifdef	NOWAY */
337 			else {
338 
339 				if (getpublickey(netname, publickey) == 0) {
340 					trying_authdes = 0;
341 					cho = strchr(hostname, '.');
342 
343 					if (cho) {
344 						*cho = 0;
345 
346 						if (!host2netname(netname,
347 						    hostname,
348 						    domain)) {
349 							if (Debug)
350 				printf("host2netname failed %s\n", hp->h_name);
351 						} else {
352 							if (getpublickey(
353 							    netname,
354 							    publickey) != 0)
355 							trying_authdes = 1;
356 						}
357 					}
358 				}
359 			}
360 		} else {
361 			trying_authdes = 0;
362 			if (Debug)
363 				printf("yp_get_default_domain failed \n");
364 		}
365 	}
366 
367 	if (trying_authdes) {
368 		Client->cl_auth = (AUTH *)authdes_create(netname, 60*60,
369 						&server_addr, NULL);
370 
371 		if (Client->cl_auth == NULL) {
372 
373 			if (Debug)
374 				printf("authdes_create failed %s\n", netname);
375 			trying_authdes = 0;
376 		}
377 	}
378 
379 
380 	if (trying_authdes == 0)
381 		if ((Client->cl_auth = authsys_create_default()) == NULL) {
382 			clnt_destroy(Client);
383 			fprintf(stderr,"on: can't create authunix structure.\n");
384 			exit(1);
385 		}
386 
387 
388 	/*
389 	 * Now that we have created the TCP connection, we do some
390 	 * work while the server daemon is being swapped in.
391 	 */
392 	if (getcwd(curdir, MAXPATHLEN) == (char *)NULL) {
393 		fprintf(stderr, "on: can't find . (%s)\n", curdir);
394 		exit(1);
395 	}
396 
397 	if (findmount(curdir, wdhost, fsname, dirwithin) == 0) {
398 
399 		if (Debug) {
400 			fprintf(stderr,
401 				"findmount failed: curdir %s\twdhost %s\t",
402 				curdir, wdhost);
403 			fprintf(stderr, "fsname %s\tdirwithin %s\n",
404 				fsname, dirwithin);
405 		}
406 
407 		fprintf(stderr, "on: can't locate mount point for %s (%s)\n",
408 			curdir, dirwithin);
409 		exit(1);
410 	}
411 
412 	if (Debug) {
413 		printf("findmount suceeds: cwd= %s, wd host %s, fs %s,",
414 			curdir, wdhost, fsname);
415 		printf("dir within %s\n", dirwithin);
416 	}
417 
418 	Only2 = samefd(1, 2);
419 
420 	rst.rst_cmd = (void *)(cmdp);
421 	rst.rst_host = (void *)wdhost;
422 	rst.rst_fsname = (void *)fsname;
423 	rst.rst_dirwithin = (void *)dirwithin;
424 	rst.rst_env = (void *)environ;
425 	rst.rst_port0 = makeport(&InOut);
426 	rst.rst_port1 =  rst.rst_port0;	/* same port for stdin */
427 	rst.rst_flags = 0;
428 
429 	if (Debug)
430 		printf("before Interactive flags\n");
431 
432 	if (Interactive) {
433 		rst.rst_flags |= REX_INTERACTIVE;
434 		ioctl(0, TIOCGETP, &OldFlags);
435 		NewFlags = OldFlags;
436 		NewFlags.sg_flags |= (u_int)RAW;
437 		NewFlags.sg_flags &= (u_int)~ECHO;
438 		ioctl(0, TIOCSETN, &NewFlags);
439 	}
440 
441 	if (Only2) {
442 		rst.rst_port2 = rst.rst_port1;
443 	} else {
444 		rst.rst_port2 = makeport(&Err);
445 	}
446 
447 	if (Debug)
448 		printf("before client call REXPROC_START\n");
449 
450 	(void) memset(&result, '\0', sizeof(result));
451 
452 	if (clstat = clnt_call(Client, REXPROC_START,
453 			       xdr_rex_start, (caddr_t)&rst,
454 			       xdr_rex_result, (caddr_t)&result, LongTimeout)) {
455 
456 		if (Debug)
457 			printf("Client call failed for REXPROC_START\r\n");
458 
459 		if (trying_authdes) {
460 			auth_destroy(Client->cl_auth);
461 			clnt_destroy(Client);
462 			trying_authdes = 0;
463 			if (Interactive)
464 				ioctl(0, TIOCSETN, &OldFlags);
465 			goto try_auth_unix;
466 		} else {
467 			fprintf(stderr, "on %s: ", rhost);
468 			clnt_perrno(clstat);
469 			fprintf(stderr, "\n");
470 			Die(1);
471 		}
472 	}
473 
474 	if (result.rlt_stat != 0) {
475 		fprintf(stderr, "on %s: %s\n\r", rhost, result.rlt_message);
476 		Die(1);
477 	}
478 
479 	clnt_freeres(Client, xdr_rex_result, (caddr_t)&result);
480 
481 	if (Debug)
482 		printf("Client call suceeded for REXPROC_START\r\n");
483 
484 	if (Interactive) {
485 		/*
486 		 * Pass the tty modes along to the server
487 		 */
488 		struct rex_ttymode mode;
489 		int err;
490 
491 		mode.basic.sg_ispeed = OldFlags.sg_ispeed;
492 		mode.basic.sg_ospeed = OldFlags.sg_ospeed;
493 		mode.basic.sg_erase = OldFlags.sg_erase;
494 		mode.basic.sg_kill = OldFlags.sg_kill;
495 		mode.basic.sg_flags = (short) (OldFlags.sg_flags & 0xFFFF);
496 		err =  (ioctl(0, TIOCGETC, &mode.more) < 0 ||
497 			ioctl(0, TIOCGLTC, &mode.yetmore) < 0 ||
498 			ioctl(0, TIOCLGET, &mode.andmore) < 0);
499 		if (Debug)
500 			printf("Before clnt_call(REXPROC_MODES) err=%d\n", err);
501 
502 		if (!err && (clstat = clnt_call(Client, REXPROC_MODES,
503 					xdr_rex_ttymode, (caddr_t)&mode,
504 					xdr_void, NULL, LongTimeout))) {
505 
506 			fprintf(stderr, "on (modes) %s: ", rhost);
507 			clnt_perrno(clstat);
508 			fprintf(stderr, "\r\n");
509 		}
510 
511 		err = ioctl(0, TIOCGWINSZ, &newsize) < 0;
512 		/* typecast important in following lines */
513 		WindowSize.ts_lines = (int)newsize.ws_row;
514 		WindowSize.ts_cols = (int)newsize.ws_col;
515 
516 		if (Debug)
517 			printf("Before client call REXPROC_WINCH\n");
518 
519 		if (!err && (clstat = clnt_call(Client, REXPROC_WINCH,
520 					xdr_rex_ttysize, (caddr_t)&WindowSize,
521 					xdr_void, NULL, LongTimeout))) {
522 
523 			fprintf(stderr, "on (size) %s: ", rhost);
524 			clnt_perrno(clstat);
525 			fprintf(stderr, "\r\n");
526 		}
527 
528 		sigset(SIGWINCH, sigwinch);
529 		sigset(SIGINT, sendsig);
530 		sigset(SIGQUIT, sendsig);
531 		sigset(SIGTERM, sendsig);
532 	}
533 	sigset(SIGCONT, cont);
534 	sigset(SIGURG, oob);
535 	doaccept(&InOut);
536 	(void) fcntl(InOut, F_SETOWN, getpid());
537 	FD_ZERO(&remmask);
538 	FD_SET(InOut, &remmask);
539 	if (Debug)
540 		printf("accept on stdout\r\n");
541 
542 	if (!Only2) {
543 
544 		doaccept(&Err);
545 		shutdown(Err, 1); /* 1=> further sends disallowed */
546 		if (Debug)
547 			printf("accept on stderr\r\n");
548 		FD_SET(Err, &remmask);
549 	}
550 
551 	FD_ZERO(&zmask);
552 	if (NoInput) {
553 
554 		/*
555 		 * no input - simulate end-of-file instead
556 		 */
557 		shutdown(InOut, 1); /* 1=> further sends disallowed */
558 	} else {
559 		/*
560 		 * set up to read standard input, send to remote
561 		 */
562 		FD_SET(0, &zmask);
563 	}
564 
565 	FD_ZERO(&selmask);
566 	while (FD_ISSET(InOut, &remmask) || FD_ISSET(Err, &remmask)) {
567 		if (FD_ISSET(InOut, &remmask))
568 			FD_SET(InOut, &selmask);
569 		else
570 			FD_CLR(InOut, &selmask);
571 		if (FD_ISSET(Err, &remmask))
572 			FD_SET(Err, &selmask);
573 		else
574 			FD_CLR(Err, &selmask);
575 		if (FD_ISSET(0, &zmask))
576 			FD_SET(0, &selmask);
577 		else
578 			FD_CLR(0, &selmask);
579 		nfds = select(FD_SETSIZE, &selmask, (fd_set *) 0, (fd_set *) 0,
580 			      (struct timeval *) 0);
581 
582 
583  		if (nfds <= 0) {
584 			if (errno == EINTR) continue;
585 			perror("on: select");
586 			Die(1);
587 		}
588 		if (FD_ISSET(InOut, &selmask)) {
589 
590 			cc = read(InOut, buf, sizeof buf);
591 			if (cc > 0)
592 				write(1, buf, cc);
593 			else
594 				FD_CLR(InOut, &remmask);
595 		}
596 
597 		if (!Only2 && FD_ISSET(Err, &selmask)) {
598 
599 			cc = read(Err, buf, sizeof buf);
600 			if (cc > 0)
601 				write(2, buf, cc);
602 			else
603 				FD_CLR(Err, &remmask);
604 		}
605 
606 		if (!NoInput && FD_ISSET(0, &selmask)) {
607 
608 			cc = read(0, buf, sizeof buf);
609 			if (cc > 0)
610 				write(InOut, buf, cc);
611 			else {
612 				/*
613 				 * End of standard input - shutdown outgoing
614 				 * direction of the TCP connection.
615 				 */
616 				if (Debug)
617 					printf("Got EOF - shutting down connection\n");
618 				FD_CLR(0, &zmask);
619 				shutdown(InOut, 1); /* further sends disallowed */
620 			}
621 		}
622 	}
623 
624 	close(InOut);
625 	if (!Only2)
626 		close(Err);
627 
628 	(void) memset(&result, '\0', sizeof(result));
629 
630 	if (clstat = clnt_call(Client, REXPROC_WAIT,
631 			       xdr_void, 0, xdr_rex_result, (caddr_t)&result,
632 			       LongTimeout)) {
633 
634 		fprintf(stderr, "on: ");
635 		clnt_perrno(clstat);
636 		fprintf(stderr, "\r\n");
637 		Die(1);
638 	}
639 	Die(result.rlt_stat);
640 }
641 
642 /*
643  * like exit, but resets the terminal state first
644  */
645 void
646 Die(int stat)
647 
648 {
649 	if (Interactive) {
650 		ioctl(0, TIOCSETN, &OldFlags);
651 		printf("\r\n");
652 	}
653 	exit(stat);
654 }
655 
656 
657 void
658 remstop()
659 
660 {
661 	Die(23);
662 }
663 
664 /*
665  * returns true if we can safely say that the two file descriptors
666  * are the "same" (both are same file).
667  */
668 int
669 samefd(a, b)
670 {
671 	struct stat astat, bstat;
672 
673 	if (fstat(a, &astat) || fstat(b, &bstat))
674 		return (0);
675 	if (astat.st_ino == 0 || bstat.st_ino == 0)
676 		return (0);
677 	return (!bcmp(&astat, &bstat, sizeof (astat)));
678 }
679 
680 
681 /*
682  * accept the incoming connection on the given
683  * file descriptor, and return the new file descritpor
684  */
685 void
686 doaccept(fdp)
687 	int *fdp;
688 {
689 	int fd;
690 
691 	fd = accept(*fdp, 0, 0);
692 
693 	if (fd < 0) {
694 		perror("accept");
695 		remstop();
696 	}
697 	close(*fdp);
698 	*fdp = fd;
699 }
700 
701 /*
702  * create a socket, and return its the port number.
703  */
704 u_short
705 makeport(fdp)
706 	int *fdp;
707 {
708 	struct sockaddr_in sin;
709 	socklen_t len = (socklen_t)sizeof (sin);
710 	int fd;
711 
712 	fd = socket(AF_INET, SOCK_STREAM, 0);
713 
714 	if (fd < 0) {
715 		perror("socket");
716 		exit(1);
717 	}
718 
719 	bzero((char *)&sin, sizeof (sin));
720 	sin.sin_family = AF_INET;
721 	bind(fd, (struct sockaddr *)&sin, sizeof (sin));
722 	getsockname(fd, (struct sockaddr *)&sin, &len);
723 	listen(fd, 1);
724 	*fdp = fd;
725 	return (htons(sin.sin_port));
726 }
727 
728 void
729 usage(void)
730 {
731 	fprintf(stderr, "Usage: on [-i|-n] [-d] machine cmd [args]...\n");
732 	exit(1);
733 }
734 
735 /*
736  *  SETPROCTITLE -- set the title of this process for "ps"
737  *
738  *	Does nothing if there were not enough arguments on the command
739  * 	line for the information.
740  *
741  *	Side Effects:
742  *		Clobbers argv[] of our main procedure.
743  */
744 void
745 setproctitle(user, host)
746 	char *user, *host;
747 {
748 	register char *tohere;
749 
750 	tohere = Argv[0];
751 	if ((int)LastArgv == (int)((char *)NULL) ||
752 	    (int)(strlen(user) + strlen(host)+3) > (int)(LastArgv - tohere))
753 		return;
754 	*tohere++ = '-';		/* So ps prints (rpc.rexd) */
755 	sprintf(tohere, "%s@%s", user, host);
756 	while (*tohere++)		/* Skip to end of printf output	*/
757 		;
758 	while (tohere < LastArgv)	/* Avoid confusing ps		*/
759 		*tohere++ = ' ';
760 }
761