/
__init__.py
2203 lines (1828 loc) · 88.8 KB
/
__init__.py
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
# vim: fileencoding=utf-8
# Programmer friendly subprocess wrapper.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: October 11, 2018
# URL: https://executor.readthedocs.io
"""
Core functionality of the `executor` package.
If you're looking for an easy way to run external commands from Python take a
look at the :func:`execute()` function. When you need more flexibility consider
using the underlying :class:`ExternalCommand` class directly instead.
:func:`execute()` versus :class:`ExternalCommand`
-------------------------------------------------
In :mod:`executor` 1.x the :func:`execute()` function was the only interface
for external command execution. This had several drawbacks:
- The documentation for the :func:`execute()` function was getting way too
complex given all of the supported options and combinations.
- There was no way to execute asynchronous external commands (running in the
background) without sidestepping the complete :mod:`executor` module and
going straight for :class:`subprocess.Popen` (with all of the verbosity
that you get for free with :mod:`subprocess` :-).
- There was no way to prepare an external command without starting it
immediately, making it impossible to prepare a batch of external commands
before starting them (whether synchronously or asynchronously).
To solve these problems :mod:`executor` 2.x introduced the
:class:`ExternalCommand` class. This explains why :func:`execute()` is now a
trivial wrapper around :class:`ExternalCommand`: It's main purpose is to be an
easy to use shortcut that preserves compatibility with the old interface.
Classes and functions
---------------------
"""
# Standard library modules.
import errno
import logging
import os
import pipes
import pprint
import shlex
import signal
import subprocess
import sys
import tempfile
# External dependencies.
from humanfriendly import compact, concatenate, format, pluralize
from humanfriendly.terminal import connected_to_terminal
from property_manager import (
PropertyManager,
mutable_property,
required_property,
set_property,
writable_property,
)
from six import string_types, text_type
# Modules included in our package.
from executor.process import ControllableProcess
# Semi-standard module versioning.
__version__ = '21.2'
# Initialize a logger.
logger = logging.getLogger(__name__)
DEFAULT_ENCODING = 'UTF-8'
"""The default encoding of the standard input, output and error streams (a string)."""
DEFAULT_WORKING_DIRECTORY = os.curdir
"""
The default working directory for external commands (a string). Defaults to the
working directory of the current process using :data:`os.curdir`.
"""
DEFAULT_SHELL = 'bash'
"""
The default shell used to evaluate shell expressions (a string).
This variable isn't based on the ``$SHELL`` environment variable because:
1. Shells like ``sh``, ``dash``, ``bash`` and ``zsh`` all have their own
subtly incompatible semantics.
2. People regularly use shells like ``fish`` as their default login shell :-).
At an interactive prompt this is no problem (advanced users have obviously
learned to context switch) but when you're writing source code the last thing
you want to worry about is which shell is going to evaluate your commands! The
:mod:`executor` package expects this shell to support the following features:
- The ``-c`` option to evaluate a shell command provided as a command line
argument.
- The ``-`` argument to instruct the shell to read shell commands from its
standard input stream and evaluate those.
Apart from these two things nothing else is expected from the default shell so
you're free to customize it if you really want to write your shell commands in
``fish`` or ``zsh`` syntax :-).
"""
COMMAND_NOT_FOUND_CODES = (errno.ENOENT,)
"""Numeric error codes returned when a command isn't available on the system (a tuple of integers)."""
COMMAND_NOT_FOUND_STATUS = 127
"""The exit status used by shells when a command is not found (an integer)."""
IS_WINDOWS = sys.platform.startswith('win')
def execute(*command, **options):
"""
Execute an external command and make sure it succeeded.
:param command: All positional arguments are passed on to the constructor
of :class:`ExternalCommand`.
:param options: All keyword arguments are passed on to the constructor of
:class:`ExternalCommand`.
:returns: Refer to :func:`execute_prepared()`.
:raises: :exc:`ExternalCommandFailed` when the command exits with a
nonzero exit code (and :attr:`~ExternalCommand.check` is
:data:`True`).
If :attr:`~ExternalCommand.asynchronous` is :data:`True` then :func:`execute()`
will automatically start the external command for you using
:func:`~ExternalCommand.start()` (but it won't wait for it to end). If you
want to create an :class:`ExternalCommand` object instance without
immediately starting the external command then you can use
:class:`ExternalCommand` directly.
**Some examples**
By default the status code of the external command is returned as a boolean:
>>> from executor import execute
>>> execute('true')
True
However when an external command exits with a nonzero status code an
exception is raised, this is intended to "make it easy to do the right
thing" (never forget to check the status code of an external command
without having to write a lot of repetitive code):
>>> execute('false')
Traceback (most recent call last):
File "executor/__init__.py", line 124, in execute
cmd.start()
File "executor/__init__.py", line 516, in start
self.wait()
File "executor/__init__.py", line 541, in wait
self.check_errors()
File "executor/__init__.py", line 568, in check_errors
raise ExternalCommandFailed(self)
executor.ExternalCommandFailed: External command failed with exit code 1! (command: false)
What's also useful to know is that exceptions raised by :func:`execute()`
expose :attr:`~ExternalCommandFailed.command` and
:attr:`~ExternalCommandFailed.returncode` attributes. If you know a command
is likely to exit with a nonzero status code and you want :func:`execute()`
to simply return a boolean you can do this instead:
>>> execute('false', check=False)
False
"""
return execute_prepared(ExternalCommand(*command, **options))
def execute_prepared(command):
"""
The logic behind :func:`execute()` and :func:`.remote()`.
:param command: An :class:`ExternalCommand` object (or an object created
from a subclass with a compatible interface like for
example :class:`.RemoteCommand`).
:returns: The return value of this function depends on several options:
- If :attr:`~ExternalCommand.asynchronous` is :data:`True` the
constructed :class:`ExternalCommand` object is returned.
- If :attr:`~ExternalCommand.callback` is set the value of
:attr:`~ExternalCommand.result` is returned.
- If :attr:`~ExternalCommand.capture` is :data:`True` the value
of :attr:`ExternalCommand.output` is returned.
- By default the value of :attr:`~ExternalCommand.succeeded` is
returned.
:raises: See :func:`execute()` and :func:`.remote()`.
"""
if command.asynchronous:
command.start()
return command
else:
command.start()
command.wait()
if command.callback:
return command.result
elif command.capture:
return command.output
else:
return command.succeeded
class ExternalCommand(ControllableProcess):
"""
Programmer friendly :class:`subprocess.Popen` wrapper.
The :class:`ExternalCommand` class wraps :class:`subprocess.Popen` to make
it easier to do the right thing (the simplicity of :func:`os.system()` with
the robustness of :class:`subprocess.Popen`) and to provide additional
features (e.g. asynchronous command execution that preserves the ability to
provide input and capture output).
**Process manipulation**
:class:`ExternalCommand` inherits from :class:`~executor.process.ControllableProcess`
which means that all of the process manipulation supported by
:class:`~executor.process.ControllableProcess` is also supported by
:class:`ExternalCommand` objects.
**Context manager**
:class:`ExternalCommand` objects can be used as context managers by using
the :keyword:`with` statement:
- When the scope of the :keyword:`with` statement starts the
:func:`start()` method is called (if the external command
isn't already running).
- When the scope of the :keyword:`with` statement ends
:func:`~executor.process.ControllableProcess.terminate()` is called if
the command is still running. The :func:`load_output()` and
:func:`cleanup()` functions are used to cleanup after the external
command. If an exception isn't already being raised
:func:`check_errors()` is called to make sure the external command
succeeded.
.. _event callbacks:
**Event callbacks**
The :attr:`start_event`, :attr:`retry_event` and :attr:`finish_event`
properties can be set to callbacks (callable values like functions) to
subscribe to the corresponding events. The callback receives a single
positional argument which is the :class:`ExternalCommand` object.
The :attr:`start_event` and :attr:`finish_event` properties were
originally created for use in command pools, for example to report to
the operator when specific commands are started and when they finish.
The event handling is performed inside the :class:`ExternalCommand`
class though, so you're free to repurpose these events outside the
context of command pools.
"""
def __init__(self, *command, **options):
"""
Initialize an :class:`ExternalCommand` object.
:param command: Any positional arguments are converted to a list and
used to set :attr:`command`.
:param options: Keyword arguments can be used to conveniently override
the default values of :attr:`asynchronous`, :attr:`callback`,
:attr:`capture`, :attr:`capture_stderr`, :attr:`check`,
:attr:`directory`, :attr:`encoding`,
:attr:`environment`, :attr:`fakeroot`, :attr:`input`,
:attr:`~executor.process.ControllableProcess.logger`,
:attr:`merge_streams`, :attr:`really_silent`,
:attr:`shell`, :attr:`silent`, :attr:`stdout_file`,
:attr:`stderr_file`, :attr:`uid`, :attr:`user`,
:attr:`sudo` and :attr:`virtual_environment`. Keyword
argument that are not supported will raise
:exc:`TypeError` as usual.
The external command is not started until you call :func:`start()` or
:func:`wait()`.
"""
# Store the command and its arguments but make it possible for
# subclasses to redefine whether `command' is a required property.
self.command = list(command)
# Set properties based on keyword arguments.
super(ExternalCommand, self).__init__(**options)
# Initialize instance variables.
self.stdin_stream = CachedStream(self, 'stdin')
self.stdout_stream = CachedStream(self, 'stdout')
self.stderr_stream = CachedStream(self, 'stderr')
@mutable_property
def asynchronous(self):
"""
Enable asynchronous command execution.
If this option is :data:`True` (not the default) preparations are made
to execute the external command asynchronously (in the background).
This has several consequences:
- Calling :func:`start()` will start the external command but will
not block until the external command is finished, instead you are
responsible for calling :func:`wait()` at some later point in
time.
- When :attr:`input` is set its value will be written to a temporary
file and the standard input stream of the external command is
connected to read from the temporary file.
By using a temporary file the external command can consume its input
as fast or slow as it pleases without needing a separate thread or
process to "feed" the external command.
- When :class:`capture` is :data:`True` the standard output of the
external command is redirected to a temporary file whose contents are
read once the external command has finished.
By using a temporary file the external command can produce output as
fast or slow as it pleases without needing a thread or subprocess on
our side to consume the output in real time.
.. seealso:: :attr:`async` (backwards compatible alias)
"""
return False
@mutable_property
def buffer_size(self):
"""
Control the size of the stdin/stdout/stderr pipe buffers.
The value of :attr:`buffer_size` becomes the `bufsize` argument
that's passed to :class:`subprocess.Popen` by :func:`start()`.
If :data:`asynchronous` is :data:`True` and :attr:`buffered` is :data:`False`
the value of :attr:`buffer_size` defaults to 0 which means unbuffered,
in all other cases its value defaults to -1 which means to use the
system default buffer size.
"""
return 0 if self.asynchronous and not self.buffered else -1
@mutable_property
def buffered(self):
"""
Control whether command output is buffered to temporary files.
When :attr:`asynchronous` is :data:`True` and the standard output and/or error
streams are being captured, temporary files will be used to collect the
output. This enables the use of the :attr:`output`, :attr:`stdout` and
:attr:`stderr` properties to easily get the full output of the command
in a single string.
You can set :data:`buffered` to :data:`False` to disable the use of
temporary files, in this case :data:`subprocess.PIPE` is passed to
:class:`subprocess.Popen`. Once :attr:`is_running` is :data:`True` you
can use the :attr:`stdin`, :attr:`stdout` and/or :attr:`stderr`
properties to communicate with the command.
This enables runtime processing of the standard input, output and error
streams and makes it possible to run commands that never return but
keep producing output (for example ``xscreensaver-command -watch``).
Here's an example that sets :attr:`buffered` to :data:`False` and uses
the magic method :func:`__iter__()` to iterate over the lines of output
in realtime:
.. code-block:: python
# Run external commands when xscreensaver changes state.
import os
from executor import execute
known_states = set(['BLANK', 'LOCK', 'UNBLANK'])
while True:
options = dict(asynchronous=True, capture=True, buffered=False)
with execute('xscreensaver-command', '-watch', **options) as command:
for line in command:
tokens = line.split()
if tokens and tokens[0] in known_states:
value = os.environ.get('XSCREENSAVER_%s_COMMAND' % tokens[0])
if value:
execute(value)
Some sanity checks and error handling have been omitted from the
example above, in order to keep it simple, but I did test it and
it should actually work (at least it did for me):
.. code-block:: sh
$ export XSCREENSAVER_BLANK_COMMAND='echo $(date) - Screen is now blanked'
$ export XSCREENSAVER_LOCK_COMMAND='echo $(date) - Screen is now locked'
$ export XSCREENSAVER_UNBLANK_COMMAND='echo $(date) - Screen is now unblanked'
$ python xscreensaver-monitor.py
Sat Jan 20 16:03:07 CET 2018 - Screen is now blanked
Sat Jan 20 16:03:15 CET 2018 - Screen is now unblanked
Sat Jan 20 16:03:20 CET 2018 - Screen is now locked
Sat Jan 20 16:03:34 CET 2018 - Screen is now unblanked
"""
return True
@writable_property
def callback(self):
"""
Optional callback used to generate the value of :attr:`result`.
The :attr:`callback` and :attr:`result` properties were created for use
in command pools, where it can be useful to define how to process
(parse) a command's output when the command is constructed.
"""
@mutable_property
def capture(self):
"""
Enable capturing of the standard output stream.
If this option is :data:`True` (not the default) the standard output of
the external command is captured and made available to the caller via
:attr:`stdout` and :attr:`output`.
The standard error stream will not be captured, use :attr:`capture_stderr`
for that. You can also silence the standard error stream using the
:attr:`silent` option.
If :attr:`callback` is set :attr:`capture` defaults to :data:`True`
(but you can still set :attr:`capture` to :data:`False` if that is what
you want).
"""
return True if self.callback else False
@mutable_property
def capture_stderr(self):
"""
Enable capturing of the standard error stream.
If this option is :data:`True` (not the default) the standard error
stream of the external command is captured and made available to the
caller via :attr:`stderr`.
"""
return False
@mutable_property
def check(self):
"""
Enable automatic status code checking.
If this option is :data:`True` (the default) and the external command
exits with a nonzero status code :exc:`ExternalCommandFailed` will be
raised by :func:`start()` (when :attr:`asynchronous` isn't set) or
:func:`wait()` (when :attr:`asynchronous` is set).
"""
return True
@mutable_property
def command(self):
"""
A list of strings with the command to execute.
.. note:: In executor version 14.0 it became valid to set :attr:`input`
and :attr:`shell` without providing :attr:`command` (in older
versions it was required to set :attr:`command` regardless of
the other options).
"""
@property
def command_line(self):
"""
The command line of the external command.
The command line used to actually run the external command requested by
the user (a list of strings). The command line is constructed based on
:attr:`command` according to the following rules:
- If :attr:`shell` is :data:`True` the external command is run using
``bash -c '...'`` (assuming you haven't changed :data:`DEFAULT_SHELL`)
which means constructs like semicolons, ampersands and pipes can be
used (and all the usual caveats apply :-).
- If :attr:`virtual_environment` is set the command is converted to a
shell command line and prefixed by the applicable ``source ...``
command.
- If :attr:`uid` or :attr:`user` is set the ``sudo -u`` command will be
prefixed to the command line generated here.
- If :attr:`fakeroot` or :attr:`sudo` is set the respective command
name is prefixed to the command line generated here (``sudo`` is only
prefixed when the current process doesn't already have super user
privileges).
- If :attr:`ionice` is set the appropriate command is prefixed to the
command line generated here.
"""
command_line = list(self.command)
use_shell = self.shell
have_commands = (len(command_line) > 0)
have_input = (self.input is not None)
if use_shell and have_input and not have_commands:
# If `shell' is enabled and `input' is given but no `command' is
# given, we will start the DEFAULT_SHELL and instruct it to read
# shell commands to execute from its standard input stream.
use_shell = False
command_line = [DEFAULT_SHELL, '-']
# Apply the `shell' and/or `virtual_environment' options.
if self.virtual_environment:
activate_command = 'source %s' % quote(os.path.join(self.virtual_environment, 'bin', 'activate'))
if use_shell:
# Shell command(s) provided via positional arguments or standard input.
command_line = self.prefix_shell_command(activate_command, command_line[0]) + command_line[1:]
else:
# Non-shell command line provided via positional arguments.
command_line = self.prefix_shell_command(activate_command, command_line)
elif use_shell:
# Prepare to execute a shell command.
command_line = [DEFAULT_SHELL, '-c'] + command_line
# Run the command under `fakeroot' to fake super user privileges?
if self.fakeroot:
command_line = ['fakeroot'] + command_line
# Allow running of the command under `sudo' and/or `ionice'.
return self.sudo_command + self.ionice_command + command_line
@property
def decoded_stdout(self):
"""
The value of :attr:`stdout` decoded using :attr:`encoding`.
This is a :func:`python2:unicode` object (in Python 2) or a
:class:`python3:str` object (in Python 3).
"""
value = self.stdout
if value is not None:
return value.decode(self.encoding)
@property
def decoded_stderr(self):
"""
The value of :attr:`stderr` decoded using :attr:`encoding`.
This is a :func:`python2:unicode` object (in Python 2) or a
:class:`python3:str` object (in Python 3).
"""
value = self.stderr
if value is not None:
return value.decode(self.encoding)
@writable_property(cached=True)
def dependencies(self):
"""
The dependencies of the command (a list of :class:`ExternalCommand` objects).
The :attr:`dependencies` property enables low level concurrency control
in command pools by imposing a specific order of execution:
- Command pools will never start a command until the
:attr:`~.ExternalCommand.is_finished` properties of all of the
command's :attr:`~.ExternalCommand.dependencies` are :data:`True`.
- If :attr:`dependencies` is empty it has no effect and concurrency is
controlled by :attr:`group_by` and :attr:`~.CommandPool.concurrency`.
"""
return []
@mutable_property
def directory(self):
"""
The working directory for the external command.
A string, defaults to :data:`DEFAULT_WORKING_DIRECTORY`.
"""
return DEFAULT_WORKING_DIRECTORY
@property
def encoded_input(self):
"""
The value of :attr:`input` encoded using :attr:`encoding`.
This is a :class:`python2:str` object (in Python 2) or a
:class:`python3:bytes` object (in Python 3).
"""
return (self.input.encode(self.encoding)
if isinstance(self.input, text_type)
else self.input)
@mutable_property
def encoding(self):
"""
The character encoding of standard input and standard output.
A string, defaults to :data:`DEFAULT_ENCODING`. This option is used to
encode :attr:`input` and to decode :attr:`output`.
"""
return DEFAULT_ENCODING
@writable_property(cached=True)
def environment(self):
"""
A dictionary of environment variables for the external command.
You only need to specify environment variables that differ from those
of the current process (that is to say the environment variables of the
current process are merged with the variables that you specify here).
"""
return {}
@mutable_property
def error_message(self):
"""A string describing how the external command failed or :data:`None`."""
if self.error_type is CommandNotFound:
return self.format_error_message("\n\n".join([
"External command isn't available!",
"Command:\n%s" % quote(self.command_line),
"Search path:\n%s" % pprint.pformat(get_search_path()),
]))
elif self.error_type is ExternalCommandFailed:
return self.format_error_message("\n\n".join([
"External command failed with exit code %s!" % self.returncode,
"Command:\n%s" % quote(self.command_line),
]))
@mutable_property
def error_type(self):
"""
An appropriate exception class or :data:`None` (when no error occurred).
:class:`CommandNotFound` if the external command exits with return code
:data:`COMMAND_NOT_FOUND_STATUS` or :exc:`ExternalCommandFailed` if the
external command exits with any other nonzero return code.
"""
if self.returncode == COMMAND_NOT_FOUND_STATUS:
return CommandNotFound
elif self.returncode not in (None, 0):
return ExternalCommandFailed
@property
def failed(self):
"""
Whether the external command has failed.
- :data:`True` if :attr:`returncode` is a nonzero number
or :attr:`error_type` is set (e.g. because the external
command doesn't exist).
- :data:`False` if :attr:`returncode` is zero.
- :data:`None` when the external command hasn't been started or is
still running.
"""
return (not self.succeeded) if self.succeeded is not None else None
@mutable_property
def fakeroot(self):
"""
Run the external command under ``fakeroot``.
If this option is :data:`True` (not the default) and the current
process doesn't have `superuser privileges`_ the external command is
run with ``fakeroot``. If the ``fakeroot`` program is not installed the
external command will fail.
.. _superuser privileges: http://en.wikipedia.org/wiki/Superuser#Unix_and_Unix-like
"""
return False
@mutable_property
def finish_event(self):
"""Optional callback that's called just after the command finishes (see `event callbacks`_)."""
@mutable_property
def group_by(self):
"""
Identifier that's used to group the external command (any hashable value).
The :attr:`group_by` property enables high level concurrency control in
command pools by making it easy to control which commands are allowed
to run concurrently and which are required to run serially:
- Command pools will never start more than one command within a group
of commands that share the same value of :attr:`group_by` (for values
that aren't :data:`None`).
- If :attr:`group_by` is :data:`None` it has no effect and concurrency
is controlled by :attr:`dependencies` and
:attr:`~.CommandPool.concurrency`.
"""
@property
def have_superuser_privileges(self):
"""
Whether the parent Python process is running under `superuser privileges`_.
:data:`True` if running with `superuser privileges`_, :data:`False`
otherwise. Used by :attr:`command_line` to decide whether
:attr:`sudo` needs to be used.
"""
return os.getuid() == 0
@mutable_property
def input(self):
"""
The input to feed to the external command on the standard input stream.
When you provide a :func:`python2:unicode` object (in Python 2) or a
:class:`python3:str` object (in Python 3) as input it will be encoded
using :attr:`encoding`. To avoid the automatic conversion you can
simply pass a :class:`python2:str` object (in Python 2) or a
:class:`python3:bytes` object (in Python 3). This conversion logic is
implemented in the :attr:`encoded_input` attribute.
When :attr:`input` is set to :data:`True` a pipe will be created to
communicate with the external command in real time. See also the
:attr:`buffered` and :attr:`stdin` properties.
Defaults to :data:`None`.
"""
@mutable_property
def ionice(self):
"""
The I/O scheduling class for the external command (a string or :data:`None`).
When this property is set then ionice_ will be used to set the I/O
scheduling class for the external command. This can be useful to reduce
the impact of heavy disk operations on the rest of the system.
:raises: Any exceptions raised by :func:`validate_ionice_class()`.
.. _ionice: https://linux.die.net/man/1/ionice
"""
@ionice.setter
def ionice(self, value):
"""Validate and set the I/O scheduling class."""
if value is not None:
validate_ionice_class(value)
set_property(self, 'ionice', value)
@property
def ionice_command(self):
"""The ionice_ command based on :attr:`ionice` (a list of strings)."""
return ['ionice', '-c', self.ionice] if self.ionice else []
@property
def is_finished(self):
"""
Whether the external command has finished execution (excluding retries).
:data:`True` once the external command has been started and has since
finished (excluding retries), :data:`False` when the external command
hasn't been started yet or is still running.
"""
return self.was_started and not self.is_running
@property
def is_finished_with_retries(self):
"""
Whether the external command has finished execution (including retries).
:data:`True` once the external command has been started and has since
finished (including retries), :data:`False` when the external command
hasn't been started yet, is still running or can be retried.
"""
return self.is_finished and not self.retry_allowed
@property
def is_running(self):
""":data:`True` if the process is currently running, :data:`False` otherwise."""
if self.subprocess is not None:
return self.subprocess.poll() is None
else:
return False
@property
def is_terminated(self):
"""
Whether the external command has been terminated (a boolean).
:data:`True` if the external command was terminated using ``SIGTERM``
(e.g. by :func:`~executor.process.ControllableProcess.terminate()`),
:data:`False` otherwise.
"""
return abs(self.returncode) == signal.SIGTERM if self.is_finished and self.returncode < 0 else False
@mutable_property
def merge_streams(self):
"""
Whether to merge the standard output and error streams.
A boolean, defaults to :data:`False`. If this option is enabled
:attr:`stdout` will contain the external command's output on both
streams.
"""
return False
@property
def output(self):
"""
The value of :attr:`stdout` decoded using :attr:`encoding`.
This is a :func:`python2:unicode` object (in Python 2) or a
:class:`python3:str` object (in Python 3).
This is only available when :attr:`capture` is :data:`True`. If
:attr:`capture` is not :data:`True` then :attr:`output` will be
:data:`None`.
After decoding any leading and trailing whitespace is stripped and if
the resulting string doesn't contain any remaining newlines then the
string with leading and trailing whitespace stripped will be returned,
otherwise the decoded string is returned unchanged:
>>> from executor import ExternalCommand
>>> cmd = ExternalCommand('echo na\xc3\xafve', capture=True)
>>> cmd.start()
>>> cmd.output
u'na\\xefve'
>>> cmd.stdout
'na\\xc3\\xafve\\n'
This is intended to make simple things easy (:attr:`output` makes it
easy to deal with external commands that output a single line) while
providing an escape hatch when the default assumptions don't hold (you
can always use :attr:`stdout` to get the raw output).
See also the :func:`__iter__()` magic method which makes it very easy
to iterate over the lines of output produced by the command.
"""
text_output = self.decoded_stdout
if text_output is not None:
stripped_output = text_output.strip()
return stripped_output if '\n' not in stripped_output else text_output
@mutable_property
def really_silent(self):
"""
Whether output is really silenced or actually captured (a boolean).
When the :attr:`silent` option was originally added to executor it was
implemented by redirecting the output streams to :data:`os.devnull`,
similar to how ``command &> /dev/null`` works in Bash.
Since I made that decision I've regretted it many times because I ran
into situations where :attr:`check` and :attr:`silent` were both set
and :exc:`ExternalCommandFailed` was raised but I had no way to
determine what had gone wrong.
This is why the :attr:`really_silent` property was introduced in
executor release 19.0:
- When :attr:`silent` is :data:`True` and :attr:`check` is
:data:`False` the value of :attr:`really_silent` will be
:data:`True`, otherwise it is :data:`False`.
- When :attr:`really_silent` is :data:`False` (because :attr:`check` is
:data:`True`) the :attr:`silent` property effectively becomes an
alias for :attr:`capture` and :attr:`capture_stderr` which means the
output on both streams is captured instead of discarded.
- Because output is captured instead of discarded the output of
failing commands can be reported by :exc:`ExternalCommandFailed`
(which is raised because :attr:`check` is :data:`True`).
This change was made after much consideration because it is backwards
incompatible and not only in a theoretical sense: Imagine a daemon
process spewing megabytes of log output on its standard error stream.
As an escape hatch to restore backwards compatibility you can set
:attr:`really_silent` to :data:`True` to override the computed value.
"""
return self.silent and not self.check
@property
def result(self):
"""
The result of calling the value given by :attr:`callback`.
If the command hasn't been started yet :func:`start()` is called. When
the command hasn't finished yet func:`wait()` is called. If
:attr:`callback` isn't set :data:`None` is returned.
"""
if self.callback:
if not self.is_finished:
self.wait()
return self.callback(self)
@mutable_property
def retry(self):
"""
Whether the external command should be retried when it fails (a boolean, defaults to :data:`False`).
.. warning:: Retrying of failing commands is an experimental feature
that was introduced with the release of executor 20.0.
Please refer to the `20.0 release notes`_ for details.
.. _20.0 release notes: https://executor.readthedocs.io/en/latest/changelog.html#release-20-0-2018-05-21
"""
return False
@mutable_property
def retry_allowed(self):
"""
:data:`True` if the external command can be retried, :data:`False` otherwise.
The value of this property is computed by checking if the following
conditions hold:
- :attr:`retry` is :data:`True`,
- :attr:`failed` is :data:`True`,
- :attr:`returncode` is not :data:`COMMAND_NOT_FOUND_STATUS`,
- :attr:`retry_count` is lower than :attr:`retry_limit`
(only if :attr:`retry_limit` is not zero).
Note that when the :attr:`retry_event` callback returns :data:`False`
to cancel the retrying of a failed command, the computed value of
:attr:`retry_allowed` is overridden by assigning :attr:`retry_allowed`
the value :data:`False`.
"""
return (self.retry and self.failed and
self.returncode != COMMAND_NOT_FOUND_STATUS and
(self.retry_limit == 0 or self.retry_count < self.retry_limit))
@mutable_property
def retry_count(self):
"""
The number of times that the command was retried (an integer number, defaults to 0).
The value of :attr:`retry_count` is automatically incremented by
:func:`start_once()` when it notices that :attr:`was_started` is
:data:`True` before :func:`start_once()` has started the command.
"""
return 0
@mutable_property
def retry_event(self):
"""
Optional callback that's called when a command is retried (see `event callbacks`_).
The callback can return :data:`False` to abort retrying.
"""
@mutable_property
def retry_limit(self):
"""
The maximum number of times to *retry* the command when it fails (an integer, defaults to 2).
Given the default value of two, when :attr:`retry` is :data:`True`
the command will be run at most three times (the initial run and
two retries). The value 0 means the command will be retried until
it succeeds.
"""
return 2
@mutable_property
def returncode(self):
"""
The return code of the external command (an integer) or :data:`None`.
This will be :data:`None` until the external command has finished.
"""
if self.subprocess is not None:
return self.subprocess.poll()
@mutable_property
def shell(self):
"""
Whether to evaluate the external command as a shell command.
A boolean, the default depends on the value of :attr:`command`:
- If :attr:`command` contains a single string :attr:`shell` defaults to
:data:`True`.
- If :attr:`command` contains more than one string :attr:`shell`
defaults to :data:`False`.
When :data:`shell` is :data:`True` the external command is evaluated by
the shell given by :data:`DEFAULT_SHELL`, otherwise the external
command is run without shell evaluation.
"""
return len(self.command) == 1
@mutable_property
def silent(self):
"""
Whether the external command's output should be silenced (a boolean).
If this is :data:`True` (not the default) any output of the external
command is silenced by redirecting the output streams to
:data:`os.devnull` (if :attr:`really_silent` is :data:`True`) or by
capturing the output (if :attr:`really_silent` is :data:`False`).
You can enable :attr:`capture` and :attr:`silent` together to capture
the standard output stream while silencing the standard error stream.
"""
return False
@mutable_property
def start_event(self):
"""Optional callback that's called just before the command is started (see `event callbacks`_)."""
@property
def stderr(self):
"""