File: run-program.impure.lisp

package info (click to toggle)
sbcl 2%3A2.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 52,008 kB
  • sloc: lisp: 535,135; ansic: 42,629; sh: 5,737; asm: 2,406; pascal: 717; makefile: 432; python: 56; cpp: 27
file content (529 lines) | stat: -rw-r--r-- 22,820 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
;;;; various RUN-PROGRAM tests with side effects

;;;; This software is part of the SBCL system. See the README file for
;;;; more information.
;;;;
;;;; While most of SBCL is derived from the CMU CL system, the test
;;;; files (like this one) were written from scratch after the fork
;;;; from CMU CL.
;;;;
;;;; This software is in the public domain and is provided with
;;;; absolutely no warranty. See the COPYING and CREDITS files for
;;;; more information.

(cl:in-package :cl-user)

(defun bin-pwd-ignoring-result ()
  (let ((initially-open-fds (directory "/proc/self/fd/*" :resolve-symlinks nil)))
    (sb-ext:run-program "pwd" nil :search t :input :stream :output :stream :wait nil)
    (length initially-open-fds)))

(with-test (:name (run-program :autoclose-streams)
            :broken-on :sbcl  ;; not reliable enough
            :skipped-on (not :linux))
  (let ((n-initially-open-fds (bin-pwd-ignoring-result)))
    (gc)
    (sb-sys:scrub-control-stack) ; Make sure we're not referencing the #<process>
    (gc) ; now nothing should reference the streams
    (assert (= (length (directory "/proc/self/fd/*" :resolve-symlinks nil))
               n-initially-open-fds))))

;; In addition to definitions lower down the impurity we're avoiding
;; is the sigchld handler that RUN-PROGRAM sets up, which interfers
;; with the manual unix process control done by the test framework
;; (sometimes the handler will manage to WAIT3 a process before
;; run-tests WAITPIDs it).

(with-test (:name (run-program :cat 1))
  (let* ((process (run-program "cat" '() :wait nil
                               :search t :output :stream :input :stream))
         (out (process-input process))
         (in (process-output process)))
    (unwind-protect
         (loop for i from 0 to 255 do
              (write-byte i out)
              (force-output out)
              (assert (= (read-byte in) i)))
      (process-close process))))

(with-test (:name (run-program :cat 2)
                  :skipped-on (not :sb-thread))
  ;; Tests that reading from a FIFO is interruptible.
  (let* ((process (run-program "cat" '() :search t
                               :wait nil :output :stream :input :stream))
         (in (process-input process))
         (out (process-output process))
         (sem (sb-thread:make-semaphore))
         (state :init)
         (writer (sb-thread:make-thread (lambda ()
                                          (sb-thread:wait-on-semaphore sem)
                                          (setf state :sleep)
                                          (sleep 2)
                                          (setf state :write)
                                          (write-line "OK" in)
                                          (finish-output in))))
         (timeout nil)
         (got nil)
         (unwind nil))
    (sb-thread:signal-semaphore sem)
    (handler-case
        (with-timeout 0.1
          (unwind-protect
               (setf got (read-line out))
            (setf unwind state)))
      (timeout ()
        (setf timeout t)))
    (assert (not got))
    (assert timeout)
    (assert (eq unwind :sleep))
    (sb-thread:join-thread writer)
    (assert (equal "OK" (read-line out)))))

(defclass buffer-stream (sb-gray:fundamental-binary-input-stream sb-gray:fundamental-binary-output-stream)
  ((buffer :initform (make-array 128
                                :element-type '(unsigned-byte 8)
                                :adjustable t
                                :fill-pointer 0))
   (mark :initform 0)))

