VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/webservergluebase.py@ 101505

Last change on this file since 101505 was 99875, checked in by vboxsync, 19 months ago

ValKit: pylint adjustments for PEP-594 (dead batteries)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 23.8 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: webservergluebase.py 99875 2023-05-20 00:57:37Z vboxsync $
3
4"""
5Test Manager Core - Web Server Abstraction Base Class.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2023 Oracle and/or its affiliates.
11
12This file is part of VirtualBox base platform packages, as
13available from https://www.virtualbox.org.
14
15This program is free software; you can redistribute it and/or
16modify it under the terms of the GNU General Public License
17as published by the Free Software Foundation, in version 3 of the
18License.
19
20This program is distributed in the hope that it will be useful, but
21WITHOUT ANY WARRANTY; without even the implied warranty of
22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23General Public License for more details.
24
25You should have received a copy of the GNU General Public License
26along with this program; if not, see <https://www.gnu.org/licenses>.
27
28The contents of this file may alternatively be used under the terms
29of the Common Development and Distribution License Version 1.0
30(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
31in the VirtualBox distribution, in which case the provisions of the
32CDDL are applicable instead of those of the GPL.
33
34You may elect to license modified versions of this file under the
35terms and conditions of either the GPL or the CDDL or both.
36
37SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
38"""
39__version__ = "$Revision: 99875 $"
40
41
42# Standard python imports.
43import cgitb; # pylint: disable=deprecated-module ## @todo these will be retired in python 3.13!
44import codecs;
45import os
46import sys
47
48# Validation Kit imports.
49from common import webutils, utils;
50from testmanager import config;
51
52
53class WebServerGlueException(Exception):
54 """
55 For exceptions raised by glue code.
56 """
57 pass; # pylint: disable=unnecessary-pass
58
59
60class WebServerGlueBase(object):
61 """
62 Web server interface abstraction and some HTML utils.
63 """
64
65 ## Enables more debug output.
66 kfDebugInfoEnabled = True;
67
68 ## The maximum number of characters to cache.
69 kcchMaxCached = 65536;
70
71 ## Special getUserName return value.
72 ksUnknownUser = 'Unknown User';
73
74 ## HTTP status codes and their messages.
75 kdStatusMsgs = {
76 100: 'Continue',
77 101: 'Switching Protocols',
78 102: 'Processing',
79 103: 'Early Hints',
80 200: 'OK',
81 201: 'Created',
82 202: 'Accepted',
83 203: 'Non-Authoritative Information',
84 204: 'No Content',
85 205: 'Reset Content',
86 206: 'Partial Content',
87 207: 'Multi-Status',
88 208: 'Already Reported',
89 226: 'IM Used',
90 300: 'Multiple Choices',
91 301: 'Moved Permantently',
92 302: 'Found',
93 303: 'See Other',
94 304: 'Not Modified',
95 305: 'Use Proxy',
96 306: 'Switch Proxy',
97 307: 'Temporary Redirect',
98 308: 'Permanent Redirect',
99 400: 'Bad Request',
100 401: 'Unauthorized',
101 402: 'Payment Required',
102 403: 'Forbidden',
103 404: 'Not Found',
104 405: 'Method Not Allowed',
105 406: 'Not Acceptable',
106 407: 'Proxy Authentication Required',
107 408: 'Request Timeout',
108 409: 'Conflict',
109 410: 'Gone',
110 411: 'Length Required',
111 412: 'Precondition Failed',
112 413: 'Payload Too Large',
113 414: 'URI Too Long',
114 415: 'Unsupported Media Type',
115 416: 'Range Not Satisfiable',
116 417: 'Expectation Failed',
117 418: 'I\'m a teapot',
118 421: 'Misdirection Request',
119 422: 'Unprocessable Entity',
120 423: 'Locked',
121 424: 'Failed Dependency',
122 425: 'Too Early',
123 426: 'Upgrade Required',
124 428: 'Precondition Required',
125 429: 'Too Many Requests',
126 431: 'Request Header Fields Too Large',
127 451: 'Unavailable For Legal Reasons',
128 500: 'Internal Server Error',
129 501: 'Not Implemented',
130 502: 'Bad Gateway',
131 503: 'Service Unavailable',
132 504: 'Gateway Timeout',
133 505: 'HTTP Version Not Supported',
134 506: 'Variant Also Negotiates',
135 507: 'Insufficient Storage',
136 508: 'Loop Detected',
137 510: 'Not Extended',
138 511: 'Network Authentication Required',
139 };
140
141
142 def __init__(self, sValidationKitDir, fHtmlDebugOutput = True):
143 self._sValidationKitDir = sValidationKitDir;
144
145 # Debug
146 self.tsStart = utils.timestampNano();
147 self._fHtmlDebugOutput = fHtmlDebugOutput; # For trace
148 self._oDbgFile = sys.stderr;
149 if config.g_ksSrvGlueDebugLogDst is not None and config.g_kfSrvGlueDebug is True:
150 self._oDbgFile = open(config.g_ksSrvGlueDebugLogDst, 'a'); # pylint: disable=consider-using-with,unspecified-encoding
151 if config.g_kfSrvGlueCgiDumpArgs:
152 self._oDbgFile.write('Arguments: %s\nEnvironment:\n' % (sys.argv,));
153 if config.g_kfSrvGlueCgiDumpEnv:
154 for sVar in sorted(os.environ):
155 self._oDbgFile.write(' %s=\'%s\' \\\n' % (sVar, os.environ[sVar],));
156
157 self._afnDebugInfo = [];
158
159 # HTTP header.
160 self._fHeaderWrittenOut = False;
161 self._dHeaderFields = \
162 { \
163 'Content-Type': 'text/html; charset=utf-8',
164 };
165
166 # Body.
167 self._sBodyType = None;
168 self._dParams = {};
169 self._sHtmlBody = '';
170 self._cchCached = 0;
171 self._cchBodyWrittenOut = 0;
172
173 # Output.
174 if sys.version_info[0] >= 3:
175 self.oOutputRaw = sys.stdout.detach(); # pylint: disable=no-member
176 sys.stdout = None; # Prevents flush_std_files() from complaining on stderr during sys.exit().
177 else:
178 self.oOutputRaw = sys.stdout;
179 self.oOutputText = codecs.getwriter('utf-8')(self.oOutputRaw);
180
181
182 #
183 # Get stuff.
184 #
185
186 def getParameters(self):
187 """
188 Returns a dictionary with the query parameters.
189
190 The parameter name is the key, the values are given as lists. If a
191 parameter is given more than once, the value is appended to the
192 existing dictionary entry.
193 """
194 return {};
195
196 def getClientAddr(self):
197 """
198 Returns the client address, as a string.
199 """
200 raise WebServerGlueException('getClientAddr is not implemented');
201
202 def getMethod(self):
203 """
204 Gets the HTTP request method.
205 """
206 return 'POST';
207
208 def getLoginName(self):
209 """
210 Gets login name provided by Apache.
211 Returns kUnknownUser if not logged on.
212 """
213 return WebServerGlueBase.ksUnknownUser;
214
215 def getUrlScheme(self):
216 """
217 Gets scheme name (aka. access protocol) from request URL, i.e. 'http' or 'https'.
218 See also urlparse.scheme.
219 """
220 return 'http';
221
222 def getUrlNetLoc(self):
223 """
224 Gets the network location (server host name / ip) from the request URL.
225 See also urlparse.netloc.
226 """
227 raise WebServerGlueException('getUrlNetLoc is not implemented');
228
229 def getUrlPath(self):
230 """
231 Gets the hirarchical path (relative to server) from the request URL.
232 See also urlparse.path.
233 Note! This includes the leading slash.
234 """
235 raise WebServerGlueException('getUrlPath is not implemented');
236
237 def getUrlBasePath(self):
238 """
239 Gets the hirarchical base path (relative to server) from the request URL.
240 Note! This includes both a leading an trailing slash.
241 """
242 sPath = self.getUrlPath(); # virtual method # pylint: disable=assignment-from-no-return
243 iLastSlash = sPath.rfind('/');
244 if iLastSlash >= 0:
245 sPath = sPath[:iLastSlash];
246 sPath = sPath.rstrip('/');
247 return sPath + '/';
248
249 def getUrl(self):
250 """
251 Gets the URL being accessed, sans parameters.
252 For instance this will return, "http://localhost/testmanager/admin.cgi"
253 when "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
254 """
255 return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlPath());
256
257 def getBaseUrl(self):
258 """
259 Gets the base URL (with trailing slash).
260 For instance this will return, "http://localhost/testmanager/" when
261 "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
262 """
263 return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlBasePath());
264
265 def getUserAgent(self):
266 """
267 Gets the User-Agent field of the HTTP header, returning empty string
268 if not present.
269 """
270 return '';
271
272 def getContentType(self):
273 """
274 Gets the Content-Type field of the HTTP header, parsed into a type
275 string and a dictionary.
276 """
277 return ('text/html', {});
278
279 def getContentLength(self):
280 """
281 Gets the content length.
282 Returns int.
283 """
284 return 0;
285
286 def getBodyIoStream(self):
287 """
288 Returns file object for reading the HTML body.
289 """
290 raise WebServerGlueException('getUrlPath is not implemented');
291
292 def getBodyIoStreamBinary(self):
293 """
294 Returns file object for reading the binary HTML body.
295 """
296 raise WebServerGlueException('getBodyIoStreamBinary is not implemented');
297
298 #
299 # Output stuff.
300 #
301
302 def _writeHeader(self, sHeaderLine):
303 """
304 Worker function which child classes can override.
305 """
306 sys.stderr.write('_writeHeader: cch=%s "%s..."\n' % (len(sHeaderLine), sHeaderLine[0:10],))
307 self.oOutputText.write(sHeaderLine);
308 return True;
309
310 def flushHeader(self):
311 """
312 Flushes the HTTP header.
313 """
314 if self._fHeaderWrittenOut is False:
315 for sKey, sValue in self._dHeaderFields.items():
316 self._writeHeader('%s: %s\n' % (sKey, sValue,));
317 self._fHeaderWrittenOut = True;
318 self._writeHeader('\n'); # End of header indicator.
319 return None;
320
321 def setHeaderField(self, sField, sValue):
322 """
323 Sets a header field.
324 """
325 assert self._fHeaderWrittenOut is False;
326 self._dHeaderFields[sField] = sValue;
327 return True;
328
329 def setRedirect(self, sLocation, iCode = 302):
330 """
331 Sets up redirection of the page.
332 Raises an exception if called too late.
333 """
334 if self._fHeaderWrittenOut is True:
335 raise WebServerGlueException('setRedirect called after the header was written');
336 if iCode != 302:
337 raise WebServerGlueException('Redirection code %d is not supported' % (iCode,));
338
339 self.setHeaderField('Location', sLocation);
340 self.setHeaderField('Status', '302 Found');
341 return True;
342
343 def setStatus(self, iStatus, sMsg = None):
344 """ Sets the status code. """
345 if not sMsg:
346 sMsg = self.kdStatusMsgs[iStatus];
347 return self.setHeaderField('Status', '%u %s' % (iStatus, sMsg));
348
349 def setContentType(self, sType):
350 """ Sets the content type header field. """
351 return self.setHeaderField('Content-Type', sType);
352
353 def _writeWorker(self, sChunkOfHtml):
354 """
355 Worker function which child classes can override.
356 """
357 sys.stderr.write('_writeWorker: cch=%s "%s..."\n' % (len(sChunkOfHtml), sChunkOfHtml[0:10],))
358 self.oOutputText.write(sChunkOfHtml);
359 return True;
360
361 def write(self, sChunkOfHtml):
362 """
363 Writes chunk of HTML, making sure the HTTP header is flushed first.
364 """
365 if self._sBodyType is None:
366 self._sBodyType = 'html';
367 elif self._sBodyType != 'html':
368 raise WebServerGlueException('Cannot use writeParameter when body type is "%s"' % (self._sBodyType, ));
369
370 self._sHtmlBody += sChunkOfHtml;
371 self._cchCached += len(sChunkOfHtml);
372
373 if self._cchCached > self.kcchMaxCached:
374 self.flush();
375 return True;
376
377 def writeRaw(self, abChunk):
378 """
379 Writes a raw chunk the document. Can be binary or any encoding.
380 No caching.
381 """
382 if self._sBodyType is None:
383 self._sBodyType = 'raw';
384 elif self._sBodyType != 'raw':
385 raise WebServerGlueException('Cannot use writeRaw when body type is "%s"' % (self._sBodyType, ));
386
387 self.flushHeader();
388 if self._cchCached > 0:
389 self.flush();
390
391 sys.stderr.write('writeRaw: cb=%s\n' % (len(abChunk),))
392 self.oOutputRaw.write(abChunk);
393 return True;
394
395 def writeParams(self, dParams):
396 """
397 Writes one or more reply parameters in a form style response. The names
398 and values in dParams are unencoded, this method takes care of that.
399
400 Note! This automatically changes the content type to
401 'application/x-www-form-urlencoded', if the header hasn't been flushed
402 already.
403 """
404 if self._sBodyType is None:
405 if not self._fHeaderWrittenOut:
406 self.setHeaderField('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
407 elif self._dHeaderFields['Content-Type'] != 'application/x-www-form-urlencoded; charset=utf-8':
408 raise WebServerGlueException('Cannot use writeParams when content-type is "%s"' % \
409 (self._dHeaderFields['Content-Type'],));
410 self._sBodyType = 'form';
411
412 elif self._sBodyType != 'form':
413 raise WebServerGlueException('Cannot use writeParams when body type is "%s"' % (self._sBodyType, ));
414
415 for sKey in dParams:
416 sValue = str(dParams[sKey]);
417 self._dParams[sKey] = sValue;
418 self._cchCached += len(sKey) + len(sValue);
419
420 if self._cchCached > self.kcchMaxCached:
421 self.flush();
422
423 return True;
424
425 def flush(self):
426 """
427 Flush the output.
428 """
429 self.flushHeader();
430
431 if self._sBodyType == 'form':
432 sBody = webutils.encodeUrlParams(self._dParams);
433 self._writeWorker(sBody);
434
435 self._dParams = {};
436 self._cchBodyWrittenOut += self._cchCached;
437
438 elif self._sBodyType == 'html':
439 self._writeWorker(self._sHtmlBody);
440
441 self._sHtmlBody = '';
442 self._cchBodyWrittenOut += self._cchCached;
443
444 self._cchCached = 0;
445 return None;
446
447 #
448 # Paths.
449 #
450
451 def pathTmWebUI(self):
452 """
453 Gets the path to the TM 'webui' directory.
454 """
455 return os.path.join(self._sValidationKitDir, 'testmanager', 'webui');
456
457 #
458 # Error stuff & Debugging.
459 #
460
461 def errorLog(self, sError, aXcptInfo, sLogFile):
462 """
463 Writes the error to a log file.
464 """
465 # Easy solution for log file size: Only one report.
466 try: os.unlink(sLogFile);
467 except: pass;
468
469 # Try write the log file.
470 fRc = True;
471 fSaved = self._fHtmlDebugOutput;
472
473 try:
474 with open(sLogFile, 'w') as oFile: # pylint: disable=unspecified-encoding
475 oFile.write(sError + '\n\n');
476 if aXcptInfo[0] is not None:
477 oFile.write(' B a c k t r a c e\n');
478 oFile.write('===================\n');
479 oFile.write(cgitb.text(aXcptInfo, 5));
480 oFile.write('\n\n');
481
482 oFile.write(' D e b u g I n f o\n');
483 oFile.write('=====================\n\n');
484 self._fHtmlDebugOutput = False;
485 self.debugDumpStuff(oFile.write);
486 except:
487 fRc = False;
488
489 self._fHtmlDebugOutput = fSaved;
490 return fRc;
491
492 def errorPage(self, sError, aXcptInfo, sLogFile = None):
493 """
494 Displays a page with an error message.
495 """
496 if sLogFile is not None:
497 self.errorLog(sError, aXcptInfo, sLogFile);
498
499 # Reset buffering, hoping that nothing was flushed yet.
500 self._sBodyType = None;
501 self._sHtmlBody = '';
502 self._cchCached = 0;
503 if not self._fHeaderWrittenOut:
504 if self._fHtmlDebugOutput:
505 self.setHeaderField('Content-Type', 'text/html; charset=utf-8');
506 else:
507 self.setHeaderField('Content-Type', 'text/plain; charset=utf-8');
508
509 # Write the error page.
510 if self._fHtmlDebugOutput:
511 self.write('<html><head><title>Test Manage Error</title></head>\n' +
512 '<body><h1>Test Manager Error:</h1>\n' +
513 '<p>' + sError + '</p>\n');
514 else:
515 self.write(' Test Manage Error\n'
516 '===================\n'
517 '\n'
518 '' + sError + '\n\n');
519
520 if aXcptInfo[0] is not None:
521 if self._fHtmlDebugOutput:
522 self.write('<h1>Backtrace:</h1>\n');
523 self.write(cgitb.html(aXcptInfo, 5));
524 else:
525 self.write('Backtrace\n'
526 '---------\n'
527 '\n');
528 self.write(cgitb.text(aXcptInfo, 5));
529 self.write('\n\n');
530
531 if self.kfDebugInfoEnabled:
532 if self._fHtmlDebugOutput:
533 self.write('<h1>Debug Info:</h1>\n');
534 else:
535 self.write('Debug Info\n'
536 '----------\n'
537 '\n');
538 self.debugDumpStuff();
539
540 for fn in self._afnDebugInfo:
541 try:
542 fn(self, self._fHtmlDebugOutput);
543 except Exception as oXcpt:
544 self.write('\nDebug info callback %s raised exception: %s\n' % (fn, oXcpt));
545
546 if self._fHtmlDebugOutput:
547 self.write('</body></html>');
548
549 self.flush();
550
551 def debugInfoPage(self, fnWrite = None):
552 """
553 Dumps useful debug info.
554 """
555 if fnWrite is None:
556 fnWrite = self.write;
557
558 fnWrite('<html><head><title>Test Manage Debug Info</title></head>\n<body>\n');
559 self.debugDumpStuff(fnWrite = fnWrite);
560 fnWrite('</body></html>');
561 self.flush();
562
563 def debugDumpDict(self, sName, dDict, fSorted = True, fnWrite = None):
564 """
565 Dumps dictionary.
566 """
567 if fnWrite is None:
568 fnWrite = self.write;
569
570 asKeys = list(dDict.keys());
571 if fSorted:
572 asKeys.sort();
573
574 if self._fHtmlDebugOutput:
575 fnWrite('<h2>%s</h2>\n'
576 '<table border="1"><tr><th>name</th><th>value</th></tr>\n' % (sName,));
577 for sKey in asKeys:
578 fnWrite(' <tr><td>' + webutils.escapeElem(sKey) + '</td><td>' \
579 + webutils.escapeElem(str(dDict.get(sKey))) \
580 + '</td></tr>\n');
581 fnWrite('</table>\n');
582 else:
583 for i in range(len(sName) - 1):
584 fnWrite('%s ' % (sName[i],));
585 fnWrite('%s\n\n' % (sName[-1],));
586
587 fnWrite('%28s Value\n' % ('Name',));
588 fnWrite('------------------------------------------------------------------------\n');
589 for sKey in asKeys:
590 fnWrite('%28s: %s\n' % (sKey, dDict.get(sKey),));
591 fnWrite('\n');
592
593 return True;
594
595 def debugDumpList(self, sName, aoStuff, fnWrite = None):
596 """
597 Dumps array.
598 """
599 if fnWrite is None:
600 fnWrite = self.write;
601
602 if self._fHtmlDebugOutput:
603 fnWrite('<h2>%s</h2>\n'
604 '<table border="1"><tr><th>index</th><th>value</th></tr>\n' % (sName,));
605 for i, oStuff in enumerate(aoStuff):
606 fnWrite(' <tr><td>' + str(i) + '</td><td>' + webutils.escapeElem(str(oStuff)) + '</td></tr>\n');
607 fnWrite('</table>\n');
608 else:
609 for ch in sName[:-1]:
610 fnWrite('%s ' % (ch,));
611 fnWrite('%s\n\n' % (sName[-1],));
612
613 fnWrite('Index Value\n');
614 fnWrite('------------------------------------------------------------------------\n');
615 for i, oStuff in enumerate(aoStuff):
616 fnWrite('%5u %s\n' % (i, str(oStuff)));
617 fnWrite('\n');
618
619 return True;
620
621 def debugDumpParameters(self, fnWrite):
622 """ Dumps request parameters. """
623 if fnWrite is None:
624 fnWrite = self.write;
625
626 try:
627 dParams = self.getParameters();
628 return self.debugDumpDict('Parameters', dParams);
629 except Exception as oXcpt:
630 if self._fHtmlDebugOutput:
631 fnWrite('<p>Exception %s while retriving parameters.</p>\n' % (oXcpt,))
632 else:
633 fnWrite('Exception %s while retriving parameters.\n' % (oXcpt,))
634 return False;
635
636 def debugDumpEnv(self, fnWrite = None):
637 """ Dumps os.environ. """
638 return self.debugDumpDict('Environment (os.environ)', os.environ, fnWrite = fnWrite);
639
640 def debugDumpArgv(self, fnWrite = None):
641 """ Dumps sys.argv. """
642 return self.debugDumpList('Arguments (sys.argv)', sys.argv, fnWrite = fnWrite);
643
644 def debugDumpPython(self, fnWrite = None):
645 """
646 Dump python info.
647 """
648 dInfo = {};
649 dInfo['sys.version'] = sys.version;
650 dInfo['sys.hexversion'] = sys.hexversion;
651 dInfo['sys.api_version'] = sys.api_version;
652 if hasattr(sys, 'subversion'):
653 dInfo['sys.subversion'] = sys.subversion; # pylint: disable=no-member
654 dInfo['sys.platform'] = sys.platform;
655 dInfo['sys.executable'] = sys.executable;
656 dInfo['sys.copyright'] = sys.copyright;
657 dInfo['sys.byteorder'] = sys.byteorder;
658 dInfo['sys.exec_prefix'] = sys.exec_prefix;
659 dInfo['sys.prefix'] = sys.prefix;
660 dInfo['sys.path'] = sys.path;
661 dInfo['sys.builtin_module_names'] = sys.builtin_module_names;
662 dInfo['sys.flags'] = sys.flags;
663
664 return self.debugDumpDict('Python Info', dInfo, fnWrite = fnWrite);
665
666
667 def debugDumpStuff(self, fnWrite = None):
668 """
669 Dumps stuff to the error page and debug info page.
670 Should be extended by child classes when possible.
671 """
672 self.debugDumpParameters(fnWrite);
673 self.debugDumpEnv(fnWrite);
674 self.debugDumpArgv(fnWrite);
675 self.debugDumpPython(fnWrite);
676 return True;
677
678 def dprint(self, sMessage):
679 """
680 Prints to debug log (usually apache error log).
681 """
682 if config.g_kfSrvGlueDebug is True:
683 if config.g_kfSrvGlueDebugTS is False:
684 self._oDbgFile.write(sMessage);
685 if not sMessage.endswith('\n'):
686 self._oDbgFile.write('\n');
687 else:
688 tsNow = utils.timestampMilli();
689 tsReq = tsNow - (self.tsStart / 1000000);
690 iPid = os.getpid();
691 for sLine in sMessage.split('\n'):
692 self._oDbgFile.write('%s/%03u,pid=%04x: %s\n' % (tsNow, tsReq, iPid, sLine,));
693
694 return True;
695
696 def registerDebugInfoCallback(self, fnDebugInfo):
697 """
698 Registers a debug info method for calling when the error page is shown.
699
700 The fnDebugInfo function takes two parameters. The first is this
701 object, the second is a boolean indicating html (True) or text (False)
702 output. The return value is ignored.
703 """
704 if self.kfDebugInfoEnabled:
705 self._afnDebugInfo.append(fnDebugInfo);
706 return True;
707
708 def unregisterDebugInfoCallback(self, fnDebugInfo):
709 """
710 Unregisters a debug info method previously registered by
711 registerDebugInfoCallback.
712 """
713 if self.kfDebugInfoEnabled:
714 try: self._afnDebugInfo.remove(fnDebugInfo);
715 except: pass;
716 return True;
717
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette