]> git.ipfire.org Git - thirdparty/util-linux.git/blame - lib/pty-session.c
script: add debug messages around waitpid()
[thirdparty/util-linux.git] / lib / pty-session.c
CommitLineData
6954895c
KZ
1/*
2 * This is pseudo-terminal container for child process where parent creates a
3 * proxy between the current std{in,out,etrr} and the child's pty. Advantages:
4 *
5 * - child has no access to parent's terminal (e.g. su --pty)
6 * - parent can log all traffic between user and child's terminall (e.g. script(1))
7 * - it's possible to start commands on terminal although parent has no terminal
8 *
9 * This code is in the public domain; do with it what you wish.
10 *
11 * Written by Karel Zak <kzak@redhat.com> in Jul 2019
12 */
13#include <stdio.h>
14#include <stdlib.h>
15#include <pty.h>
16#include <poll.h>
17#include <sys/signalfd.h>
18#include <paths.h>
19#include <sys/types.h>
20#include <sys/wait.h>
21
22#include "c.h"
23#include "all-io.h"
24#include "ttyutils.h"
25#include "pty-session.h"
4d5b2fed 26#include "monotonic.h"
6954895c
KZ
27#include "debug.h"
28
29static UL_DEBUG_DEFINE_MASK(ulpty);
30UL_DEBUG_DEFINE_MASKNAMES(ulpty) = UL_DEBUG_EMPTY_MASKNAMES;
31
32#define ULPTY_DEBUG_INIT (1 << 1)
33#define ULPTY_DEBUG_SETUP (1 << 2)
34#define ULPTY_DEBUG_SIG (1 << 3)
35#define ULPTY_DEBUG_IO (1 << 4)
36#define ULPTY_DEBUG_DONE (1 << 5)
37#define ULPTY_DEBUG_ALL 0xFFFF
38
39#define DBG(m, x) __UL_DBG(ulpty, ULPTY_DEBUG_, m, x)
40#define ON_DBG(m, x) __UL_DBG_CALL(ulpty, ULPTY_DEBUG_, m, x)
41
42#define UL_DEBUG_CURRENT_MASK UL_DEBUG_MASK(ulpty)
43#include "debugobj.h"
44
45void ul_pty_init_debug(int mask)
46{
47 if (ulpty_debug_mask)
48 return;
49 __UL_INIT_DEBUG_FROM_ENV(ulpty, ULPTY_DEBUG_, mask, ULPTY_DEBUG);
50}
51
52struct ul_pty *ul_new_pty(int is_stdin_tty)
53{
54 struct ul_pty *pty = calloc(1, sizeof(*pty));
55
56 if (!pty)
57 return NULL;
58
59 DBG(SETUP, ul_debugobj(pty, "alloc handler"));
60 pty->isterm = is_stdin_tty;
61 pty->master = -1;
62 pty->slave = -1;
63 pty->sigfd = -1;
64 pty->child = (pid_t) -1;
65
66 return pty;
67}
68
ab61a038 69void ul_free_pty(struct ul_pty *pty)
6954895c 70{
ab61a038 71 free(pty);
6954895c
KZ
72}
73
ab61a038 74
6954895c
KZ
75int ul_pty_get_delivered_signal(struct ul_pty *pty)
76{
77 assert(pty);
78 return pty->delivered_signal;
79}
80
81struct ul_pty_callbacks *ul_pty_get_callbacks(struct ul_pty *pty)
82{
83 assert(pty);
84 return &pty->callbacks;
85}
86
87void ul_pty_set_callback_data(struct ul_pty *pty, void *data)
88{
89 assert(pty);
90 pty->callback_data = data;
91}
92
93void ul_pty_set_child(struct ul_pty *pty, pid_t child)
94{
95 assert(pty);
96 pty->child = child;
97}
98
4d5b2fed
KZ
99int ul_pty_get_childfd(struct ul_pty *pty)
100{
101 assert(pty);
102 return pty->master;
103}
104
bdd43357
KZ
105pid_t ul_pty_get_child(struct ul_pty *pty)
106{
107 assert(pty);
108 return pty->child;
109}
110
6954895c
KZ
111/* it's active when signals are redurected to sigfd */
112int ul_pty_is_running(struct ul_pty *pty)
113{
114 assert(pty);
115 return pty->sigfd >= 0;
116}
117
4d5b2fed
KZ
118void ul_pty_set_mainloop_time(struct ul_pty *pty, struct timeval *tv)
119{
120 assert(pty);
121 if (!tv) {
122 DBG(IO, ul_debugobj(pty, "mainloop time: clear"));
123 timerclear(&pty->next_callback_time);
124 } else {
125 pty->next_callback_time.tv_sec = tv->tv_sec;
126 pty->next_callback_time.tv_usec = tv->tv_usec;
127 DBG(IO, ul_debugobj(pty, "mainloop time: %ld.%06ld", tv->tv_sec, tv->tv_usec));
128 }
129}
130
6954895c
KZ
131/* call me before fork() */
132int ul_pty_setup(struct ul_pty *pty)
133{
134 struct termios slave_attrs;
135 int rc;
136
ab61a038
KZ
137 /* save the current signals setting */
138 sigprocmask(0, NULL, &pty->orgsig);
139
6954895c
KZ
140 if (pty->isterm) {
141 DBG(SETUP, ul_debugobj(pty, "create for terminal"));
142
143 /* original setting of the current terminal */
144 if (tcgetattr(STDIN_FILENO, &pty->stdin_attrs) != 0)
145 return -errno;
146 ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
147 /* create master+slave */
148 rc = openpty(&pty->master, &pty->slave, NULL, &pty->stdin_attrs, &pty->win);
149
150 /* set the current terminal to raw mode; pty_cleanup() reverses this change on exit */
151 slave_attrs = pty->stdin_attrs;
152 cfmakeraw(&slave_attrs);
153 slave_attrs.c_lflag &= ~ECHO;
154 tcsetattr(STDIN_FILENO, TCSANOW, &slave_attrs);
155 } else {
156 DBG(SETUP, ul_debugobj(pty, "create for non-terminal"));
157 rc = openpty(&pty->master, &pty->slave, NULL, NULL, NULL);
158
159 if (!rc) {
160 tcgetattr(pty->slave, &slave_attrs);
161 slave_attrs.c_lflag &= ~ECHO;
162 tcsetattr(pty->slave, TCSANOW, &slave_attrs);
163 }
164 }
165
166 DBG(SETUP, ul_debugobj(pty, "pty setup done [master=%d, slave=%d, rc=%d]",
167 pty->master, pty->slave, rc));
168 return rc;
169}
170
171/* cleanup in parent process */
172void ul_pty_cleanup(struct ul_pty *pty)
173{
174 struct termios rtt;
175
176 if (pty->master == -1 || !pty->isterm)
177 return;
178
179 DBG(DONE, ul_debugobj(pty, "cleanup"));
180 rtt = pty->stdin_attrs;
181 tcsetattr(STDIN_FILENO, TCSADRAIN, &rtt);
182}
183
184/* call me in child process */
185void ul_pty_init_slave(struct ul_pty *pty)
186{
187 DBG(SETUP, ul_debugobj(pty, "initialize slave"));
188
189 setsid();
190
191 ioctl(pty->slave, TIOCSCTTY, 1);
192 close(pty->master);
193
194 dup2(pty->slave, STDIN_FILENO);
195 dup2(pty->slave, STDOUT_FILENO);
196 dup2(pty->slave, STDERR_FILENO);
197
198 close(pty->slave);
199
200 if (pty->sigfd >= 0)
201 close(pty->sigfd);
202
203 pty->slave = -1;
204 pty->master = -1;
205 pty->sigfd = -1;
206
207 sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
208
209 DBG(SETUP, ul_debugobj(pty, "... initialize slave done"));
210}
211
212static int write_output(char *obuf, ssize_t bytes)
213{
214 DBG(IO, ul_debug(" writing output"));
215
216 if (write_all(STDOUT_FILENO, obuf, bytes)) {
217 DBG(IO, ul_debug(" writing output *failed*"));
218 return -errno;
219 }
220
221 return 0;
222}
223
4d5b2fed 224static int write_to_child(struct ul_pty *pty, char *buf, size_t bufsz)
6954895c
KZ
225{
226 return write_all(pty->master, buf, bufsz);
227}
228
229/*
230 * The pty is usually faster than shell, so it's a good idea to wait until
231 * the previous message has been already read by shell from slave before we
232 * write to master. This is necessary especially for EOF situation when we can
233 * send EOF to master before shell is fully initialized, to workaround this
234 * problem we wait until slave is empty. For example:
235 *
236 * echo "date" | su --pty
237 *
238 * Unfortunately, the child (usually shell) can ignore stdin at all, so we
239 * don't wait forever to avoid dead locks...
240 *
241 * Note that su --pty is primarily designed for interactive sessions as it
242 * maintains master+slave tty stuff within the session. Use pipe to write to
243 * pty and assume non-interactive (tee-like) behavior is NOT well supported.
244 */
245static void write_eof_to_child(struct ul_pty *pty)
246{
247 unsigned int tries = 0;
248 struct pollfd fds[] = {
249 { .fd = pty->slave, .events = POLLIN }
250 };
251 char c = DEF_EOF;
252
253 DBG(IO, ul_debugobj(pty, " waiting for empty slave"));
254 while (poll(fds, 1, 10) == 1 && tries < 8) {
255 DBG(IO, ul_debugobj(pty, " slave is not empty"));
256 xusleep(250000);
257 tries++;
258 }
259 if (tries < 8)
260 DBG(IO, ul_debugobj(pty, " slave is empty now"));
261
262 DBG(IO, ul_debugobj(pty, " sending EOF to master"));
263 write_to_child(pty, &c, sizeof(char));
264}
265
4d5b2fed
KZ
266static int mainloop_callback(struct ul_pty *pty)
267{
268 if (!pty->callbacks.mainloop)
269 return 0;
270
271 DBG(IO, ul_debugobj(pty, "calling mainloop callback"));
272 return pty->callbacks.mainloop(pty->callback_data);
273}
274
6954895c
KZ
275static int handle_io(struct ul_pty *pty, int fd, int *eof)
276{
277 char buf[BUFSIZ];
278 ssize_t bytes;
4f7f723b 279 int rc = 0;
6954895c 280
c4bacbd1 281 DBG(IO, ul_debugobj(pty, " handle I/O on fd=%d", fd));
6954895c
KZ
282 *eof = 0;
283
284 /* read from active FD */
285 bytes = read(fd, buf, sizeof(buf));
286 if (bytes < 0) {
287 if (errno == EAGAIN || errno == EINTR)
288 return 0;
289 return -errno;
290 }
291
292 if (bytes == 0) {
293 *eof = 1;
294 return 0;
295 }
296
297 /* from stdin (user) to command */
298 if (fd == STDIN_FILENO) {
299 DBG(IO, ul_debugobj(pty, " stdin --> master %zd bytes", bytes));
300
301 if (write_to_child(pty, buf, bytes))
302 return -errno;
303
304 /* without sync write_output() will write both input &
305 * shell output that looks like double echoing */
306 fdatasync(pty->master);
307
308 /* from command (master) to stdout */
309 } else if (fd == pty->master) {
310 DBG(IO, ul_debugobj(pty, " master --> stdout %zd bytes", bytes));
311 write_output(buf, bytes);
312 }
313
4f7f723b
KZ
314 if (pty->callbacks.log_stream_activity)
315 rc = pty->callbacks.log_stream_activity(
316 pty->callback_data, fd, buf, bytes);
317
318 return rc;
6954895c
KZ
319}
320
bdd43357
KZ
321void ul_pty_wait_for_child(struct ul_pty *pty)
322{
323 int status;
324 pid_t pid;
325 int options = 0;
326
327 if (pty->child == (pid_t) -1)
328 return;
329
125314c0 330 DBG(SIG, ul_debug("waiting for child [child=%d]", (int) pty->child));
bdd43357
KZ
331
332 if (ul_pty_is_running(pty)) {
333 /* wait for specific child */
334 options = WNOHANG;
335 for (;;) {
336 pid = waitpid(pty->child, &status, options);
125314c0 337 DBG(SIG, ul_debug(" waitpid done [rc=%d]", (int) pid));
bdd43357
KZ
338 if (pid != (pid_t) - 1) {
339 if (pty->callbacks.child_die)
340 pty->callbacks.child_die(
341 pty->callback_data,
342 pty->child, status);
343 ul_pty_set_child(pty, (pid_t) -1);
344 } else
345 break;
346 }
347 } else {
348 /* final wait */
349 while ((pid = wait3(&status, options, NULL)) > 0) {
125314c0 350 DBG(SIG, ul_debug(" wait3 done [rc=%d]", (int) pid));
bdd43357
KZ
351 if (pid == pty->child) {
352 if (pty->callbacks.child_die)
353 pty->callbacks.child_die(
354 pty->callback_data,
355 pty->child, status);
356 ul_pty_set_child(pty, (pid_t) -1);
357 }
358 }
359 }
360}
361
6954895c
KZ
362static int handle_signal(struct ul_pty *pty, int fd)
363{
364 struct signalfd_siginfo info;
365 ssize_t bytes;
4f7f723b 366 int rc = 0;
6954895c 367
c4bacbd1 368 DBG(SIG, ul_debugobj(pty, " handle signal on fd=%d", fd));
6954895c
KZ
369
370 bytes = read(fd, &info, sizeof(info));
371 if (bytes != sizeof(info)) {
372 if (bytes < 0 && (errno == EAGAIN || errno == EINTR))
373 return 0;
374 return -errno;
375 }
376
377 switch (info.ssi_signo) {
378 case SIGCHLD:
379 DBG(SIG, ul_debugobj(pty, " get signal SIGCHLD"));
380
381 if (info.ssi_code == CLD_EXITED
382 || info.ssi_code == CLD_KILLED
bdd43357
KZ
383 || info.ssi_code == CLD_DUMPED) {
384
385 if (pty->callbacks.child_wait)
386 pty->callbacks.child_wait(pty->callback_data,
387 pty->child);
388 else
389 ul_pty_wait_for_child(pty);
6954895c 390
bdd43357
KZ
391 } else if (info.ssi_status == SIGSTOP && pty->child > 0)
392 pty->callbacks.child_sigstop(pty->callback_data,
393 pty->child);
6954895c 394
984082fa 395 if (pty->child <= 0) {
125314c0 396 DBG(SIG, ul_debugobj(pty, " no child, setting leaving timeout"));
6954895c 397 pty->poll_timeout = 10;
984082fa
KZ
398 timerclear(&pty->next_callback_time);
399 }
6954895c
KZ
400 return 0;
401 case SIGWINCH:
402 DBG(SIG, ul_debugobj(pty, " get signal SIGWINCH"));
403 if (pty->isterm) {
404 ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
405 ioctl(pty->slave, TIOCSWINSZ, (char *)&pty->win);
4f7f723b
KZ
406
407 if (pty->callbacks.log_signal)
408 rc = pty->callbacks.log_signal(pty->callback_data,
409 &info, (void *) &pty->win);
6954895c
KZ
410 }
411 break;
412 case SIGTERM:
413 /* fallthrough */
414 case SIGINT:
415 /* fallthrough */
416 case SIGQUIT:
417 DBG(SIG, ul_debugobj(pty, " get signal SIG{TERM,INT,QUIT}"));
418 pty->delivered_signal = info.ssi_signo;
419 /* Child termination is going to generate SIGCHILD (see above) */
420 if (pty->child > 0)
421 kill(pty->child, SIGTERM);
4f7f723b
KZ
422
423 if (pty->callbacks.log_signal)
424 rc = pty->callbacks.log_signal(pty->callback_data,
425 &info, (void *) &pty->win);
6954895c
KZ
426 break;
427 default:
428 abort();
429 }
430
4f7f723b 431 return rc;
6954895c
KZ
432}
433
434/* loop in parent */
435int ul_pty_proxy_master(struct ul_pty *pty)
436{
437 sigset_t ourset;
438 int rc = 0, ret, eof = 0;
439 enum {
440 POLLFD_SIGNAL = 0,
441 POLLFD_MASTER,
442 POLLFD_STDIN
443
444 };
445 struct pollfd pfd[] = {
446 [POLLFD_SIGNAL] = { .fd = -1, .events = POLLIN | POLLERR | POLLHUP },
447 [POLLFD_MASTER] = { .fd = pty->master, .events = POLLIN | POLLERR | POLLHUP },
448 [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP }
449 };
450
451 /* We use signalfd and standard signals by handlers are blocked
452 * at all
453 */
454 sigfillset(&ourset);
455 if (sigprocmask(SIG_BLOCK, &ourset, NULL))
456 return -errno;
457
458 sigemptyset(&ourset);
459 sigaddset(&ourset, SIGCHLD);
460 sigaddset(&ourset, SIGWINCH);
461 sigaddset(&ourset, SIGALRM);
462 sigaddset(&ourset, SIGTERM);
463 sigaddset(&ourset, SIGINT);
464 sigaddset(&ourset, SIGQUIT);
465
466 if ((pty->sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0) {
467 rc = -errno;
468 goto done;
469 }
470
471 pfd[POLLFD_SIGNAL].fd = pty->sigfd;
472 pty->poll_timeout = -1;
473
474 while (!pty->delivered_signal) {
475 size_t i;
4d5b2fed
KZ
476 int errsv, timeout;
477
478 DBG(IO, ul_debugobj(pty, "--poll() loop--"));
479
480 /* note, callback usually updates @next_callback_time */
481 if (timerisset(&pty->next_callback_time)) {
482 struct timeval now;
483
484 DBG(IO, ul_debugobj(pty, " callback requested"));
485 gettime_monotonic(&now);
486 if (timercmp(&now, &pty->next_callback_time, >)) {
487 rc = mainloop_callback(pty);
488 if (rc)
489 break;
490 }
491 }
492
493 /* set timeout */
494 if (timerisset(&pty->next_callback_time)) {
495 struct timeval now, rest;
496
497 gettime_monotonic(&now);
498 timersub(&pty->next_callback_time, &now, &rest);
499 timeout = (rest.tv_sec * 1000) + (rest.tv_usec / 1000);
500 } else
501 timeout = pty->poll_timeout;
6954895c 502
4d5b2fed
KZ
503 /* wait for input, signal or timeout */
504 DBG(IO, ul_debugobj(pty, "calling poll() [timeout=%dms]", timeout));
505 ret = poll(pfd, ARRAY_SIZE(pfd), timeout);
6954895c 506
6954895c
KZ
507 errsv = errno;
508 DBG(IO, ul_debugobj(pty, "poll() rc=%d", ret));
509
4d5b2fed 510 /* error */
6954895c
KZ
511 if (ret < 0) {
512 if (errsv == EAGAIN)
513 continue;
514 rc = -errno;
515 break;
516 }
4d5b2fed
KZ
517
518 /* timeout */
6954895c 519 if (ret == 0) {
4d5b2fed
KZ
520 if (timerisset(&pty->next_callback_time)) {
521 rc = mainloop_callback(pty);
522 if (rc == 0)
523 continue;
524 } else
525 rc = 0;
526
984082fa 527 DBG(IO, ul_debugobj(pty, "leaving poll() loop [timeout=%d, rc=%d]", timeout, rc));
6954895c
KZ
528 break;
529 }
4d5b2fed 530 /* event */
6954895c
KZ
531 for (i = 0; i < ARRAY_SIZE(pfd); i++) {
532 rc = 0;
533
534 if (pfd[i].revents == 0)
535 continue;
536
537 DBG(IO, ul_debugobj(pty, " active pfd[%s].fd=%d %s %s %s",
538 i == POLLFD_STDIN ? "stdin" :
539 i == POLLFD_MASTER ? "master" :
540 i == POLLFD_SIGNAL ? "signal" : "???",
541 pfd[i].fd,
542 pfd[i].revents & POLLIN ? "POLLIN" : "",
543 pfd[i].revents & POLLHUP ? "POLLHUP" : "",
544 pfd[i].revents & POLLERR ? "POLLERR" : ""));
545 switch (i) {
546 case POLLFD_STDIN:
547 case POLLFD_MASTER:
548 /* data */
549 if (pfd[i].revents & POLLIN)
550 rc = handle_io(pty, pfd[i].fd, &eof);
551 /* EOF maybe detected by two ways:
552 * A) poll() return POLLHUP event after close()
553 * B) read() returns 0 (no data) */
554 if ((pfd[i].revents & POLLHUP) || eof) {
555 DBG(IO, ul_debugobj(pty, " ignore FD"));
556 pfd[i].fd = -1;
557 if (i == POLLFD_STDIN) {
558 write_eof_to_child(pty);
559 DBG(IO, ul_debugobj(pty, " ignore STDIN"));
560 }
561 }
562 continue;
563 case POLLFD_SIGNAL:
564 rc = handle_signal(pty, pfd[i].fd);
565 break;
566 }
567 if (rc)
568 break;
569 }
570 }
571
572done:
573 if (pty->sigfd != -1)
574 close(pty->sigfd);
575 pty->sigfd = -1;
576
577 /* restore original setting */
578 sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
579
580 DBG(IO, ul_debug("poll() done [signal=%d, rc=%d]", pty->delivered_signal, rc));
581 return rc;
582}
583
584#ifdef TEST_PROGRAM_PTY
585/*
586 * $ make test_pty
587 * $ ./test_pty
588 *
589 * ... and see for example tty(1) or "ps afu"
590 */
bdd43357 591static void child_sigstop(void *data __attribute__((__unused__)), pid_t child)
6954895c 592{
6954895c 593 kill(getpid(), SIGSTOP);
bdd43357 594 kill(child, SIGCONT);
6954895c
KZ
595}
596
597int main(int argc, char *argv[])
598{
6954895c
KZ
599 struct ul_pty_callbacks *cb;
600 const char *shell, *command = NULL, *shname = NULL;
601 int caught_signal = 0;
bdd43357 602 pid_t child;
8cf448e9 603 struct ul_pty *pty;
6954895c
KZ
604
605 shell = getenv("SHELL");
606 if (shell == NULL)
607 shell = _PATH_BSHELL;
608 if (argc == 2)
609 command = argv[1];
610
611 ul_pty_init_debug(0);
612
8cf448e9
KZ
613 pty = ul_new_pty(isatty(STDIN_FILENO));
614 if (!pty)
6954895c
KZ
615 err(EXIT_FAILURE, "failed to allocate PTY handler");
616
8cf448e9 617 cb = ul_pty_get_callbacks(pty);
6954895c
KZ
618 cb->child_sigstop = child_sigstop;
619
8cf448e9 620 if (ul_pty_setup(pty))
6954895c
KZ
621 err(EXIT_FAILURE, "failed to create pseudo-terminal");
622
623 fflush(stdout); /* ??? */
624
bdd43357 625 switch ((int) (child = fork())) {
6954895c 626 case -1: /* error */
8cf448e9 627 ul_pty_cleanup(pty);
6954895c
KZ
628 err(EXIT_FAILURE, "cannot create child process");
629 break;
630
631 case 0: /* child */
8cf448e9 632 ul_pty_init_slave(pty);
6954895c
KZ
633
634 signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */
635
636 shname = strrchr(shell, '/');
637 shname = shname ? shname + 1 : shell;
638
639 if (command)
640 execl(shell, shname, "-c", command, NULL);
641 else
642 execl(shell, shname, "-i", NULL);
643 err(EXIT_FAILURE, "failed to execute %s", shell);
644 break;
645
646 default:
647 break;
648 }
649
650 /* parent */
8cf448e9 651 ul_pty_set_child(pty, child);
6954895c
KZ
652
653 /* this is the main loop */
8cf448e9 654 ul_pty_proxy_master(pty);
6954895c
KZ
655
656 /* all done; cleanup and kill */
8cf448e9 657 caught_signal = ul_pty_get_delivered_signal(pty);
6954895c 658
8cf448e9
KZ
659 if (!caught_signal && ul_pty_get_child(pty) != (pid_t)-1)
660 ul_pty_wait_for_child(pty); /* final wait */
6954895c 661
8cf448e9 662 if (caught_signal && ul_pty_get_child(pty) != (pid_t)-1) {
6954895c 663 fprintf(stderr, "\nSession terminated, killing shell...");
bdd43357 664 kill(child, SIGTERM);
6954895c 665 sleep(2);
bdd43357 666 kill(child, SIGKILL);
6954895c
KZ
667 fprintf(stderr, " ...killed.\n");
668 }
669
8cf448e9
KZ
670 ul_pty_cleanup(pty);
671 ul_free_pty(pty);
6954895c
KZ
672 return EXIT_SUCCESS;
673}
674
675#endif /* TEST_PROGRAM */
676