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

Subscribers

People subscribed via source and target branches