-
Notifications
You must be signed in to change notification settings - Fork 19
/
low-level.R
1337 lines (1215 loc) · 37.8 KB
/
low-level.R
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
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#' Create a process handle
#'
#' @param pid Process id. Integer scalar. `NULL` means the current R
#' process.
#' @param time Start time of the process. Usually `NULL` and ps will query
#' the start time.
#' @return `ps_handle()` returns a process handle (class `ps_handle`).
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
ps_handle <- function(pid = NULL, time = NULL) {
if (!is.null(pid)) pid <- assert_pid(pid)
if (!is.null(time)) assert_time(time)
.Call(psll_handle, pid, time)
}
#' @rdname ps_handle
#' @export
as.character.ps_handle <- function(x, ...) {
pieces <- .Call(psll_format, x)
paste0("<ps::ps_handle> PID=", pieces[[2]], ", NAME=", pieces[[1]],
", AT=", format_unix_time(pieces[[3]]))
}
#' @param x Process handle.
#' @param ... Not used currently.
#'
#' @rdname ps_handle
#' @export
format.ps_handle <- function(x, ...) {
as.character(x, ...)
}
#' @rdname ps_handle
#' @export
print.ps_handle <- function(x, ...) {
cat(format(x, ...), "\n", sep = "")
invisible(x)
}
#' Pid of a process handle
#'
#' This function works even if the process has already finished.
#'
#' @param p Process handle.
#' @return Process id.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_pid(p)
#' ps_pid(p) == Sys.getpid()
ps_pid <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_pid, p)
}
#' Start time of a process
#'
#' The pid and the start time pair serves as the identifier of the process,
#' as process ids might be reused, but the chance of starting two processes
#' with identical ids within the resolution of the timer is minimal.
#'
#' This function works even if the process has already finished.
#'
#' @param p Process handle.
#' @return `POSIXct` object, start time, in GMT.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_create_time(p)
ps_create_time <- function(p = ps_handle()) {
assert_ps_handle(p)
format_unix_time(.Call(psll_create_time, p))
}
#' Checks whether a process is running
#'
#' It returns `FALSE` if the process has already finished.
#'
#' It uses the start time of the process to work around pid reuse. I.e.
# it returns the correct answer, even if the process has finished and
# its pid was reused.
#'
#' @param p Process handle.
#' @return Logical scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_is_running(p)
ps_is_running <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_is_running, p)
}
#' Parent pid or parent process of a process
#'
#' `ps_ppid()` returns the parent pid, `ps_parent()` returns a `ps_handle`
#' of the parent.
#'
#' On POSIX systems, if the parent process terminates, another process
#' (typically the pid 1 process) is marked as parent. `ps_ppid()` and
#' `ps_parent()` will return this process then.
#'
#' Both `ps_ppid()` and `ps_parent()` work for zombie processes.
#'
#' @param p Process handle.
#' @return `ps_ppid()` returns and integer scalar, the pid of the parent
#' of `p`. `ps_parent()` returns a `ps_handle`.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_ppid(p)
#' ps_parent(p)
ps_ppid <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_ppid, p)
}
#' @rdname ps_ppid
#' @export
ps_parent <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_parent, p)
}
#' Process name
#'
#' The name of the program, which is typically the name of the executable.
#'
#' On Unix this can change, e.g. via an exec*() system call.
#'
#' `ps_name()` works on zombie processes.
#'
#' @param p Process handle.
#' @return Character scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_name(p)
#' ps_exe(p)
#' ps_cmdline(p)
ps_name <- function(p = ps_handle()) {
assert_ps_handle(p)
n <- .Call(psll_name, p)
if (nchar(n) >= 15) {
## On UNIX the name gets truncated to the first 15 characters.
## If it matches the first part of the cmdline we return that
## one instead because it's usually more explicative.
## Examples are "gnome-keyring-d" vs. "gnome-keyring-daemon".
## In addition, under qemu (e.g. in cross-platform Docker), the
## first entry is qemu and the second entry is the file name
cmdline <- tryCatch(
ps_cmdline(p),
error = function(e) NULL
)
if (!is.null(cmdline) && length(cmdline) > 0L) {
exname <- basename(cmdline[1])
if (str_starts_with(exname, n)) {
n <- exname
} else if (grepl("qemu", exname) && length(cmdline) >= 2 &&
str_starts_with(exname2 <- basename(cmdline[2]), n)) {
n <- exname2
}
}
}
n
}
#' Full path of the executable of a process
#'
#' Path to the executable of the process. May also be an empty string or
#' `NA` if it cannot be determined.
#'
#' For a zombie process it throws a `zombie_process` error.
#'
#' @param p Process handle.
#' @return Character scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_name(p)
#' ps_exe(p)
#' ps_cmdline(p)
ps_exe <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_exe, p)
}
#' Command line of the process
#'
#' Command line of the process, i.e. the executable and the command line
#' arguments, in a character vector. On Unix the program might change its
#' command line, and some programs actually do it.
#'
#' For a zombie process it throws a `zombie_process` error.
#'
#' @param p Process handle.
#' @return Character vector.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_name(p)
#' ps_exe(p)
#' ps_cmdline(p)
ps_cmdline <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_cmdline, p)
}
#' Current process status
#'
#' One of the following:
#' * `"idle"`: Process being created by fork, or process has been sleeping
#' for a long time. macOS only.
#' * `"running"`: Currently runnable on macOS and Windows. Actually
#' running on Linux.
#' * `"sleeping"` Sleeping on a wait or poll.
#' * `"disk_sleep"` Uninterruptible sleep, waiting for an I/O operation
#' (Linux only).
#' * `"stopped"` Stopped, either by a job control signal or because it
#' is being traced.
#' * `"uninterruptible"` Process is in uninterruptible wait. macOS only.
#' * `"tracing_stop"` Stopped for tracing (Linux only).
#' * `"zombie"` Zombie. Finished, but parent has not read out the exit
#' status yet.
#' * `"dead"` Should never be seen (Linux).
#' * `"wake_kill"` Received fatal signal (Linux only).
#' * `"waking"` Paging (Linux only, not valid since the 2.6.xx kernel).
#'
#' It might return `NA_character_` on macOS.
#'
#' Works for zombie processes.
#'
#' @section Note on macOS:
#' On macOS `ps_status()` often falls back to calling the external `ps`
#' program, because macOS does not let R access the status of most other
#' processes. Notably, it is usually able to access the status of other R
#' processes.
#'
#' The external `ps` program always runs as the root user, and
#' it also has special entitlements, so it can typically access the status
#' of most processes.
#'
#' If this behavior is problematic for you, e.g. because calling an
#' external program is too slow, set the `ps.no_external_ps` option to
#' `TRUE`:
#' ```
#' options(ps.no_external_ps = TRUE)
#' ```
#' Note that setting this option to `TRUE` will cause `ps_status()` to
#' return `NA_character_` for most processes.
#'
#' @param p Process handle.
#' @return Character scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_status(p)
ps_status <- function(p = ps_handle()) {
assert_ps_handle(p)
ret <- .Call(psll_status, p)
if (is.na(ret) && ps_os_type()[["MACOS"]] &&
!isTRUE(getOption("ps.no_external_ps"))) {
ret <- ps_status_macos_ps(ps_pid(p))
}
ret
}
#' Owner of the process
#'
#' The name of the user that owns the process. On Unix it is calculated
#' from the real user id.
#'
#' On Unix, a numeric uid id returned if the uid is not in the user
#' database, thus a username cannot be determined.
#'
#' Works for zombie processes.
#'
#' @param p Process handle.
#' @return String scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_username(p)
ps_username <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_username, p)
}
#' Process current working directory as an absolute path.
#'
#' For a zombie process it throws a `zombie_process` error.
#'
#' @param p Process handle.
#' @return String scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_cwd(p)
ps_cwd <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_cwd, p)
}
#' User ids and group ids of the process
#'
#' User ids and group ids of the process. Both return integer vectors with
#' names: `real`, `effective` and `saved`.
#'
#' Both work for zombie processes.
#'
#' They are not implemented on Windows, they throw a `not_implemented`
#' error.
#'
#' @param p Process handle.
#' @return Named integer vector of length 3, with names: `real`,
#' `effective` and `saved`.
#'
#' @seealso [ps_username()] returns a user _name_ and works on all
#' platforms.
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_uids(p)
#' ps_gids(p)
ps_uids <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_uids, p)
}
#' @rdname ps_uids
#' @export
ps_gids <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_gids, p)
}
#' Terminal device of the process
#'
#' Returns the terminal of the process. Not implemented on Windows, always
#' returns `NA_character_`. On Unix it returns `NA_character_` if the
#' process has no terminal.
#'
#' Works for zombie processes.
#'
#' @param p Process handle.
#' @return Character scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_terminal(p)
ps_terminal <- function(p = ps_handle()) {
assert_ps_handle(p)
ttynr <- .Call(psll_terminal, p)
if (is.character(ttynr)) {
ttynr
} else if (is.na(ttynr)) {
NA_character_
} else {
tmap <- get_terminal_map()
tmap[[as.character(ttynr)]]
}
}
#' Environment variables of a process
#'
#' `ps_environ()` returns the environment variables of the process, in a
#' named vector, similarly to the return value of `Sys.getenv()`
#' (without arguments).
#'
#' Note: this usually does not reflect changes made after the process
#' started.
#'
#' `ps_environ_raw()` is similar to `p$environ()` but returns the
#' unparsed `"var=value"` strings. This is faster, and sometimes good
#' enough.
#'
#' These functions throw a `zombie_process` error for zombie processes.
#'
#' @section macOS issues:
#'
#' `ps_environ()` usually does not work on macOS nowadays. This is because
#' macOS does not allow reading the environment variables of another
#' process. Accoding to the Darwin source code, `ps_environ` will work is
#' one of these conditions hold:
#'
#' * You are running a development or debug kernel, i.e. if you are
#' debugging the macOS kernel itself.
#' * The target process is same as the calling process.
#' * SIP if off.
#' * The target process is not restricted, e.g. it is running a binary
#' that was not signed.
#' * The calling process has the
#' `com.apple.private.read-environment-variables` entitlement. However
#' adding this entitlement to the R binary makes R crash on startup.
#'
#' Otherwise `ps_environ` will return an empty set of environment variables
#' on macOS.
#'
#' Issue 121 might have more information about this.
#'
#' @param p Process handle.
#' @return `ps_environ()` returns a named character vector (that has a
#' `Dlist` class, so it is printed nicely), `ps_environ_raw()` returns a
#' character vector.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' env <- ps_environ(p)
#' env[["R_HOME"]]
ps_environ <- function(p = ps_handle()) {
assert_ps_handle(p)
parse_envs(.Call(psll_environ, p))
}
#' @rdname ps_environ
#' @export
ps_environ_raw <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_environ, p)
}
#' Number of threads
#'
#' Throws a `zombie_process()` error for zombie processes.
#'
#' @param p Process handle.
#' @return Integer scalar.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_num_threads(p)
ps_num_threads <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_num_threads, p)
}
#' CPU times of the process
#'
#' All times are measured in seconds:
#' * `user`: Amount of time that this process has been scheduled in user
#' mode.
#' * `system`: Amount of time that this process has been scheduled in
#' kernel mode
#' * `children_user`: On Linux, amount of time that this process's
#' waited-for children have been scheduled in user mode.
#' * `children_system`: On Linux, Amount of time that this process's
#' waited-for children have been scheduled in kernel mode.
#'
#' Throws a `zombie_process()` error for zombie processes.
#'
#' @param p Process handle.
#' @return Named real vector or length four: `user`, `system`,
#' `children_user`, `children_system`. The last two are `NA` on
#' non-Linux systems.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_cpu_times(p)
#' proc.time()
ps_cpu_times <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_cpu_times, p)
}
#' Memory usage information
#'
#' @details
#'
#' `ps_memory_info()` returns information about memory usage.
#'
#' It returns a named vector. Portable fields:
#' * `rss`: "Resident Set Size", this is the non-swapped physical memory a
#' process has used (bytes). On UNIX it matches "top"‘s 'RES' column (see doc). On
#' Windows this is an alias for `wset` field and it matches "Memory"
#' column of `taskmgr.exe`.
#' * `vmem`: "Virtual Memory Size", this is the total amount of virtual
#' memory used by the process (bytes). On UNIX it matches "top"‘s 'VIRT' column
#' (see doc). On Windows this is an alias for the `pagefile` field and
#' it matches the "Working set (memory)" column of `taskmgr.exe`.
#'
#' Non-portable fields:
#' * `shared`: (Linux) memory that could be potentially shared with other
#' processes (bytes). This matches "top"‘s 'SHR' column (see doc).
#' * `text`: (Linux): aka 'TRS' (text resident set) the amount of memory
#' devoted to executable code (bytes). This matches "top"‘s 'CODE' column (see
#' doc).
#' * `data`: (Linux): aka 'DRS' (data resident set) the amount of physical
#' memory devoted to other than executable code (bytes). It matches "top"‘s
#' 'DATA' column (see doc).
#' * `lib`: (Linux): the memory used by shared libraries (bytes).
#' * `dirty`: (Linux): the amount of memory in dirty pages (bytes).
#' * `pfaults`: (macOS): number of page faults.
#' * `pageins`: (macOS): number of actual pageins.
#'
#' For the explanation of Windows fields see the
#' [PROCESS_MEMORY_COUNTERS_EX](https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-process_memory_counters_ex)
#' structure.
#'
#' `ps_memory_full_info()` returns all fields as `ps_memory_info()`, plus
#' additional information, but typically takes slightly longer to run, and
#' might not have access to some processes that `ps_memory_info()` can
#' query:
#'
#' * `uss`: Unique Set Size, this is the memory which is unique to a
#' process and which would be freed if the process was terminated right
#' now.
#' * `pss` (Linux only): Proportional Set Size, is the amount of memory
#' shared with other processes, accounted in a way that the amount is
#' divided evenly between the processes that share it. I.e. if a process
#' has 10 MBs all to itself and 10 MBs shared with another process its
#' PSS will be 15 MBs.
#' * `swap` (Linux only): amount of memory that has been swapped out to
#' disk.
#'
#' They both throw a `zombie_process()` error for zombie processes.
#'
#' @param p Process handle.
#' @return Named real vector.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_handle()
#' p
#' ps_memory_info(p)
#' ps_memory_full_info(p)
ps_memory_info <- function(p = ps_handle()) {
assert_ps_handle(p)
.Call(psll_memory_info, p)
}
#' @export
#' @rdname ps_memory_info
ps_memory_full_info <- function(p = ps_handle()) {
assert_ps_handle(p)
type <- ps_os_type()
if (type[["LINUX"]]) {
match <- function(re) {
mt <- gregexpr(re, smaps, perl = TRUE)[[1]]
st <- substring(
smaps,
attr(mt, "capture.start"),
attr(mt, "capture.start") + attr(mt, "capture.length") - 1
)
sum(as.integer(st), na.rm = TRUE) * 1024
}
info <- ps_memory_info(p)
smaps <- .Call(ps__memory_maps, p)
info[["uss"]] <- match("\nPrivate.*:\\s+(\\d+)")
info[["pss"]] <- match("\nPss:\\s+(\\d+)")
info[["swap"]] <- match("\nSwap:\\s+(\\d+)")
info
} else if (type[["MACOS"]]) {
info <- ps_memory_info(p)
info[["uss"]] <- .Call(psll_memory_uss, p)
info
} else if (type[["WINDOWS"]]) {
info <- ps_memory_info(p)
info[["uss"]] <- .Call(psll_memory_uss, p)
info
}
}
process_signal_result <- function(p, res, err_msg) {
ok <- map_lgl(res, function(x) is.character(x) || is.null(x))
if (all(ok)) {
unlist(res)
} else {
for (i in which(!ok)) {
class(res[[i]]) <- res[[i]][[2]]
}
pids <- map_int(res[!ok], function(x) x[["pid"]] %||% NA_integer_)
nms <- map_chr(p[!ok], function(pp) {
tryCatch(ps_name(pp), error = function(e) "???")
})
pmsg <- paste0(pids, " (", nms, ")", collapse = ", ")
# put these classes at the end
common <- c("ps_error", "error", "condition")
cls <- c(
unique(setdiff(unlist(lapply(res[!ok], function(x) class(x))), common)),
common
)
err <- structure(
list(
message = paste0(
err_msg,
if (length(p) == 1) ": " else " some processes: ",
pmsg
),
results = res,
pid = pids
),
class = cls
)
stop(err)
}
}
#' Send signal to a process
#'
#' Send a signal to the process. Not implemented on Windows. See
#' [signals()] for the list of signals on the current platform.
#'
#' It checks if the process is still running, before sending the signal,
#' to avoid signalling the wrong process, because of pid reuse.
#'
#' @param p Process handle, or a list of process handles.
#' @param sig Signal number, see [signals()].
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' px <- processx::process$new("sleep", "10")
#' p <- ps_handle(px$get_pid())
#' p
#' ps_send_signal(p, signals()$SIGINT)
#' p
#' ps_is_running(p)
#' px$get_exit_status()
ps_send_signal <- function(p = ps_handle(), sig) {
p <- assert_ps_handle_or_handle_list(p)
assert_signal(sig)
res <- lapply(p, function(pp) {
tryCatch(
.Call(psll_send_signal, pp, sig),
error = function(e) e
)
})
process_signal_result(p, res, "Failed to send signal to")
}
#' Suspend (stop) the process
#'
#' Suspend process execution with `SIGSTOP` preemptively checking
#' whether PID has been reused. On Windows this has the effect of
#' suspending all process threads.
#'
#' @param p Process handle or a list of process handles.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' px <- processx::process$new("sleep", "10")
#' p <- ps_handle(px$get_pid())
#' p
#' ps_suspend(p)
#' ps_status(p)
#' ps_resume(p)
#' ps_status(p)
#' ps_kill(p)
ps_suspend <- function(p = ps_handle()) {
p <- assert_ps_handle_or_handle_list(p)
res <- lapply(p, function(pp) {
tryCatch(
.Call(psll_suspend, pp),
error = function(e) e
)
})
process_signal_result(p, res, "Failed to suspend")
}
#' Resume (continue) a stopped process
#'
#' Resume process execution with SIGCONT preemptively checking
#' whether PID has been reused. On Windows this has the effect of resuming
#' all process threads.
#'
#' @param p Process handle or a list of process handles.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' px <- processx::process$new("sleep", "10")
#' p <- ps_handle(px$get_pid())
#' p
#' ps_suspend(p)
#' ps_status(p)
#' ps_resume(p)
#' ps_status(p)
#' ps_kill(p)
ps_resume <- function(p = ps_handle()) {
p <- assert_ps_handle_or_handle_list(p)
res <- lapply(p, function(pp) {
tryCatch(
.Call(psll_resume, pp),
error = function(e) e
)
})
process_signal_result(p, res, "Failed to resume")
}
#' Terminate a Unix process
#'
#' Send a `SIGTERM` signal to the process. Not implemented on Windows.
#'
#' Checks if the process is still running, to work around pid reuse.
#'
#' @param p Process handle or a list of process handles.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' px <- processx::process$new("sleep", "10")
#' p <- ps_handle(px$get_pid())
#' p
#' ps_terminate(p)
#' p
#' ps_is_running(p)
#' px$get_exit_status()
ps_terminate <- function(p = ps_handle()) {
p <- assert_ps_handle_or_handle_list(p)
res <- lapply(p, function(pp) {
tryCatch(
.Call(psll_terminate, pp),
error = function(e) e
)
})
process_signal_result(p, res, "Failed to terminate")
}
#' Kill one or more processes
#'
#' Kill the process with SIGKILL preemptively checking whether PID has
#' been reused. On Windows it uses `TerminateProcess()`.
#'
#' Note that since ps version 1.8, `ps_kill()` does not error if the
#' `p` process (or some processes if `p` is a list) are already terminated.
#'
#' @param p Process handle, or a list of process handles.
#' @param grace Grace period, in milliseconds, used on Unix. If it is not
#' zero, then `ps_kill()` first sends a `SIGTERM` signal to all processes
#' in `p`. If some proccesses do not terminate within `grace`
#' milliseconds after the `SIGTERM` signal, `ps_kill()` kills them by
#' sending `SIGKILL` signals.
#' @return Character vector, with one element for each process handle in
#' `p`. If the process was already dead before `ps_kill()` tried to kill
#' it, the corresponding return value is `"dead"`. If `ps_kill()` just
#' killed it, it is `"killed"`.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ps::ps_os_type()["POSIX"] && ! ps:::is_cran_check()
#' px <- processx::process$new("sleep", "10")
#' p <- ps_handle(px$get_pid())
#' p
#' ps_kill(p)
#' p
#' ps_is_running(p)
#' px$get_exit_status()
ps_kill <- function(p = ps_handle(), grace = 200) {
p <- assert_ps_handle_or_handle_list(p)
grace <- assert_grace(grace)
if (ps_os_type()[["WINDOWS"]]) {
res <- lapply(p, function(pp) {
tryCatch({
if (ps_is_running(pp)) {
.Call(psll_kill, pp, 0L)
"killed"
} else {
"dead"
}
}, error = function(e) {
if (inherits(e, "no_such_process")) "dead" else e
})
})
} else {
res <- call_with_cleanup(psll_kill, p, grace)
}
process_signal_result(p, res, "Failed to kill")
}
#' List of child processes (process objects) of the process. Note that
#' this typically requires enumerating all processes on the system, so
#' it is a costly operation.
#'
#' @param p Process handle.
#' @param recursive Whether to include the children of the children, etc.
#' @return List of `ps_handle` objects.
#'
#' @family process handle functions
#' @export
#' @importFrom utils head tail
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' p <- ps_parent(ps_handle())
#' ps_children(p)
ps_children <- function(p = ps_handle(), recursive = FALSE) {
assert_ps_handle(p)
assert_flag(recursive)
mypid <- ps_pid(p)
mytime <- ps_create_time(p)
map <- ps_ppid_map()
ret <- list()
if (!recursive) {
for (i in seq_len(nrow(map))) {
if (map$ppid[i] == mypid) {
tryCatch({
child <- ps_handle(map$pid[i])
if (mytime <= ps_create_time(child)) {
ret <- c(ret, child)
} },
no_such_process = function(e) NULL,
zombie_process = function(e) NULL)
}
}
} else {
seen <- integer()
stack <- mypid
while (length(stack)) {
pid <- tail(stack, 1)
stack <- head(stack, -1)
if (pid %in% seen) next # nocov (happens _very_ rarely)
seen <- c(seen, pid)
child_pids <- map[ map[,2] == pid, 1]
for (child_pid in child_pids) {
tryCatch({
child <- ps_handle(child_pid)
if (mytime <= ps_create_time(child)) {
ret <- c(ret, child)
stack <- c(stack, child_pid)
} },
no_such_process = function(e) NULL,
zombie_process = function(e) NULL)
}
}
}
## This will throw if p has finished
ps_ppid(p)
ret
}
#' Query the ancestry of a process
#'
#' Query the parent processes recursively, up to the first process.
#' (On some platforms, like Windows, the process tree is not a tree
#' and may contain loops, in which case `ps_descent()` only goes up
#' until the first repetition.)
#'
#' @param p Process handle.
#' @return A list of process handles, starting with `p`, each one
#' is the parent process of the previous one.
#'
#' @family process handle functions
#' @export
#' @examplesIf ps::ps_is_supported() && ! ps:::is_cran_check()
#' ps_descent()
ps_descent <- function(p = ps_handle()) {
assert_ps_handle(p)
windows <- ps_os_type()[["WINDOWS"]]
branch <- list()
branch_pids <- integer()
current <- p
current_pid <- ps_pid(p)
if (windows) current_time <- ps_create_time(p)
while (TRUE) {
branch <- c(branch, list(current))
branch_pids <- c(branch_pids, current_pid)
parent <- fallback(ps_parent(current), NULL)
# Might fail on Windows, if the process does not exist
if (is.null(parent)) break;
# If the parent pid is the same, we stop.
# Also, Windows might have loops
parent_pid <- ps_pid(parent)
if (parent_pid %in% branch_pids) break;
# Need to check for pid reuse on Windows
if (windows) {
parent_time <- ps_create_time(parent)
if (current_time <= parent_time) break
current_time <- parent_time
}
current <- parent
current_pid <- parent_pid
}
branch
}
ps_ppid_map <- function() {
pids <- ps_pids()
processes <- not_null(lapply(pids, function(p) {
tryCatch(ps_handle(p), error = function(e) NULL) }))
pids <- map_int(processes, ps_pid)
ppids <- map_int(processes, function(p) fallback(ps_ppid(p), NA_integer_))
ok <- !is.na(ppids)
data_frame(
pid = pids[ok],
ppid = ppids[ok]
)
}
#' Number of open file descriptors
#'
#' Note that in some IDEs, e.g. RStudio or R.app on macOS, the IDE itself
#' opens files from other threads, in addition to the files opened from the
#' main R thread.
#'
#' For a zombie process it throws a `zombie_process` error.
#'
#' @param p Process handle.
#' @return Integer scalar.