(defmethod stream-element-type ((stream buffer-stream))
  '(unsigned-byte 8))

(defmethod sb-gray:stream-read-sequence ((stream buffer-stream) seq &optional (start 0) end)
  (let* ((buffer (slot-value stream 'buffer))
         (end (or end (length seq)))
         (mark (slot-value stream 'mark))
         (fill-pointer (fill-pointer buffer))
         (new-mark (+ mark (min fill-pointer (- end start)))))
    (setf (slot-value stream 'mark) new-mark)
    (replace seq buffer
             :start1 start :end1 end
             :start2 mark :end2 fill-pointer)
    (min end (+ start (- fill-pointer mark)))))

(defmethod sb-gray:stream-write-sequence ((stream buffer-stream) seq &optional (start 0) end)
  (let* ((buffer (slot-value stream 'buffer))
         (end (or end (length seq)))
         (fill-pointer (fill-pointer buffer))
         (new-fill (min (array-total-size buffer) (+ fill-pointer (- end start)))))
    (setf (fill-pointer buffer) new-fill)
    (replace buffer seq
             :start1 fill-pointer
             :start2 start :end2 end)
    seq))

(with-test (:name (run-program :cat 3))
  ;; User-defined binary input and output streams.
  (let ((in (make-instance 'buffer-stream))
        (out (make-instance 'buffer-stream))
        (data #(0 1 2 3 4 5 6 7 8 9 10 11 12)))
    (write-sequence data in)
    (let ((process (run-program "cat" '()
                                :search t
                                :wait t
                                :output out :input in))
          (buf (make-array (length data))))
      (declare (ignore process))
      (assert (= 13 (read-sequence buf out)))
      (assert (= 0 (read-sequence (make-array 8) out)))
      (assert (equalp buf data)))))

(with-test (:name (run-program :cat 4))
  ;; Null broadcast stream as output
  (let* ((process (run-program "cat" '() :wait nil
                               :search t
                               :output (make-broadcast-stream)
                               :input :stream))
         (in (process-input process)))
    (unwind-protect
         (progn
           (write-string "foobar" in)
           (close in)
           (process-wait process))
      (process-close process))))

;;; Test driving an external program (cat) through pipes wrapped in
;;; composite streams.

(require :sb-posix)

#-win32
(progn
  (defun make-pipe ()
    (multiple-value-bind (in out) (sb-posix:pipe)
      (let ((input (sb-sys:make-fd-stream in
                                          :input t
                                          :external-format :ascii
                                          :buffering :none :name "in"))
            (output (sb-sys:make-fd-stream out
                                           :output t
                                           :external-format :ascii
                                           :buffering :none :name "out")))
        (make-two-way-stream input output))))

  (defparameter *cat-in-pipe* (make-pipe))
  (defparameter *cat-in* (make-synonym-stream '*cat-in-pipe*))
  (defparameter *cat-out-pipe* (make-pipe))
  (defparameter *cat-out* (make-synonym-stream '*cat-out-pipe*)))

(with-test (:name (run-program :cat 5) :skipped-on :win32)
  (let ((cat (run-program "cat" nil :search t :input *cat-in* :output *cat-out*
                          :wait nil)))
    (dolist (test '("This is a test!"
                    "This is another test!"
                    "This is the last test...."))
      (write-line test *cat-in*)
      (assert (equal test (read-line *cat-out*))))
    (process-close cat)))

;;; The above test used to use ed, but there were buffering issues: on some platforms
;;; buffering of stdin and stdout depends on their TTYness, and ed isn't sufficiently
;;; agressive about flushing them. So, here's another test using :PTY.

#-win32
(unless (probe-file "/bin/ed") (push :no-bin-ed-installed *features*))

#-win32
(progn
  (defparameter *tmpfile* (scratch-file-name))

  (with-test (:name (run-program :/bin/ed) :skipped-on :no-bin-ed-installed)
    (with-open-file (f *tmpfile*
                       :direction :output
                       :if-exists :supersede)
      (write-line "bar" f))
    (unwind-protect
         (let* ((ed (run-program "/bin/ed" (list *tmpfile*) :wait nil :pty t))
                (ed-in (process-pty ed))
                (ed-out (process-pty ed)))
           (labels ((read-linish (stream)
                      (with-output-to-string (s)
                        (loop for c = (read-char stream)
                              while (and c (not (eq #\newline c)))
                              ;; Some eds like to send \r\n
                              do (unless (eq #\return c)
                                   (write-char c s)))))
                    (assert-ed (command response)
                      (when command
                        (write-line command ed-in)
                        (force-output ed-in))
                      (when response
                        (let ((got (read-linish ed-out)))
                          (unless (equal response got)
                            (error "wanted '~A' from ed, got '~A'" response got))))
                      ed))
             (assert-ed nil "4")
             (assert-ed ".s/bar/baz/g" nil)
             (assert-ed "w" "4")
             (assert-ed "q" nil)
             (process-wait ed)
             (with-open-file (f *tmpfile*)
               (assert (equal "baz" (read-line f))))))
      (delete-file *tmpfile*)))) ;; #-win32

;; Around 1.0.12 there was a regression when :INPUT or :OUTPUT was a
;; pathname designator.  Since these use the same code, it should
;; suffice to test just :INPUT.
(with-test (:name (run-program :input :output pathname))
  (with-scratch-file (file)
    (with-open-file (f file :direction :output)
      (setf file (truename file))
      (write-line "Foo" f))
    (assert (run-program "cat" ()
                         :input file :output t
                         :search t :wait t))))

;;; This used to crash on Darwin and trigger recursive lock errors on
;;; every platform.
;;; ...and now it triggers recursive lock errors on safepoint + linux.
(with-test (:name (run-program :stress) :skipped-on (:and :sb-safepoint :linux))
  ;; Do it a hundred times in batches of 10 so that with a low limit
  ;; of the number of processes the test can have a chance to pass.
  ;;
  ;; If #+sb-thread, then make this test even more brutal by calling
  ;; RUN-PROGRAM in new threads. This is neither good nor bad as far as
  ;; total run time, but good in that it excercises RUN-PROGRAM
  ;; from other than the main thread.
  (flet ((start-run ()
           (run-program "echo"
                        '("It would be nice if this didn't crash.")
                        :search t :wait nil :output nil)))
    (dotimes (i 10)
      (mapc #'process-wait
            #+sb-thread (mapcar #'sb-thread:join-thread
                                (loop repeat 10
                                      collect (sb-thread:make-thread #'start-run)))
            #-sb-thread (loop repeat 10 collect (start-run))))))

(with-test (:name (run-program :pty-stream)
            :skipped-on :win32
            :broken-on :darwin)
  (let (process
        stream)
    (assert (search "OK"
                   (handler-bind
                       ((timeout (lambda (c)
                                   c
                                   (format t "~a ~a~%" process
                                           (when stream
                                             (get-output-stream-string stream))))))
                     (with-timeout 60
                       (with-output-to-string (s)
                         (setf stream s)
                         (setf process
                               (run-program "/bin/sh" '("-c" "echo OK; exit 42") :pty s
                                                                                 :wait nil))
                         (process-wait process)
                         (assert (= (process-exit-code process) 42))
                         s)))))))

;; Check whether RUN-PROGRAM puts its child process into the foreground
;; when stdin is inherited. If it fails to do so we will receive a SIGTTIN.
;;
;; We can't check for the signal itself since run-program.c resets the
;; forked process' signal mask to defaults. But the default is `stop'
;; of which we can be notified asynchronously by providing a status hook.
(with-test (:name (run-program :inherit-stdin) :fails-on :win32
                  :skipped-on :no-bin-ed-installed)
  (let (stopped)
    (flet ((status-hook (proc)
             (case (process-status proc)
               (:stopped (setf stopped t)))))
      (let ((proc (run-program "/bin/ed" nil :search nil :wait nil
                               :input t :output t
                               :status-hook #'status-hook)))
        ;; Give the program a generous time to generate the SIGTTIN.
        ;; If it hasn't done so after that time we can consider it
        ;; to be working (i.e. waiting for input without generating SIGTTIN).
        (sleep 0.5)
        ;; either way we have to signal it to terminate
        (process-kill proc sb-posix:sigterm)
        (process-close proc)
        (assert (not stopped))))))


;; Check that in when you do run-program with :wait t that causes
;; encoding error, it does not affect the following run-program
(with-test (:name (run-program :clean-exit-after-encoding-error))
  (let ((had-error-p nil))
    (flet ((barf (&optional (format :default))
             (with-output-to-string (stream)
               (run-program (sb-ext:posix-getenv "SBCL_RUNTIME")
                            '("--core"
                              (sb-ext:posix-getenv "SBCL_CORE")
                              "--disable-ldb" "--noinform" "--no-sysinit" "--no-userinit" "--noprint" "--disable-debugger"
                              "--eval"
                              "(mapc (lambda (b) (write-byte b *standard-output*)) '(#x20 #xfe #xff #x0))"
                              "--quit" )
                            :output stream
                            :external-format format)))
           (no-barf ()
             (with-output-to-string (stream)
               (run-program "echo"
                            '("This is a test")
                            :search t
                            :output stream))))
      (handler-case
          (barf :utf-8)
        (error ()
          (setq had-error-p t)))
      (assert had-error-p)
      ;; now run the harmless program
      (setq had-error-p nil)
      (handler-case
          (no-barf)
        (error ()
          (setq had-error-p t)))
      (assert (not had-error-p)))))

(with-test (:name (run-program :no-such-thing))
  (assert (search "Couldn't execute"
                  (handler-case
                      (progn (run-program "no-such-program-we-hope" '()) nil)
                    (error (e)
                      (princ-to-string e))))))

(with-test (:name (run-program :not-executable))
  (assert (search "Couldn't execute"
                  (handler-case
                      (progn (run-program "run-program.impure.lisp" '()) nil)
                    (error (e)
                      (princ-to-string e))))))

#-win32
(with-test (:name (run-program :if-input-does-not-exist))
  (let ((file (pathname (sb-posix:mktemp "rpXXXXXX"))))
    (when (and (boundp 'run-tests::*allowed-inputs*)
               ;; If the permitted inputs are :ANY then leave it be
               (listp (symbol-value 'run-tests::*allowed-inputs*)))
      (push (namestring file) (symbol-value 'run-tests::*allowed-inputs*)))
    (assert (null (run-program "cat" '() :search t :input file)))
    (assert (null (run-program "cat" '() :search t :output #.(or *compile-file-truename*
                                                                 *load-truename*)
                                      :if-output-exists nil)))))


(with-test (:name (run-program :set-directory))
  (let* ((directory #-win32 "/"
                    #+win32 "c:\\")
         (out (process-output
               (run-program #-win32 "/bin/sh"
                            #-win32 '("-c" "pwd")
                            #+win32 "cmd.exe"
                            #+win32 '("/c" "cd")
                            :output :stream
                            :directory directory
                            :search t))))
    (assert
     (equal directory
            (string-right-trim '(#\Return) (read-line out))))))

(with-test (:name (run-program :directory-nil))
  (run-program #-win32 "/bin/sh"
               #-win32 '("-c" "pwd")
               #+win32 "cmd.exe"
               #+win32 '("/c" "cd")
               :directory nil
               :search t))

(with-test (:name (run-program :bad-options))
  (assert-error
   (run-program #-win32 "/bin/sh"
                #-win32 '("-c" "pwd")
                #+win32 "cmd.exe"
                #+win32 '("/c" "cd")
                :search t
                :output :bad)))

(with-test (:name (run-program :stop+continue) :skipped-on :win32)
  (let ((process (run-program "cat" '() :search t :input :stream :wait nil)))
    (flet ((kill-and-check-status (signal expected-status)
             (process-kill process signal)
             (loop :repeat 2000
                :when (eq (process-status process) expected-status)
                :do (return)
                :do (sleep 1/100)
                :finally (error "~@<Process ~A did not change its ~
                                 status to ~S within 20 seconds.~@:>"
                                process expected-status))))
      (kill-and-check-status sb-posix:sigstop :stopped)
      (kill-and-check-status sb-posix:sigcont :running)
      (kill-and-check-status sb-posix:sigkill :signaled)
      (process-wait process)
      (assert (not (process-alive-p process))))))

#-win32
(with-test (:name (run-program :stop+continue :posix-kill))
  (let ((process (run-program "cat" '() :search t :input :stream :wait nil)))
    (flet ((kill-and-check-status (signal expected-status)
             (sb-posix:kill (process-pid process) signal)
             (loop :repeat 2000
                :when (eq (process-status process) expected-status)
                :do (return)
                :do (sleep 1/100)
                :finally (error "~@<Process ~A did not change its ~
                                 status to ~S within 20 seconds.~@:>"
                                process expected-status))))
      (kill-and-check-status sb-posix:sigstop :stopped)
      (kill-and-check-status sb-posix:sigcont :running)
      (kill-and-check-status sb-posix:sigkill :signaled)
      (process-wait process)
      (assert (not (process-alive-p process))))))

(defun malloc ()
  (let ((x (sb-alien:make-alien int 10000)))
    (sb-alien:free-alien x)
    1))

(with-test (:name (run-program :malloc-deadlock)
            :broken-on :sb-safepoint
            :skipped-on (or :ubsan (not :sb-thread) :win32 :android))
  (let* (stop
         (delay-between-gc
          (or #+freebsd
              (let* ((p (run-program "sysctl" '("hw.model") :search t :output :stream))
                     (output (process-output p))
                     (result (read-line output)))
                (close output)
                ;; With the default delay of 0 using FreeBSD on QEMU this test never
                ;; finished because the RUN-PROGRAM thread would never get scheduled
                ;; after its first entry into the loop.
                ;; It wasn't hung, it just wasn't getting CPU time. For me anyway.
                (when (search "QEMU" result)
                  .00000001)) ; 10 nanoseconds
              #+(and darwin arm64)
               0.01
              0))
         (threads (list*
                   (sb-thread:make-thread (lambda ()
                                            (loop until (progn (sb-thread:barrier (:read))
                                                               stop)
                                                  do
                                                  (sleep delay-between-gc)
                                                  (gc :full t))))
                   (loop repeat 3
                         collect
                         (sb-thread:make-thread
                          (lambda ()
                            (loop until (progn (sb-thread:barrier (:read))
                                               stop)
                                  do (malloc))))))))
    (loop with dot = (get-internal-real-time)
          with end = (+ dot (* internal-time-units-per-second 4))
          for time = (get-internal-real-time)
          while (> end time)
          do
          (when (> (- time dot)
                   (/ internal-time-units-per-second 10))
            (setf dot time)
            (write-char #\.)
            (finish-output))
          (with-output-to-string (str)
            (run-program "uname" ()
                         :output str
                         :search t
                         :wait t)))
    (setf stop t)
    (sb-thread:barrier (:write))
    (mapc #'sb-thread:join-thread threads)))

(with-test (:name (run-program :child-fd-leak)
            :skipped-on (or :openbsd :win32))
  (when (probe-file "/dev/fd")
    (with-open-file (stream "/dev/null")
      (let* ((fd (sb-sys:fd-stream-fd stream))
             (process (run-program "test" (list "-e" (format nil "/dev/fd/~a" fd))
                                   :search t)))
        (assert (not (zerop (process-exit-code process))))))))

;; PROCESS-CLOSE's contract is "close the streams and stop updating
;; the process", but SBCL should still internally watch the process:
;; to do otherwise will cause an accumulation of zombies.
(with-test (:name (run-program :minimizes-zombies)
            ;; 1. IDK whether Windows has the zombies, &
            ;; 2. this test runs ps(1) to look for them,
            ;; so skip on Windows.
            :skipped-on :win32)
  (flet ((gather-process-output (&rest argv)
           (with-output-to-string (s)
             (run-program (first argv) (rest argv) :output s :search t)))
         (make-zombies (count &rest argv)
           (loop repeat count
                 for proc = (run-program (first argv) (rest argv)
                                :wait nil :search t)
                 collect (process-pid proc)
                 do (process-close proc)
                    (process-kill proc #+unix sb-unix:sigterm #+win32 :ignore))))
    (let ((pids (make-zombies 3 "sleep" "300")))
      ;; Give a little time for signal delivery (to kids, and to SBCL).
      (sleep 1/100)
      (let ((ps-output
             (gather-process-output
              ;; Test written in 2024; these ps(1) flags are specified
              ;; as early as 2008, so hopefully everybody has 'em.
              ;; https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/
              "ps" "-opid" "-oppid" "-oargs" (format nil "-p~{~D~^,~}" pids))))
        ;; Should be just a header line.
        (assert (= 1 (count #\newline ps-output)) ()
                "Zombies found in ps(1) output (SBCL pid ~D):~%~A"
                (sb-unix:unix-getpid) ps-output)))))