Merge ~kissiel/checkbox-support:add-interactive-cmd into checkbox-support:master

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Maciej Kisielewski
Approved revision: e50bf8677d74d87b93f4b1ba535c59cd6a5ad361
Merged at revision: fe18535d0370bf802da128f19b86a092a01b49d3
Proposed branch: ~kissiel/checkbox-support:add-interactive-cmd
Merge into: checkbox-support:master
Diff against target: 118 lines (+112/-0)
1 file modified
checkbox_support/interactive_cmd.py (+112/-0)
Reviewer Review Type Date Requested Status
Maciej Kisielewski (community) Needs Resubmitting
Jonathan Cave (community) Needs Information
Review via email: mp+321152@code.launchpad.net

Description of the change

I would like to start proposing changes to providers that use this module.
Instead of vendorizing the module in each provider, I think it's better to land it here.

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :
Revision history for this message
Jonathan Cave (jocave) wrote :

Comment below.

Otherwise looks good.

review: Needs Information
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Old cruft it is.
Fixing...

review: Needs Fixing
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Removed cruft, and also changed mode to -x.

review: Needs Resubmitting
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Let's land it, so I can base some other work on it

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/checkbox_support/interactive_cmd.py b/checkbox_support/interactive_cmd.py
0new file mode 1006440new file mode 100644
index 0000000..981a21b
--- /dev/null
+++ b/checkbox_support/interactive_cmd.py
@@ -0,0 +1,112 @@
1# Copyright 2017 Canonical Ltd.
2# All rights reserved.
3#
4# Written by:
5# Authors: Maciej Kisielewski <maciej.kisielewski@canonical.com>
6# This is a copy of the original module located here:
7# https://github.com/kissiel/gallows
8import re
9import select
10import subprocess
11import sys
12import time
13
14
15class InteractiveCommand:
16 def __init__(self, args):
17 self._args = args
18 self._is_running = False
19 self._pending = 0
20
21 def __enter__(self):
22 self.start()
23 return self
24
25 def __exit__(self, exc_type, exc_value, traceback):
26 self.kill()
27
28 def start(self):
29 self._proc = subprocess.Popen(
30 self._args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
31 stderr=subprocess.STDOUT)
32 self._is_running = True
33 self._poller = select.poll()
34 self._poller.register(self._proc.stdout, select.POLLIN)
35
36 def kill(self):
37 if self._is_running:
38 # explicitly closing files to make test not complain about leaking
39 # them (GC race condition?)
40 self._close_fds([self._proc.stdin, self._proc.stdout])
41 self._proc.terminate()
42 self._is_running = False
43 self._proc.wait()
44
45 def writeline(self, line, sleep=0.1):
46 if not self._is_running:
47 raise Exception('Process is not running')
48 try:
49 self._proc.stdin.write((line + '\n').encode(sys.stdin.encoding))
50 self._proc.stdin.flush()
51 except BrokenPipeError:
52 self._close_fds([self._proc.stdin])
53 raise
54 time.sleep(sleep)
55
56 def wait_for_output(self, timeout=5):
57 events = self._poller.poll(timeout * 1000)
58 if not events:
59 self._pending = 0
60 return None
61 else:
62 self._pending = len(self._proc.stdout.peek())
63 return self._pending
64
65 def wait_until_matched(self, pattern, timeout):
66 assert timeout >= 0, "cannot wait until past times"
67 deadline = time.time() + timeout
68 output = ''
69 while timeout > 0:
70 self.wait_for_output(timeout)
71 output += self.read_all()
72 re_match = re.search(pattern, output)
73 if re_match:
74 return re_match
75 timeout = deadline - time.time()
76 return None
77
78 def read_all(self):
79 if not self._pending:
80 return ''
81 else:
82 raw = self._proc.stdout.read(self._pending)
83 self._pending = 0
84 return raw.decode(sys.stdout.encoding)
85
86 def write_repeated(self, command, pattern, attempts, timeout):
87 """
88 Write `command`, try matching `pattern` in the response and repeat
89 for `attempts` times if pattern not matched.
90
91 return True if it matched at any point. Or
92 return False if attempts were depleted and pattern hasn't been matched
93 """
94 matched = None
95 while not matched and attempts > 0:
96 self.writeline(command)
97 matched = self.wait_until_matched(pattern, timeout)
98 if matched:
99 break
100 attempts -= 1
101 return matched
102
103 @property
104 def is_running(self):
105 return self._is_running
106
107 def _close_fds(self, fds):
108 for pipe in fds:
109 try:
110 pipe.close()
111 except BrokenPipeError:
112 pass

Subscribers

People subscribed via source and target branches