LineDance 0.1.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: LineDance
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synchronous line-oriented IPC with command-line utilities
|
|
5
|
+
Author: Jeremy Hill
|
|
6
|
+
Author-email: jezhill@gmail.com
|
|
7
|
+
License: CC0-1.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Description-Content-Type: text/x-rst
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: license
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
.. default-role:: code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
LineDance
|
|
25
|
+
=========
|
|
26
|
+
|
|
27
|
+
LineDance provides a small, dependency-free interface for conducting synchronous,
|
|
28
|
+
line-oriented exchanges with a command-line utility. It supports Python 2.7 and
|
|
29
|
+
Python 3.
|
|
30
|
+
|
|
31
|
+
A `Partner` instance owns one subprocess. Each call writes one line to its stdin
|
|
32
|
+
and waits for stdout, stderr, or process termination::
|
|
33
|
+
|
|
34
|
+
from linedance import Partner
|
|
35
|
+
|
|
36
|
+
partner = Partner( "perl -ple '$|=1; tr/a-z/A-Z/'" )
|
|
37
|
+
partner( 'hello world' )
|
|
38
|
+
# 'HELLO WORLD'
|
|
39
|
+
partner.Close()
|
|
40
|
+
|
|
41
|
+
Consecutive reply lines that arrive close together are returned in one newline-
|
|
42
|
+
joined string. Output on stderr raises `PartnerError`, while failure to produce
|
|
43
|
+
the first reply before an optional timeout raises `PartnerTimeout`::
|
|
44
|
+
|
|
45
|
+
partner = Partner( command, timeout=10.0, interLineTimeout=0.010 )
|
|
46
|
+
reply = partner.Communicate( 'one request' )
|
|
47
|
+
|
|
48
|
+
The defaults are mutable attributes and may also be overridden for one call.
|
|
49
|
+
`timeout=None`, the default, permits an arbitrarily long computation before the
|
|
50
|
+
first reply. Once the first line arrives, only the resetting `interLineTimeout`
|
|
51
|
+
applies, allowing any number of promptly consecutive lines to be collected.
|
|
52
|
+
|
|
53
|
+
Transcript
|
|
54
|
+
----------
|
|
55
|
+
|
|
56
|
+
Every sent or received line is retained in one chronological transcript::
|
|
57
|
+
|
|
58
|
+
partner.transcript
|
|
59
|
+
# [(timestamp, 'stdin', 'hello again'),
|
|
60
|
+
# (timestamp, 'stdout', 'HELLO AGAIN')]
|
|
61
|
+
|
|
62
|
+
The `stdin`, `stdout`, and `stderr` properties provide filtered views of the same
|
|
63
|
+
history. Empty lines are preserved. A received LF and one optional preceding CR
|
|
64
|
+
are stripped from each line.
|
|
65
|
+
|
|
66
|
+
Text and bytes
|
|
67
|
+
--------------
|
|
68
|
+
|
|
69
|
+
Native strings are encoded using the configurable `encoding` and receive the
|
|
70
|
+
configurable `terminator` (default `\n`) unless already terminated. On Python 3,
|
|
71
|
+
explicit `bytes` input is written exactly as supplied, without encoding or an
|
|
72
|
+
added terminator.
|
|
73
|
+
|
|
74
|
+
Protocol boundaries
|
|
75
|
+
-------------------
|
|
76
|
+
|
|
77
|
+
LineDance deliberately assumes a request/reply protocol. The child must flush
|
|
78
|
+
its output and terminate reply lines with `\n`. A command that remains alive but
|
|
79
|
+
produces no output has no observable reply boundary and therefore waits until its
|
|
80
|
+
timeout, if any. A timeout leaves the exchange desynchronized, so the `Partner`
|
|
81
|
+
refuses subsequent requests rather than risk attributing a late reply to the
|
|
82
|
+
wrong request.
|
|
83
|
+
|
|
84
|
+
Unsolicited output is retained but is not mistaken for a reply if it arrived
|
|
85
|
+
before the request. Protocols with prompts lacking line terminators, request IDs,
|
|
86
|
+
or explicit completion sentinels need a protocol-specific adapter.
|
|
87
|
+
|
|
88
|
+
LineDance is public-domain software released under CC0.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
linedance.py,sha256=8KfrVtR33RAChDJ65NfFog0hdxtnsEGSXd7Ebxft0yY,18742
|
|
2
|
+
linedance-0.1.1.dist-info/licenses/LICENSE.txt,sha256=9Ofzc7m5lpUDN-jUGkopOcLZC3cl6brz1QhKInF60yg,7169
|
|
3
|
+
linedance-0.1.1.dist-info/METADATA,sha256=EpBRuquP90iCJyKP6eV-unXMUmjRy-yDJYnL9LhFdrw,3112
|
|
4
|
+
linedance-0.1.1.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
5
|
+
linedance-0.1.1.dist-info/top_level.txt,sha256=M6NLGoj9uKdFahiKrStWU8kYAkFZ9e2uemslqA5018E,10
|
|
6
|
+
linedance-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Creative Commons Legal Code
|
|
2
|
+
|
|
3
|
+
CC0 1.0 Universal
|
|
4
|
+
|
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
+
HEREUNDER.
|
|
13
|
+
|
|
14
|
+
Statement of Purpose
|
|
15
|
+
|
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
+
authorship and/or a database (each, a "Work").
|
|
20
|
+
|
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
+
culture and the further production of creative, cultural and scientific
|
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
+
part through the use and efforts of others.
|
|
31
|
+
|
|
32
|
+
For these and/or other purposes and motivations, and without any
|
|
33
|
+
expectation of additional consideration or compensation, the person
|
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
+
|
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
+
limited to, the following:
|
|
44
|
+
|
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
+
communicate, and translate a Work;
|
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
+
likeness depicted in a Work;
|
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
+
in a Work;
|
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
+
protection of databases, and under any national implementation
|
|
57
|
+
thereof, including any amended or successor version of such
|
|
58
|
+
directive); and
|
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
+
world based on applicable law or treaty, and any national
|
|
61
|
+
implementations thereof.
|
|
62
|
+
|
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
+
including without limitation commercial, advertising or promotional
|
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
+
|
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
+
express Statement of Purpose.
|
|
101
|
+
|
|
102
|
+
4. Limitations and Disclaimers.
|
|
103
|
+
|
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
+
the greatest extent permissible under applicable law.
|
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
+
that may apply to the Work or any use thereof, including without
|
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
+
consents, permissions or other rights required for any use of the
|
|
118
|
+
Work.
|
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
+
party to this document and has no duty or obligation with respect to
|
|
121
|
+
this CC0 or use of the Work.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linedance
|
linedance.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# $BEGIN_LINEDANCE_LICENSE$
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the LineDance project, a Python module for synchronous,
|
|
4
|
+
# line-oriented interprocess communication.
|
|
5
|
+
#
|
|
6
|
+
# Author: Jeremy Hill (2026-)
|
|
7
|
+
# Development was supported by the NIH, NYS SCIRB, and the Stratton VA Medical
|
|
8
|
+
# Center.
|
|
9
|
+
#
|
|
10
|
+
# No Copyright
|
|
11
|
+
# ============
|
|
12
|
+
# The author has dedicated this work to the public domain under the terms of
|
|
13
|
+
# Creative Commons' CC0 1.0 Universal legal code, waiving all of his rights to
|
|
14
|
+
# the work worldwide under copyright law, including all related and neighboring
|
|
15
|
+
# rights, to the extent allowed by law.
|
|
16
|
+
#
|
|
17
|
+
# You can copy, modify, distribute and perform the work, even for commercial
|
|
18
|
+
# purposes, all without asking permission. See Other Information below.
|
|
19
|
+
#
|
|
20
|
+
# Other Information
|
|
21
|
+
# =================
|
|
22
|
+
# In no way are the patent or trademark rights of any person affected by CC0,
|
|
23
|
+
# nor are the rights that other persons may have in the work or in how the work
|
|
24
|
+
# is used, such as publicity or privacy rights.
|
|
25
|
+
#
|
|
26
|
+
# The author makes no warranties about the work, and disclaims liability for
|
|
27
|
+
# all uses of the work, to the fullest extent permitted by applicable law. When
|
|
28
|
+
# using or citing the work, you are requested to preserve the author attribution
|
|
29
|
+
# and this copyright waiver, but you should not imply endorsement by the author.
|
|
30
|
+
#
|
|
31
|
+
# $END_LINEDANCE_LICENSE$
|
|
32
|
+
r"""
|
|
33
|
+
Synchronous, line-oriented IPC with a command-line utility.
|
|
34
|
+
|
|
35
|
+
`Partner` starts one subprocess and supports repeated REPL-like request/reply
|
|
36
|
+
exchanges. Each call to `Partner.Communicate()` writes one input payload and
|
|
37
|
+
then waits for the first subsequent stdout line, stderr line, or process
|
|
38
|
+
termination:
|
|
39
|
+
|
|
40
|
+
- stdout lines are returned as a native string; consecutive lines separated by
|
|
41
|
+
no more than `interLineTimeout` are joined with ``'\n'``;
|
|
42
|
+
- stderr lines are collated by the same rule and raised in `PartnerError`;
|
|
43
|
+
- successful termination without a reply returns `None`; and
|
|
44
|
+
- unsuccessful termination raises `PartnerError` with its `returnCode`.
|
|
45
|
+
|
|
46
|
+
The initial `timeout` applies only until the first reply line. Once output
|
|
47
|
+
begins, any number of lines may follow provided each arrives within the
|
|
48
|
+
resetting `interLineTimeout`. A timed-out exchange leaves the protocol
|
|
49
|
+
desynchronized, so that `Partner` cannot safely be used for another request.
|
|
50
|
+
The exchange that first observes process termination reports it normally;
|
|
51
|
+
subsequent communication attempts raise `RuntimeError` rather than repeatedly
|
|
52
|
+
reporting EOF or writing to a finished process.
|
|
53
|
+
|
|
54
|
+
Native-string input is encoded and receives the mutable `terminator` if it does
|
|
55
|
+
not already have one. On Python 3, explicit `bytes` bypass encoding and
|
|
56
|
+
termination and are written exactly as supplied. Incoming lines are expected
|
|
57
|
+
to end in LF; exactly that LF and one optional preceding CR are removed. Empty
|
|
58
|
+
lines remain real replies and transcript events.
|
|
59
|
+
|
|
60
|
+
Every stdin, stdout, and stderr line is atomically appended to `transcript` as
|
|
61
|
+
``(timestamp, eventType, line)``. The `stdin`, `stdout`, and `stderr` properties
|
|
62
|
+
are filtered snapshots of that sequence. Unsolicited output is always recorded,
|
|
63
|
+
but output received before a request is not mistaken for that request's reply.
|
|
64
|
+
|
|
65
|
+
Dedicated daemon threads continuously drain stdout and stderr, avoiding pipe
|
|
66
|
+
deadlock without platform-specific polling and retaining Python 2.7 support.
|
|
67
|
+
The child must flush its output; parent-side buffering options cannot flush a
|
|
68
|
+
child's private buffers. Deleting a `Partner` closes stdin, briefly permits an
|
|
69
|
+
EOF-driven exit, and then escalates through terminate and kill while reaping the
|
|
70
|
+
process.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from __future__ import absolute_import
|
|
74
|
+
|
|
75
|
+
__version__ = '0.1.1'
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
'Partner',
|
|
79
|
+
'PartnerError', 'PartnerTimeout',
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
import os
|
|
83
|
+
import sys
|
|
84
|
+
import time
|
|
85
|
+
import shlex
|
|
86
|
+
import threading
|
|
87
|
+
import subprocess
|
|
88
|
+
try: import queue
|
|
89
|
+
except ImportError: import Queue as queue # Python 2
|
|
90
|
+
|
|
91
|
+
PY3 = ( sys.version_info[ 0 ] >= 3 )
|
|
92
|
+
WINDOWS = ( sys.platform.lower().startswith( 'win' ) )
|
|
93
|
+
|
|
94
|
+
class PartnerError( RuntimeError ):
|
|
95
|
+
"""The partner wrote to stderr or terminated unsuccessfully."""
|
|
96
|
+
|
|
97
|
+
def __init__( self, command, returnCode, stderr='' ):
|
|
98
|
+
self.command = command
|
|
99
|
+
self.returnCode = returnCode
|
|
100
|
+
self.stderr = stderr
|
|
101
|
+
message = 'partner process reported an error'
|
|
102
|
+
if returnCode is not None: message += ' with return code %s' % returnCode
|
|
103
|
+
if stderr: message += ':\n' + stderr.rstrip()
|
|
104
|
+
RuntimeError.__init__( self, message )
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PartnerTimeout( RuntimeError ):
|
|
108
|
+
"""The partner did not complete its response before the deadline."""
|
|
109
|
+
|
|
110
|
+
def __init__( self, timeout ):
|
|
111
|
+
self.timeout = timeout
|
|
112
|
+
RuntimeError.__init__( self, 'partner did not respond within %g seconds' % timeout )
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Partner( object ):
|
|
116
|
+
"""
|
|
117
|
+
Start `command` as a subprocess, and conduct one line-oriented exchange at
|
|
118
|
+
a time.
|
|
119
|
+
|
|
120
|
+
`command` may be a sequence of arguments or a command string. A string is
|
|
121
|
+
split with `shlex` on POSIX when `shell=False`; passing a sequence is the
|
|
122
|
+
least ambiguous form on every platform. If `shell=True`, `command` must be
|
|
123
|
+
a string.
|
|
124
|
+
|
|
125
|
+
`.Communicate(line)` writes one native `str`, adding the configured terminator
|
|
126
|
+
if it is not already present, and then returns the first newline-stripped
|
|
127
|
+
native `str` received from stdout after the write begins, together with any
|
|
128
|
+
further lines received within its `interLineTimeout`. Earlier unsolicited
|
|
129
|
+
lines are ignored as replies, but remain available in `.stdout` and `.transcript`.
|
|
130
|
+
The method returns `None` if the process instead terminates successfully.
|
|
131
|
+
Output on stderr, or an unsuccessful return code, raises `PartnerError`; all
|
|
132
|
+
stderr text is available as the exception's `.stderr` attribute and in the
|
|
133
|
+
`Partner` instance's `.stderr` and `.transcript`. The `.stdin`, `.stdout`,
|
|
134
|
+
and `.stderr` properties are filtered views of the single chronologically
|
|
135
|
+
appended `(timestamp, eventType, line)` sequence in `.transcript`.
|
|
136
|
+
|
|
137
|
+
`timeout` is the default maximum wait for the first reply line, and
|
|
138
|
+
`interLineTimeout` is the default maximum gap between bundled reply lines.
|
|
139
|
+
Both are mutable attributes and may be overridden for one `.Communicate()`
|
|
140
|
+
call. Native-string input is encoded after appending the mutable `terminator`
|
|
141
|
+
if necessary. On Python 3, explicit `bytes` input is sent exactly as supplied.
|
|
142
|
+
|
|
143
|
+
The child must flush each stdout reply. No parent-side implementation can
|
|
144
|
+
make an independently buffered child reveal a line that it has not flushed.
|
|
145
|
+
A Perl child, for example, should execute `$|=1` to disable buffering.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__( self, command, shell=False, encoding='utf-8', terminator='\n', timeout=None, interLineTimeout=0.010 ):
|
|
149
|
+
if shell and isinstance( command, ( list, tuple ) ):
|
|
150
|
+
if WINDOWS: command = ' '.join( '"' + word.replace( '"', '""' ) + '"' for word in command )
|
|
151
|
+
else: command = ' '.join( '"' + word.replace( '\\', '\\\\' ).replace( '"', '\\"' ) + '"' for word in command )
|
|
152
|
+
if not shell and hasattr( command, 'split' ):
|
|
153
|
+
command = shlex.split( command )
|
|
154
|
+
|
|
155
|
+
self.__command = command
|
|
156
|
+
self.__encoding = encoding
|
|
157
|
+
self.terminator = terminator
|
|
158
|
+
self.timeout = timeout
|
|
159
|
+
self.interLineTimeout = interLineTimeout
|
|
160
|
+
self.__events = queue.Queue()
|
|
161
|
+
self.__lock = threading.Lock()
|
|
162
|
+
self.__receptionLock = threading.Lock()
|
|
163
|
+
self.__generation = [ 0 ]
|
|
164
|
+
self.__transcript = []
|
|
165
|
+
self.__stdoutEOF = False
|
|
166
|
+
self.__stderrEOF = False
|
|
167
|
+
self.__desynchronized = False
|
|
168
|
+
self.__retired = False
|
|
169
|
+
self.__sp = subprocess.Popen(
|
|
170
|
+
command,
|
|
171
|
+
shell = shell,
|
|
172
|
+
stdin = subprocess.PIPE,
|
|
173
|
+
stdout = subprocess.PIPE,
|
|
174
|
+
stderr = subprocess.PIPE,
|
|
175
|
+
bufsize = 0,
|
|
176
|
+
)
|
|
177
|
+
self.__StartReader( 'stdout', self.__sp.stdout )
|
|
178
|
+
self.__StartReader( 'stderr', self.__sp.stderr )
|
|
179
|
+
|
|
180
|
+
def __StartReader( self, name, stream ):
|
|
181
|
+
# Do not let the long-lived reader target capture `self`: otherwise a reader
|
|
182
|
+
# blocked on the pipe would prevent an abandoned Partner from being collected.
|
|
183
|
+
encoding = self.__encoding
|
|
184
|
+
Decode = lambda value: value.decode( encoding ) if PY3 and encoding else value
|
|
185
|
+
StripLineTerminator = self.__StripLineTerminator
|
|
186
|
+
receptionLock = self.__receptionLock
|
|
187
|
+
generation = self.__generation
|
|
188
|
+
transcript = self.__transcript
|
|
189
|
+
events = self.__events
|
|
190
|
+
Clock = time.time
|
|
191
|
+
def ReadLines():
|
|
192
|
+
try:
|
|
193
|
+
while True:
|
|
194
|
+
line = stream.readline()
|
|
195
|
+
if not line: break
|
|
196
|
+
decoded = StripLineTerminator( Decode( line ) )
|
|
197
|
+
timestamp = Clock()
|
|
198
|
+
receptionLock.acquire()
|
|
199
|
+
try:
|
|
200
|
+
currentGeneration = generation[ 0 ]
|
|
201
|
+
transcript.append( ( timestamp, name, decoded ) )
|
|
202
|
+
finally:
|
|
203
|
+
receptionLock.release()
|
|
204
|
+
events.put( ( timestamp, currentGeneration, name, line ) )
|
|
205
|
+
finally:
|
|
206
|
+
try: stream.close()
|
|
207
|
+
except: pass
|
|
208
|
+
receptionLock.acquire()
|
|
209
|
+
try: currentGeneration = generation[ 0 ]
|
|
210
|
+
finally: receptionLock.release()
|
|
211
|
+
events.put( ( Clock(), currentGeneration, name, None ) )
|
|
212
|
+
thread = threading.Thread( target=ReadLines )
|
|
213
|
+
thread.daemon = True
|
|
214
|
+
thread.start()
|
|
215
|
+
|
|
216
|
+
def __Encode( self, value ):
|
|
217
|
+
return value.encode( self.__encoding ) if PY3 and self.__encoding else value
|
|
218
|
+
|
|
219
|
+
def __Decode( self, value ):
|
|
220
|
+
return value.decode( self.__encoding ) if PY3 and self.__encoding else value
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def __StripLineTerminator( value ):
|
|
224
|
+
if value.endswith( '\n' ):
|
|
225
|
+
value = value[ :-1 ]
|
|
226
|
+
if value.endswith( '\r' ): value = value[ :-1 ]
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
def __PrepareInput( self, value ):
|
|
230
|
+
if PY3 and isinstance( value, bytes ):
|
|
231
|
+
return value, value
|
|
232
|
+
if not isinstance( value, str ):
|
|
233
|
+
raise TypeError( 'input must be a native str%s, not %s' % ( ' or bytes' if PY3 else '', type( value ).__name__ ) )
|
|
234
|
+
terminator = self.terminator
|
|
235
|
+
if terminator is None: terminator = ''
|
|
236
|
+
if not isinstance( terminator, str ):
|
|
237
|
+
raise TypeError( 'terminator must be a native str or None' )
|
|
238
|
+
if terminator and value.endswith( terminator ):
|
|
239
|
+
return self.__Encode( value ), value[ :-len( terminator ) ]
|
|
240
|
+
return self.__Encode( value + terminator ), value
|
|
241
|
+
|
|
242
|
+
def __Remaining( self, deadline ):
|
|
243
|
+
if deadline is None: return None
|
|
244
|
+
return max( 0.0, deadline - time.time() )
|
|
245
|
+
|
|
246
|
+
def __NextEvent( self, deadline ):
|
|
247
|
+
remaining = self.__Remaining( deadline )
|
|
248
|
+
try:
|
|
249
|
+
if remaining is None: return self.__events.get()
|
|
250
|
+
return self.__events.get( True, remaining )
|
|
251
|
+
except queue.Empty:
|
|
252
|
+
raise PartnerTimeout( 0.0 )
|
|
253
|
+
|
|
254
|
+
def __RecordEvent( self, event, stderr, minimumGeneration=None ):
|
|
255
|
+
timestamp, generation, name, value = event
|
|
256
|
+
if value is None:
|
|
257
|
+
if name == 'stdout': self.__stdoutEOF = True
|
|
258
|
+
else: self.__stderrEOF = True
|
|
259
|
+
elif name == 'stderr' and ( minimumGeneration is None or generation >= minimumGeneration ):
|
|
260
|
+
stderr.append( value )
|
|
261
|
+
return timestamp, generation, name, value
|
|
262
|
+
|
|
263
|
+
def __BeginRequest( self, line ):
|
|
264
|
+
self.__receptionLock.acquire()
|
|
265
|
+
try:
|
|
266
|
+
self.__generation[ 0 ] += 1
|
|
267
|
+
self.__transcript.append( ( time.time(), 'stdin', line ) )
|
|
268
|
+
return self.__generation[ 0 ]
|
|
269
|
+
finally:
|
|
270
|
+
self.__receptionLock.release()
|
|
271
|
+
|
|
272
|
+
def __TerminalResult( self, deadline, stderr ):
|
|
273
|
+
while not ( self.__stdoutEOF and self.__stderrEOF ):
|
|
274
|
+
self.__RecordEvent( self.__NextEvent( deadline ), stderr )
|
|
275
|
+
while self.__sp.poll() is None:
|
|
276
|
+
if deadline is not None and time.time() >= deadline:
|
|
277
|
+
raise PartnerTimeout( 0.0 )
|
|
278
|
+
time.sleep( 0.001 )
|
|
279
|
+
returnCode = self.__sp.returncode
|
|
280
|
+
self.__CloseStdin()
|
|
281
|
+
self.__retired = True
|
|
282
|
+
stderr = self.__Decode( b''.join( stderr ) )
|
|
283
|
+
if stderr or returnCode:
|
|
284
|
+
raise PartnerError( self.__command, returnCode, stderr )
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def __StdoutResult( self, firstTimestamp, firstLine, generation, stderr, interLineTimeout ):
|
|
288
|
+
lines = [ self.__StripLineTerminator( self.__Decode( firstLine ) ) ]
|
|
289
|
+
if not interLineTimeout: return lines[ 0 ]
|
|
290
|
+
cutoff = firstTimestamp + interLineTimeout
|
|
291
|
+
while True:
|
|
292
|
+
try:
|
|
293
|
+
timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( cutoff ), stderr, generation )
|
|
294
|
+
except PartnerTimeout:
|
|
295
|
+
return '\n'.join( lines )
|
|
296
|
+
if name == 'stderr' and value is not None and receivedGeneration >= generation:
|
|
297
|
+
return self.__StderrResult( timestamp, generation, stderr, interLineTimeout )
|
|
298
|
+
if name == 'stdout' and value is not None and receivedGeneration >= generation:
|
|
299
|
+
if timestamp > cutoff: return '\n'.join( lines )
|
|
300
|
+
lines.append( self.__StripLineTerminator( self.__Decode( value ) ) )
|
|
301
|
+
cutoff = timestamp + interLineTimeout
|
|
302
|
+
if self.__stdoutEOF:
|
|
303
|
+
# Return the final reply once, but do not let a subsequent request be
|
|
304
|
+
# mistaken for the one that caused this already-observed EOF.
|
|
305
|
+
self.__retired = True
|
|
306
|
+
self.__CloseStdin()
|
|
307
|
+
return '\n'.join( lines )
|
|
308
|
+
|
|
309
|
+
def __StderrResult( self, firstTimestamp, generation, stderr, interLineTimeout ):
|
|
310
|
+
if self.__sp.poll() is not None: return self.__TerminalResult( None, stderr )
|
|
311
|
+
if not interLineTimeout:
|
|
312
|
+
raise PartnerError( self.__command, None, self.__Decode( b''.join( stderr ) ) )
|
|
313
|
+
cutoff = firstTimestamp + interLineTimeout
|
|
314
|
+
while True:
|
|
315
|
+
oldLength = len( stderr )
|
|
316
|
+
try:
|
|
317
|
+
timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( cutoff ), stderr, generation )
|
|
318
|
+
except PartnerTimeout:
|
|
319
|
+
raise PartnerError( self.__command, self.__sp.poll(), self.__Decode( b''.join( stderr ) ) )
|
|
320
|
+
if name == 'stderr' and value is not None and receivedGeneration >= generation:
|
|
321
|
+
if timestamp > cutoff:
|
|
322
|
+
del stderr[ oldLength: ]
|
|
323
|
+
raise PartnerError( self.__command, self.__sp.poll(), self.__Decode( b''.join( stderr ) ) )
|
|
324
|
+
cutoff = timestamp + interLineTimeout
|
|
325
|
+
if self.__stderrEOF:
|
|
326
|
+
return self.__TerminalResult( None, stderr )
|
|
327
|
+
|
|
328
|
+
def __CloseStdin( self ):
|
|
329
|
+
try:
|
|
330
|
+
if not self.__sp.stdin.closed: self.__sp.stdin.close()
|
|
331
|
+
except: pass
|
|
332
|
+
|
|
333
|
+
def Communicate( self, line='', timeout=None, interLineTimeout=None ):
|
|
334
|
+
"""
|
|
335
|
+
Send one line and return one reply, `None`, or raise `PartnerError`.
|
|
336
|
+
Consecutive stdout or stderr lines separated by no more than
|
|
337
|
+
`interLineTimeout` seconds are joined with newline characters into one
|
|
338
|
+
reply or one `PartnerError`, respectively.
|
|
339
|
+
|
|
340
|
+
`timeout=None` and `interLineTimeout=None` use the corresponding mutable
|
|
341
|
+
instance attributes. If the effective `timeout` expires before the first
|
|
342
|
+
reply line, `PartnerTimeout` is raised and this instance becomes unusable:
|
|
343
|
+
the outstanding request makes the protocol state unknowable. Once the first
|
|
344
|
+
line arrives, only the resetting `interLineTimeout` applies. After process
|
|
345
|
+
termination has been observed, subsequent calls raise `RuntimeError`
|
|
346
|
+
immediately.
|
|
347
|
+
"""
|
|
348
|
+
self.__lock.acquire()
|
|
349
|
+
try:
|
|
350
|
+
returnCode = self.__sp.poll()
|
|
351
|
+
if self.__retired or returnCode is not None:
|
|
352
|
+
self.__retired = True
|
|
353
|
+
self.__CloseStdin()
|
|
354
|
+
raise RuntimeError( 'partner process has already terminated with return code %s' % returnCode )
|
|
355
|
+
if timeout is None: timeout = self.timeout
|
|
356
|
+
if interLineTimeout is None: interLineTimeout = self.interLineTimeout
|
|
357
|
+
if interLineTimeout is None: interLineTimeout = 0.0
|
|
358
|
+
if timeout is not None and timeout < 0.0: raise ValueError( 'timeout cannot be negative' )
|
|
359
|
+
if interLineTimeout < 0.0: raise ValueError( 'interLineTimeout cannot be negative' )
|
|
360
|
+
if self.__desynchronized:
|
|
361
|
+
raise RuntimeError( 'partner is desynchronized after an earlier timeout' )
|
|
362
|
+
deadline = None if timeout is None else time.time() + timeout
|
|
363
|
+
stderr = []
|
|
364
|
+
|
|
365
|
+
wireInput, recordedInput = self.__PrepareInput( line )
|
|
366
|
+
generation = self.__BeginRequest( recordedInput )
|
|
367
|
+
try:
|
|
368
|
+
self.__sp.stdin.write( wireInput )
|
|
369
|
+
self.__sp.stdin.flush()
|
|
370
|
+
except ( IOError, OSError ):
|
|
371
|
+
return self.__TerminalResult( deadline, stderr )
|
|
372
|
+
|
|
373
|
+
while True:
|
|
374
|
+
timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( deadline ), stderr, generation )
|
|
375
|
+
|
|
376
|
+
if name == 'stderr' and value is not None and receivedGeneration >= generation:
|
|
377
|
+
return self.__StderrResult( timestamp, generation, stderr, interLineTimeout )
|
|
378
|
+
if name == 'stdout' and value is not None and receivedGeneration >= generation:
|
|
379
|
+
return self.__StdoutResult( timestamp, value, generation, stderr, interLineTimeout )
|
|
380
|
+
if self.__stdoutEOF:
|
|
381
|
+
return self.__TerminalResult( deadline, stderr )
|
|
382
|
+
except PartnerTimeout:
|
|
383
|
+
self.__desynchronized = True
|
|
384
|
+
raise PartnerTimeout( timeout )
|
|
385
|
+
finally:
|
|
386
|
+
self.__lock.release()
|
|
387
|
+
|
|
388
|
+
__call__ = Communicate
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def command( self ):
|
|
392
|
+
return self.__command
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def pid( self ):
|
|
396
|
+
return self.__sp.pid
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def returnCode( self ):
|
|
400
|
+
returnCode = self.__sp.poll()
|
|
401
|
+
if returnCode is not None:
|
|
402
|
+
self.__CloseStdin()
|
|
403
|
+
self.__retired = True
|
|
404
|
+
return returnCode
|
|
405
|
+
|
|
406
|
+
def __History( self, stream=None ):
|
|
407
|
+
self.__receptionLock.acquire()
|
|
408
|
+
try:
|
|
409
|
+
if stream is None: return list( self.__transcript )
|
|
410
|
+
return [ line for timestamp, eventType, line in self.__transcript if eventType == stream ]
|
|
411
|
+
finally: self.__receptionLock.release()
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def stdin( self ):
|
|
415
|
+
"""All stdin sent so far: unterminated native strings or exact Python 3 bytes."""
|
|
416
|
+
return self.__History( 'stdin' )
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def stdout( self ):
|
|
420
|
+
"""All stdout lines received so far, without line terminators."""
|
|
421
|
+
return self.__History( 'stdout' )
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def stderr( self ):
|
|
425
|
+
"""All stderr lines received so far, without line terminators."""
|
|
426
|
+
return self.__History( 'stderr' )
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def transcript( self ):
|
|
430
|
+
"""All `(timestamp, eventType, line)` tuples in atomic chronological order."""
|
|
431
|
+
return self.__History()
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def finished( self ):
|
|
435
|
+
returnCode = self.__sp.poll()
|
|
436
|
+
if returnCode is not None:
|
|
437
|
+
self.__CloseStdin()
|
|
438
|
+
self.__retired = True
|
|
439
|
+
return returnCode is not None
|
|
440
|
+
|
|
441
|
+
def Wait( self, timeout=None ):
|
|
442
|
+
deadline = None if timeout is None else time.time() + timeout
|
|
443
|
+
while self.__sp.poll() is None:
|
|
444
|
+
if deadline is not None and time.time() >= deadline:
|
|
445
|
+
raise PartnerTimeout( timeout )
|
|
446
|
+
time.sleep( 0.01 )
|
|
447
|
+
self.__CloseStdin()
|
|
448
|
+
self.__retired = True
|
|
449
|
+
return self.__sp.returncode
|
|
450
|
+
|
|
451
|
+
def Close( self, timeout=None ):
|
|
452
|
+
"""Close stdin, wait for the child to finish, and return its exit code."""
|
|
453
|
+
self.__CloseStdin()
|
|
454
|
+
return self.Wait( timeout=timeout )
|
|
455
|
+
|
|
456
|
+
def Terminate( self, timeout=None ):
|
|
457
|
+
if self.__sp.poll() is None: self.__sp.terminate()
|
|
458
|
+
return self.Wait( timeout=timeout )
|
|
459
|
+
|
|
460
|
+
def Kill( self, timeout=None ):
|
|
461
|
+
if self.__sp.poll() is None: self.__sp.kill()
|
|
462
|
+
return self.Wait( timeout=timeout )
|
|
463
|
+
|
|
464
|
+
def __enter__( self ):
|
|
465
|
+
return self
|
|
466
|
+
|
|
467
|
+
def __exit__( self, excType, excValue, traceback ):
|
|
468
|
+
if self.__sp.poll() is None: self.Close()
|
|
469
|
+
|
|
470
|
+
def __del__( self ):
|
|
471
|
+
try:
|
|
472
|
+
sp = self.__sp
|
|
473
|
+
try: sp.stdin.closed or sp.stdin.close()
|
|
474
|
+
except: pass
|
|
475
|
+
for Action in [ sp.terminate, sp.kill ]:
|
|
476
|
+
deadline = time.time() + 0.050
|
|
477
|
+
while sp.poll() is None and time.time() < deadline:
|
|
478
|
+
time.sleep( 0.001 )
|
|
479
|
+
if sp.poll() is not None: break
|
|
480
|
+
Action()
|
|
481
|
+
if sp.poll() is None: sp.kill()
|
|
482
|
+
sp.wait()
|
|
483
|
+
except: pass
|