1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: txsshell.py 107905 2025-01-23 10:02:59Z vboxsync $
|
---|
3 |
|
---|
4 | """
|
---|
5 | Test eXecution Service Shell.
|
---|
6 | """
|
---|
7 | __copyright__ = \
|
---|
8 | """
|
---|
9 | Copyright (C) 2025 Oracle and/or its affiliates.
|
---|
10 |
|
---|
11 | This file is part of VirtualBox base platform packages, as
|
---|
12 | available from https://www.virtualbox.org.
|
---|
13 |
|
---|
14 | This program is free software; you can redistribute it and/or
|
---|
15 | modify it under the terms of the GNU General Public License
|
---|
16 | as published by the Free Software Foundation, in version 3 of the
|
---|
17 | License.
|
---|
18 |
|
---|
19 | This program is distributed in the hope that it will be useful, but
|
---|
20 | WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
22 | General Public License for more details.
|
---|
23 |
|
---|
24 | You should have received a copy of the GNU General Public License
|
---|
25 | along with this program; if not, see <https://www.gnu.org/licenses>.
|
---|
26 |
|
---|
27 | The contents of this file may alternatively be used under the terms
|
---|
28 | of the Common Development and Distribution License Version 1.0
|
---|
29 | (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
|
---|
30 | in the VirtualBox distribution, in which case the provisions of the
|
---|
31 | CDDL are applicable instead of those of the GPL.
|
---|
32 |
|
---|
33 | You may elect to license modified versions of this file under the
|
---|
34 | terms and conditions of either the GPL or the CDDL or both.
|
---|
35 |
|
---|
36 | SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
|
---|
37 | """
|
---|
38 | __version__ = '$Revision: 107905 $'
|
---|
39 |
|
---|
40 | import code;
|
---|
41 | import os;
|
---|
42 | import sys;
|
---|
43 | import time;
|
---|
44 |
|
---|
45 | if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6): # since 3.6
|
---|
46 | ModuleNotFoundError = ImportError # pylint: disable=redefined-builtin
|
---|
47 |
|
---|
48 | try: __file__ # pylint: disable=used-before-assignment
|
---|
49 | except: __file__ = sys.argv[0];
|
---|
50 | g_ksValidationKitDir = os.path.dirname(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', '..'));
|
---|
51 | sys.path.append(g_ksValidationKitDir);
|
---|
52 | try:
|
---|
53 | from testdriver import reporter;
|
---|
54 | from testdriver import txsclient;
|
---|
55 | except ModuleNotFoundError:
|
---|
56 | print('Error: Validation Kit testdriver module not found. Tweak your module import paths.');
|
---|
57 | print('Search path is: %s' % (sys.path,));
|
---|
58 | sys.exit(1);
|
---|
59 |
|
---|
60 | class TxsShell(code.InteractiveConsole):
|
---|
61 | """
|
---|
62 | The TxsShell implementation, derivred from code.InteractiveConsole.
|
---|
63 | """
|
---|
64 | def __init__(self, shellLocals=None, filename='<console>'):
|
---|
65 | """
|
---|
66 | Constructor.
|
---|
67 | """
|
---|
68 | code.InteractiveConsole.__init__(self, locals=shellLocals, filename=filename);
|
---|
69 | self.prompt = '>>> ';
|
---|
70 | self.cmds = {
|
---|
71 | 'c': self.cmdConnect,
|
---|
72 | 'connect': self.cmdConnect,
|
---|
73 | 'd': self.cmdDisconnect,
|
---|
74 | 'disconnect': self.cmdDisconnect,
|
---|
75 | 'h': self.cmdHelp,
|
---|
76 | 'help': self.cmdHelp,
|
---|
77 | 'q': self.cmdQuit,
|
---|
78 | 'quit': self.cmdQuit,
|
---|
79 | 'r': self.cmdReconnect,
|
---|
80 | 'reconnect': self.cmdReconnect,
|
---|
81 | 's': self.cmdStatus,
|
---|
82 | 'status': self.cmdStatus,
|
---|
83 | 'v': self.cmdVerbose,
|
---|
84 | 'verbose': self.cmdVerbose,
|
---|
85 | };
|
---|
86 | self.fTxsReversedSetup = None;
|
---|
87 | self.oTxsTransport = None;
|
---|
88 | self.oTxsSession = None;
|
---|
89 |
|
---|
90 | self.setConnection();
|
---|
91 |
|
---|
92 | def mainLoop(self):
|
---|
93 | """
|
---|
94 | The REPL main loop.
|
---|
95 | """
|
---|
96 | self.log('Welcome to VirtualBox TxsShell!\n\n'
|
---|
97 | 'Type \'!quit\' or press Ctrl+D / CTRL + Z to exit.\n'
|
---|
98 | 'Type \'!help\' for help.\n');
|
---|
99 | self.log('Default connection is set to %s:%u (%s)\n' \
|
---|
100 | % (self.sTxsHost, self.uTxsPort, 'reversed, as server' if self.fTxsReversedSetup else 'as client'));
|
---|
101 |
|
---|
102 | try:
|
---|
103 | self.interact(banner = '');
|
---|
104 | except SystemExit:
|
---|
105 | pass
|
---|
106 | finally:
|
---|
107 | self.disconnect(fQuiet = True);
|
---|
108 | self.log('\nExiting. Have a nice day!');
|
---|
109 |
|
---|
110 | def setConnection(self, sHostname=None, uPort=None, fReversed=None):
|
---|
111 | """
|
---|
112 | Set the TXS connection parameters.
|
---|
113 |
|
---|
114 | Those will be used for retrieving the lastly used connection parameters.
|
---|
115 | """
|
---|
116 | try:
|
---|
117 | if not fReversed: # Use a reversed connection by default (if not explicitly specified).
|
---|
118 | fReversed = True;
|
---|
119 | self.sTxsHost = '127.0.0.1' if sHostname is None else sHostname;
|
---|
120 | assert self.sTxsHost is not None;
|
---|
121 | if uPort is None:
|
---|
122 | self.uTxsPort = 5048 if fReversed else 5042;
|
---|
123 | else:
|
---|
124 | self.uTxsPort = int(uPort);
|
---|
125 | self.fTxsReversedSetup = True if fReversed is None else fReversed;
|
---|
126 | self.oTxsTransport = None;
|
---|
127 | self.oTxsSession = None;
|
---|
128 | except ValueError:
|
---|
129 | self.log('Invalid parameters given!');
|
---|
130 | return False;
|
---|
131 | return True;
|
---|
132 |
|
---|
133 | def connect(self, sHostname=None, uPort=None, fReversed=None, cMsTimeout = 30 * 1000):
|
---|
134 | """
|
---|
135 | Connects to a TXS instance (server or client).
|
---|
136 | """
|
---|
137 | if not sHostname:
|
---|
138 | sHostname = self.sTxsHost;
|
---|
139 | if not uPort:
|
---|
140 | uPort = self.uTxsPort;
|
---|
141 | if not fReversed:
|
---|
142 | fReversed = self.fTxsReversedSetup;
|
---|
143 | self.log('%s (%dms timeout) ...'
|
---|
144 | % ('Waiting for connection from TXS' if fReversed is True else 'Connecting to TXS', cMsTimeout));
|
---|
145 | try:
|
---|
146 | self.oTxsTransport = txsclient.TransportTcp(sHostname, uPort, fReversed);
|
---|
147 | if not self.oTxsTransport:
|
---|
148 | return False;
|
---|
149 | self.oTxsSession = txsclient.Session(self.oTxsTransport, cMsTimeout, cMsIdleFudge = 500, fTryConnect = True);
|
---|
150 | if not self.oTxsSession:
|
---|
151 | return False;
|
---|
152 | while not self.oTxsSession.isSuccess():
|
---|
153 | time.sleep(self.oTxsSession.getMsLeft(1, 1000) / 1000.0);
|
---|
154 | self.log('.', end='', flush=True);
|
---|
155 | except KeyboardInterrupt:
|
---|
156 | self.oTxsSession.cancelTask();
|
---|
157 | self.log('Connection cancelled');
|
---|
158 | self.disconnect();
|
---|
159 | return False;
|
---|
160 | self.log(''); # New line after connection dots.
|
---|
161 | if self.oTxsSession.hasTimedOut() \
|
---|
162 | or self.oTxsSession.isCancelled():
|
---|
163 | self.log('Connection error');
|
---|
164 | self.disconnect();
|
---|
165 | return False;
|
---|
166 |
|
---|
167 | self.log('Connection successful');
|
---|
168 | return True;
|
---|
169 |
|
---|
170 | def disconnect(self, fQuiet = True):
|
---|
171 | """
|
---|
172 | Disconnects from a TXS instance.
|
---|
173 | """
|
---|
174 | if self.oTxsSession:
|
---|
175 | if self.oTxsTransport:
|
---|
176 | rc = self.oTxsSession.sendMsg('BYE');
|
---|
177 | if rc is True:
|
---|
178 | rc = self.oTxsSession.recvAckLogged('BYE');
|
---|
179 | self.oTxsTransport.disconnect();
|
---|
180 | self.oTxsTransport = None;
|
---|
181 | self.oTxsSession = None;
|
---|
182 | if not fQuiet:
|
---|
183 | self.log('Disconnected.');
|
---|
184 | elif not fQuiet:
|
---|
185 | self.log('Not connected.');
|
---|
186 |
|
---|
187 | def reconnect(self, fQuiet = False):
|
---|
188 | """
|
---|
189 | Re-connects to a TXS instance.
|
---|
190 | """
|
---|
191 | self.disconnect(fQuiet);
|
---|
192 | self.connect(fQuiet);
|
---|
193 |
|
---|
194 | def sendMsg(self, sOpcode, asArgs):
|
---|
195 | """
|
---|
196 | Sends a message to a TXS instance.
|
---|
197 | """
|
---|
198 | if not self.oTxsSession:
|
---|
199 | self.log('Not connected.');
|
---|
200 | return;
|
---|
201 | fRc = self.oTxsSession.sendMsg(sOpcode, aoPayload = asArgs);
|
---|
202 | if fRc:
|
---|
203 | fRc = self.oTxsSession.recvAckLogged(sOpcode, False);
|
---|
204 |
|
---|
205 | def cmdConnect(self, sHostname=None, uPort=None, fReversed=None, fQuiet = False):
|
---|
206 | """
|
---|
207 | Handles the '!connect' / '!c' command.
|
---|
208 | """
|
---|
209 | self.disconnect();
|
---|
210 | if not self.setConnection(sHostname, uPort, fReversed):
|
---|
211 | return;
|
---|
212 |
|
---|
213 | if not fQuiet:
|
---|
214 | self.log('Connecting to %s:%u (%s) ...' % \
|
---|
215 | (self.sTxsHost, self.uTxsPort, 'reversed, as server' if self.fTxsReversedSetup else 'as client'));
|
---|
216 | fRc = self.connect(sHostname, uPort, fReversed);
|
---|
217 | if not fRc:
|
---|
218 | self.log('Hint: Is connecting to the host allowed by the VM?\n' \
|
---|
219 | ' Try VBoxManage modifyvm <VM> --nat-localhostreachable1=on\n\n');
|
---|
220 |
|
---|
221 | def cmdVerbose(self):
|
---|
222 | """
|
---|
223 | Handles the '!verbose' / '!v' command.
|
---|
224 | """
|
---|
225 | reporter.incVerbosity();
|
---|
226 | reporter.incVerbosity();
|
---|
227 | reporter.incVerbosity();
|
---|
228 |
|
---|
229 | def cmdDisconnect(self):
|
---|
230 | """
|
---|
231 | Handles the '!disconnect' / '!d' command.
|
---|
232 | """
|
---|
233 | self.disconnect(fQuiet = False);
|
---|
234 |
|
---|
235 | def cmdHelp(self):
|
---|
236 | """
|
---|
237 | Handles the '!help' / '!h' command.
|
---|
238 | """
|
---|
239 | self.log('!connect|!c');
|
---|
240 | self.log(' Connects to a TXS instance.');
|
---|
241 | self.log('!disconnect|!d');
|
---|
242 | self.log(' Disconnects from a TXS instance.');
|
---|
243 | self.log('!help|!h');
|
---|
244 | self.log(' Displays this help.');
|
---|
245 | self.log('!quit|!q');
|
---|
246 | self.log(' Quits this application.');
|
---|
247 | self.log('!reconnect|!r');
|
---|
248 | self.log(' Reconnects to the last connected TXS instance.');
|
---|
249 | self.log('!status|!s');
|
---|
250 | self.log(' Prints the current connection status.');
|
---|
251 | self.log('!verbose|!v');
|
---|
252 | self.log(' Increases the logging verbosity.');
|
---|
253 | self.log();
|
---|
254 | self.log('When connected to a TXS instance, anything else will be sent');
|
---|
255 | self.log('directly to TXS (including parameters), for example:');
|
---|
256 | self.log(' ISDIR /tmp/');
|
---|
257 | self.log();
|
---|
258 |
|
---|
259 | def cmdQuit(self):
|
---|
260 | """
|
---|
261 | Handles the '!quit' / '!q' command.
|
---|
262 | """
|
---|
263 | raise SystemExit;
|
---|
264 |
|
---|
265 | def cmdReconnect(self):
|
---|
266 | """
|
---|
267 | Handles the '!reconnect' / '!r' command.
|
---|
268 | """
|
---|
269 | self.reconnect();
|
---|
270 |
|
---|
271 | def cmdStatus(self):
|
---|
272 | """
|
---|
273 | Handles the '!s' / '!s' command.
|
---|
274 | """
|
---|
275 | if self.oTxsSession:
|
---|
276 | self.log('Connected to %s:%u (%s)' % \
|
---|
277 | (self.sTxsHost, self.uTxsPort, 'reversed, as server' if self.fTxsReversedSetup else 'as client'));
|
---|
278 | else:
|
---|
279 | self.log('Not connected.');
|
---|
280 |
|
---|
281 | def log(self, sText = None, end = '\n', flush = None):
|
---|
282 | """
|
---|
283 | Prints a message
|
---|
284 | """
|
---|
285 | if sText:
|
---|
286 | sys.stdout.write(sText);
|
---|
287 | if end:
|
---|
288 | sys.stdout.write(end);
|
---|
289 | if flush is True:
|
---|
290 | sys.stdout.flush();
|
---|
291 |
|
---|
292 | def raw_input(self, prompt=''):
|
---|
293 | """
|
---|
294 | Overwrites raw_input() to handle custom commands.
|
---|
295 | """
|
---|
296 | try:
|
---|
297 | sLine = code.InteractiveConsole.raw_input(self, prompt);
|
---|
298 | except EOFError:
|
---|
299 | raise SystemExit;
|
---|
300 | except KeyboardInterrupt:
|
---|
301 | self.log();
|
---|
302 | return self.raw_input(prompt);
|
---|
303 |
|
---|
304 | # Check if the line is a custom command.
|
---|
305 | if sLine.startswith('!'):
|
---|
306 | asCmdTokens = sLine[1:].strip().split();
|
---|
307 | if not asCmdTokens:
|
---|
308 | self.log('No command specified.');
|
---|
309 | return self.raw_input(prompt);
|
---|
310 |
|
---|
311 | sCmdName = asCmdTokens[0];
|
---|
312 | asArgs = asCmdTokens[1:];
|
---|
313 |
|
---|
314 | if sCmdName in self.cmds:
|
---|
315 | try:
|
---|
316 | # Call the custom command with the provided arguments.
|
---|
317 | self.cmds[sCmdName](*asArgs);
|
---|
318 | except TypeError as e:
|
---|
319 | self.log('*** Error: %s' % (e, ));
|
---|
320 | return self.raw_input(prompt);
|
---|
321 |
|
---|
322 | self.log('*** Unknown command: %s' % (sCmdName, ));
|
---|
323 | return self.raw_input(prompt);
|
---|
324 |
|
---|
325 | # Send to TXS.
|
---|
326 | asCmdTokens = sLine.strip().split();
|
---|
327 | if asCmdTokens:
|
---|
328 | self.sendMsg(asCmdTokens[0], asCmdTokens[1:]);
|
---|
329 |
|
---|
330 | return ''; # Don't evaluate as normal Python code.
|
---|
331 |
|
---|
332 | if __name__ == '__main__':
|
---|
333 | TxsShell(shellLocals=globals()).mainLoop();
|
---|