VirtualBox

source: vbox/trunk/src/VBox/Devices/Storage/DrvSCSIHost.cpp@ 59176

Last change on this file since 59176 was 57393, checked in by vboxsync, 9 years ago

DECLCALLBACK

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 17.7 KB
Line 
1/* $Id: DrvSCSIHost.cpp 57393 2015-08-17 15:02:05Z vboxsync $ */
2/** @file
3 * VBox storage drivers: Host SCSI access driver.
4 */
5
6/*
7 * Copyright (C) 2006-2015 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18
19/*********************************************************************************************************************************
20* Header Files *
21*********************************************************************************************************************************/
22//#define DEBUG
23#define LOG_GROUP LOG_GROUP_DRV_SCSIHOST
24#include <VBox/vmm/pdmdrv.h>
25#include <VBox/vmm/pdmifs.h>
26#include <VBox/vmm/pdmthread.h>
27#include <VBox/scsi.h>
28#include <iprt/assert.h>
29#include <iprt/file.h>
30#include <iprt/mem.h>
31#include <iprt/req.h>
32#include <iprt/string.h>
33#include <iprt/uuid.h>
34
35#if defined(RT_OS_LINUX)
36# include <limits.h>
37# include <scsi/sg.h>
38# include <sys/ioctl.h>
39#endif
40
41#include "VBoxDD.h"
42
43/**
44 * SCSI driver instance data.
45 *
46 * @implements PDMISCSICONNECTOR
47 */
48typedef struct DRVSCSIHOST
49{
50 /** Pointer driver instance. */
51 PPDMDRVINS pDrvIns;
52
53 /** Pointer to the SCSI port interface of the device above. */
54 PPDMISCSIPORT pDevScsiPort;
55 /** The SCSI connector interface. */
56 PDMISCSICONNECTOR ISCSIConnector;
57
58 /** Path to the device file. */
59 char *pszDevicePath;
60 /** Handle to the device. */
61 RTFILE hDeviceFile;
62
63 /** The dedicated I/O thread. */
64 PPDMTHREAD pAsyncIOThread;
65 /** Queue for passing the requests to the thread. */
66 RTREQQUEUE hQueueRequests;
67} DRVSCSIHOST, *PDRVSCSIHOST;
68
69/** Converts a pointer to DRVSCSIHOST::ISCSIConnecotr to a PDRVSCSIHOST. */
70#define PDMISCSICONNECTOR_2_DRVSCSIHOST(pInterface) ( (PDRVSCSIHOST)((uintptr_t)pInterface - RT_OFFSETOF(DRVSCSIHOST, ISCSIConnector)) )
71
72#ifdef DEBUG
73/**
74 * Dumps a SCSI request structure for debugging purposes.
75 *
76 * @returns nothing.
77 * @param pRequest Pointer to the request to dump.
78 */
79static void drvscsihostDumpScsiRequest(PPDMSCSIREQUEST pRequest)
80{
81 Log(("Dump for pRequest=%#p Command: %s\n", pRequest, SCSICmdText(pRequest->pbCDB[0])));
82 Log(("cbCDB=%u\n", pRequest->cbCDB));
83 for (uint32_t i = 0; i < pRequest->cbCDB; i++)
84 Log(("pbCDB[%u]=%#x\n", i, pRequest->pbCDB[i]));
85 Log(("cbScatterGather=%u\n", pRequest->cbScatterGather));
86 Log(("cScatterGatherEntries=%u\n", pRequest->cScatterGatherEntries));
87 /* Print all scatter gather entries. */
88 for (uint32_t i = 0; i < pRequest->cScatterGatherEntries; i++)
89 {
90 Log(("ScatterGatherEntry[%u].cbSeg=%u\n", i, pRequest->paScatterGatherHead[i].cbSeg));
91 Log(("ScatterGatherEntry[%u].pvSeg=%#p\n", i, pRequest->paScatterGatherHead[i].pvSeg));
92 }
93 Log(("pvUser=%#p\n", pRequest->pvUser));
94}
95#endif
96
97/**
98 * Copy the content of a buffer to a scatter gather list
99 * copying only the amount of data which fits into the
100 * scatter gather list.
101 *
102 * @returns VBox status code.
103 * @param pRequest Pointer to the request which contains the S/G list entries.
104 * @param pvBuf Pointer to the buffer which should be copied.
105 * @param cbBuf Size of the buffer.
106 */
107static int drvscsihostScatterGatherListCopyFromBuffer(PPDMSCSIREQUEST pRequest, void *pvBuf, size_t cbBuf)
108{
109 unsigned cSGEntry = 0;
110 PRTSGSEG pSGEntry = &pRequest->paScatterGatherHead[cSGEntry];
111 uint8_t *pu8Buf = (uint8_t *)pvBuf;
112
113 while (cSGEntry < pRequest->cScatterGatherEntries)
114 {
115 size_t cbToCopy = (cbBuf < pSGEntry->cbSeg) ? cbBuf : pSGEntry->cbSeg;
116
117 memcpy(pSGEntry->pvSeg, pu8Buf, cbToCopy);
118
119 cbBuf -= cbToCopy;
120 /* We finished. */
121 if (!cbBuf)
122 break;
123
124 /* Advance the buffer. */
125 pu8Buf += cbToCopy;
126
127 /* Go to the next entry in the list. */
128 pSGEntry++;
129 cSGEntry++;
130 }
131
132 return VINF_SUCCESS;
133}
134
135/**
136 * Set the sense and advanced sense key in the buffer for error conditions.
137 *
138 * @returns nothing.
139 * @param pRequest Pointer to the request which contains the sense buffer.
140 * @param uSCSISenseKey The sense key to set.
141 * @param uSCSIASC The advanced sense key to set.
142 */
143DECLINLINE(void) drvscsiCmdError(PPDMSCSIREQUEST pRequest, uint8_t uSCSISenseKey, uint8_t uSCSIASC)
144{
145 AssertMsg(pRequest->cbSenseBuffer >= 2, ("Sense buffer is not big enough\n"));
146 AssertMsg(pRequest->pbSenseBuffer, ("Sense buffer pointer is NULL\n"));
147 pRequest->pbSenseBuffer[0] = uSCSISenseKey;
148 pRequest->pbSenseBuffer[1] = uSCSIASC;
149}
150
151/**
152 * Sets the sense key for a status good condition.
153 *
154 * @returns nothing.
155 * @param pRequest Pointer to the request which contains the sense buffer.
156 */
157DECLINLINE(void) drvscsihostCmdOk(PPDMSCSIREQUEST pRequest)
158{
159 AssertMsg(pRequest->cbSenseBuffer >= 2, ("Sense buffer is not big enough\n"));
160 AssertMsg(pRequest->pbSenseBuffer, ("Sense buffer pointer is NULL\n"));
161 pRequest->pbSenseBuffer[0] = SCSI_SENSE_NONE;
162 pRequest->pbSenseBuffer[1] = SCSI_ASC_NONE;
163}
164
165/**
166 * Returns the transfer direction of the given command
167 * in case the device does not provide this info.
168 *
169 * @returns transfer direction of the command.
170 * SCSIHOSTTXDIR_NONE if no data is transferred.
171 * SCSIHOSTTXDIR_FROM_DEVICE if the data is read from the device.
172 * SCSIHOSTTXDIR_TO_DEVICE if the data is written to the device.
173 * @param uCommand The command byte.
174 */
175static unsigned drvscsihostGetTransferDirectionFromCommand(uint8_t uCommand)
176{
177 switch (uCommand)
178 {
179 case SCSI_INQUIRY:
180 case SCSI_REPORT_LUNS:
181 case SCSI_MODE_SENSE_6:
182 case SCSI_READ_TOC_PMA_ATIP:
183 case SCSI_READ_CAPACITY:
184 case SCSI_MODE_SENSE_10:
185 case SCSI_GET_EVENT_STATUS_NOTIFICATION:
186 case SCSI_GET_CONFIGURATION:
187 case SCSI_READ_10:
188 case SCSI_READ_12:
189 case SCSI_READ_BUFFER:
190 case SCSI_READ_BUFFER_CAPACITY:
191 case SCSI_READ_DISC_INFORMATION:
192 case SCSI_READ_DVD_STRUCTURE:
193 case SCSI_READ_FORMAT_CAPACITIES:
194 case SCSI_READ_SUBCHANNEL:
195 case SCSI_READ_TRACK_INFORMATION:
196 case SCSI_READ_CD:
197 case SCSI_READ_CD_MSF:
198 return PDMSCSIREQUESTTXDIR_FROM_DEVICE;
199 case SCSI_TEST_UNIT_READY:
200 case SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL:
201 case SCSI_START_STOP_UNIT:
202 return PDMSCSIREQUESTTXDIR_NONE;
203 case SCSI_WRITE_10:
204 case SCSI_WRITE_12:
205 case SCSI_WRITE_BUFFER:
206 return PDMSCSIREQUESTTXDIR_TO_DEVICE;
207 default:
208 AssertMsgFailed(("Command not known %#x\n", uCommand));
209 }
210
211 /* We should never get here in debug mode. */
212 AssertMsgFailed(("Impossible to get here!!!\n"));
213 return PDMSCSIREQUESTTXDIR_NONE; /* to make compilers happy. */
214}
215
216static int drvscsihostProcessRequestOne(PDRVSCSIHOST pThis, PPDMSCSIREQUEST pRequest)
217{
218 int rc = VINF_SUCCESS;
219 unsigned uTxDir;
220
221 LogFlowFunc(("Entered\n"));
222
223#ifdef DEBUG
224 drvscsihostDumpScsiRequest(pRequest);
225#endif
226
227 /* We implement only one device. */
228 if (pRequest->uLogicalUnit != 0)
229 {
230 switch (pRequest->pbCDB[0])
231 {
232 case SCSI_INQUIRY:
233 {
234 SCSIINQUIRYDATA ScsiInquiryReply;
235
236 memset(&ScsiInquiryReply, 0, sizeof(ScsiInquiryReply));
237
238 ScsiInquiryReply.u5PeripheralDeviceType = SCSI_INQUIRY_DATA_PERIPHERAL_DEVICE_TYPE_UNKNOWN;
239 ScsiInquiryReply.u3PeripheralQualifier = SCSI_INQUIRY_DATA_PERIPHERAL_QUALIFIER_NOT_CONNECTED_NOT_SUPPORTED;
240 drvscsihostScatterGatherListCopyFromBuffer(pRequest, &ScsiInquiryReply, sizeof(SCSIINQUIRYDATA));
241 drvscsihostCmdOk(pRequest);
242 break;
243 }
244 default:
245 AssertMsgFailed(("Command not implemented for attached device\n"));
246 drvscsiCmdError(pRequest, SCSI_SENSE_ILLEGAL_REQUEST, SCSI_ASC_NONE);
247 }
248 }
249 else
250 {
251#if defined(RT_OS_LINUX)
252 sg_io_hdr_t ScsiIoReq;
253 sg_iovec_t *paSG = NULL;
254
255 /* Setup SCSI request. */
256 memset(&ScsiIoReq, 0, sizeof(sg_io_hdr_t));
257 ScsiIoReq.interface_id = 'S';
258
259 if (pRequest->uDataDirection == PDMSCSIREQUESTTXDIR_UNKNOWN)
260 uTxDir = drvscsihostGetTransferDirectionFromCommand(pRequest->pbCDB[0]);
261 else
262 uTxDir = pRequest->uDataDirection;
263
264 if (uTxDir == PDMSCSIREQUESTTXDIR_NONE)
265 ScsiIoReq.dxfer_direction = SG_DXFER_NONE;
266 else if (uTxDir == PDMSCSIREQUESTTXDIR_TO_DEVICE)
267 ScsiIoReq.dxfer_direction = SG_DXFER_TO_DEV;
268 else if (uTxDir == PDMSCSIREQUESTTXDIR_FROM_DEVICE)
269 ScsiIoReq.dxfer_direction = SG_DXFER_FROM_DEV;
270 else
271 AssertMsgFailed(("Invalid transfer direction %u\n", uTxDir));
272
273 ScsiIoReq.cmd_len = pRequest->cbCDB;
274 ScsiIoReq.mx_sb_len = pRequest->cbSenseBuffer;
275 ScsiIoReq.dxfer_len = pRequest->cbScatterGather;
276
277 if (pRequest->cScatterGatherEntries > 0)
278 {
279 if (pRequest->cScatterGatherEntries == 1)
280 {
281 ScsiIoReq.iovec_count = 0;
282 ScsiIoReq.dxferp = pRequest->paScatterGatherHead[0].pvSeg;
283 }
284 else
285 {
286 ScsiIoReq.iovec_count = pRequest->cScatterGatherEntries;
287
288 paSG = (sg_iovec_t *)RTMemAllocZ(pRequest->cScatterGatherEntries * sizeof(sg_iovec_t));
289 AssertReturn(paSG, VERR_NO_MEMORY);
290
291 for (unsigned i = 0; i < pRequest->cScatterGatherEntries; i++)
292 {
293 paSG[i].iov_base = pRequest->paScatterGatherHead[i].pvSeg;
294 paSG[i].iov_len = pRequest->paScatterGatherHead[i].cbSeg;
295 }
296 ScsiIoReq.dxferp = paSG;
297 }
298 }
299
300 ScsiIoReq.cmdp = pRequest->pbCDB;
301 ScsiIoReq.sbp = pRequest->pbSenseBuffer;
302 ScsiIoReq.timeout = UINT_MAX;
303 ScsiIoReq.flags |= SG_FLAG_DIRECT_IO;
304
305 /* Issue command. */
306 rc = ioctl(RTFileToNative(pThis->hDeviceFile), SG_IO, &ScsiIoReq);
307 if (rc < 0)
308 {
309 AssertMsgFailed(("Ioctl failed with rc=%d\n", rc));
310 }
311
312 /* Request processed successfully. */
313 Log(("Command successfully processed\n"));
314 if (ScsiIoReq.iovec_count > 0)
315 RTMemFree(paSG);
316#endif
317 }
318 /* Notify device that request finished. */
319 rc = pThis->pDevScsiPort->pfnSCSIRequestCompleted(pThis->pDevScsiPort, pRequest, SCSI_STATUS_OK, false, VINF_SUCCESS);
320 AssertMsgRC(rc, ("Notifying device above failed rc=%Rrc\n", rc));
321
322 return rc;
323
324}
325
326/**
327 * Request function to wakeup the thread.
328 *
329 * @returns VWRN_STATE_CHANGED.
330 */
331static int drvscsihostAsyncIOLoopWakeupFunc(void)
332{
333 return VWRN_STATE_CHANGED;
334}
335
336/**
337 * The thread function which processes the requests asynchronously.
338 *
339 * @returns VBox status code.
340 * @param pDrvIns Pointer to the device instance data.
341 * @param pThread Pointer to the thread instance data.
342 */
343static DECLCALLBACK(int) drvscsihostAsyncIOLoop(PPDMDRVINS pDrvIns, PPDMTHREAD pThread)
344{
345 int rc = VINF_SUCCESS;
346 PDRVSCSIHOST pThis = PDMINS_2_DATA(pDrvIns, PDRVSCSIHOST);
347
348 LogFlowFunc(("Entering async IO loop.\n"));
349
350 if (pThread->enmState == PDMTHREADSTATE_INITIALIZING)
351 return VINF_SUCCESS;
352
353 while (pThread->enmState == PDMTHREADSTATE_RUNNING)
354 {
355 rc = RTReqQueueProcess(pThis->hQueueRequests, RT_INDEFINITE_WAIT);
356 AssertMsg(rc == VWRN_STATE_CHANGED, ("Left RTReqProcess and error code is not VWRN_STATE_CHANGED rc=%Rrc\n", rc));
357 }
358
359 return VINF_SUCCESS;
360}
361
362static DECLCALLBACK(int) drvscsihostAsyncIOLoopWakeup(PPDMDRVINS pDrvIns, PPDMTHREAD pThread)
363{
364 int rc;
365 PDRVSCSIHOST pThis = PDMINS_2_DATA(pDrvIns, PDRVSCSIHOST);
366 PRTREQ pReq;
367
368 AssertReturn(pThis->hQueueRequests != NIL_RTREQQUEUE, VERR_INVALID_STATE);
369
370 rc = RTReqQueueCall(pThis->hQueueRequests, &pReq, 10000 /* 10 sec. */, (PFNRT)drvscsihostAsyncIOLoopWakeupFunc, 0);
371 AssertMsgRC(rc, ("Inserting request into queue failed rc=%Rrc\n", rc));
372
373 return rc;
374}
375
376/* -=-=-=-=- ISCSIConnector -=-=-=-=- */
377
378/** @copydoc PDMISCSICONNECTOR::pfnSCSIRequestSend. */
379static DECLCALLBACK(int) drvscsihostRequestSend(PPDMISCSICONNECTOR pInterface, PPDMSCSIREQUEST pSCSIRequest)
380{
381 int rc;
382 PDRVSCSIHOST pThis = PDMISCSICONNECTOR_2_DRVSCSIHOST(pInterface);
383 PRTREQ pReq;
384
385 AssertReturn(pThis->hQueueRequests != NIL_RTREQQUEUE, VERR_INVALID_STATE);
386
387 rc = RTReqQueueCallEx(pThis->hQueueRequests, &pReq, 0, RTREQFLAGS_NO_WAIT, (PFNRT)drvscsihostProcessRequestOne, 2, pThis, pSCSIRequest);
388 AssertMsgReturn(RT_SUCCESS(rc), ("Inserting request into queue failed rc=%Rrc\n", rc), rc);
389
390 return VINF_SUCCESS;
391}
392
393/* -=-=-=-=- PDMIBASE -=-=-=-=- */
394
395/**
396 * @interface_method_impl{PDMIBASE,pfnQueryInterface}
397 */
398static DECLCALLBACK(void *) drvscsihostQueryInterface(PPDMIBASE pInterface, const char *pszIID)
399{
400 PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface);
401 PDRVSCSIHOST pThis = PDMINS_2_DATA(pDrvIns, PDRVSCSIHOST);
402
403 PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase);
404 PDMIBASE_RETURN_INTERFACE(pszIID, PDMISCSICONNECTOR, &pThis->ISCSIConnector);
405 return NULL;
406}
407
408/* -=-=-=-=- PDMDRVREG -=-=-=-=- */
409
410/**
411 * Destruct a driver instance.
412 *
413 * Most VM resources are freed by the VM. This callback is provided so that any non-VM
414 * resources can be freed correctly.
415 *
416 * @param pDrvIns The driver instance data.
417 */
418static DECLCALLBACK(void) drvscsihostDestruct(PPDMDRVINS pDrvIns)
419{
420 PDRVSCSIHOST pThis = PDMINS_2_DATA(pDrvIns, PDRVSCSIHOST);
421 PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns);
422
423 RTFileClose(pThis->hDeviceFile);
424 pThis->hDeviceFile = NIL_RTFILE;
425
426 if (pThis->pszDevicePath)
427 {
428 MMR3HeapFree(pThis->pszDevicePath);
429 pThis->pszDevicePath = NULL;
430 }
431
432 if (pThis->hQueueRequests != NIL_RTREQQUEUE)
433 {
434 int rc = RTReqQueueDestroy(pThis->hQueueRequests);
435 AssertMsgRC(rc, ("Failed to destroy queue rc=%Rrc\n", rc));
436 pThis->hQueueRequests = NIL_RTREQQUEUE;
437 }
438
439}
440
441/**
442 * Construct a block driver instance.
443 *
444 * @copydoc FNPDMDRVCONSTRUCT
445 */
446static DECLCALLBACK(int) drvscsihostConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags)
447{
448 PDRVSCSIHOST pThis = PDMINS_2_DATA(pDrvIns, PDRVSCSIHOST);
449 LogFlowFunc(("pDrvIns=%#p pCfg=%#p\n", pDrvIns, pCfg));
450 PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns);
451
452 /*
453 * Initialize the instance data first because of the destructor.
454 */
455 pDrvIns->IBase.pfnQueryInterface = drvscsihostQueryInterface;
456 pThis->ISCSIConnector.pfnSCSIRequestSend = drvscsihostRequestSend;
457 pThis->pDrvIns = pDrvIns;
458 pThis->hDeviceFile = NIL_RTFILE;
459 pThis->hQueueRequests = NIL_RTREQQUEUE;
460
461 /*
462 * Read the configuration.
463 */
464 if (!CFGMR3AreValuesValid(pCfg, "DevicePath\0"))
465 return PDMDRV_SET_ERROR(pDrvIns, VERR_PDM_DRVINS_UNKNOWN_CFG_VALUES,
466 N_("Invalid configuration for host scsi access driver"));
467
468
469 /* Query the SCSI port interface above. */
470 pThis->pDevScsiPort = PDMIBASE_QUERY_INTERFACE(pDrvIns->pUpBase, PDMISCSIPORT);
471 AssertMsgReturn(pThis->pDevScsiPort, ("Missing SCSI port interface above\n"), VERR_PDM_MISSING_INTERFACE);
472
473 /* Create request queue. */
474 int rc = RTReqQueueCreate(&pThis->hQueueRequests);
475 AssertMsgReturn(RT_SUCCESS(rc), ("Failed to create request queue rc=%Rrc\n", rc), rc);
476
477 /* Open the device. */
478 rc = CFGMR3QueryStringAlloc(pCfg, "DevicePath", &pThis->pszDevicePath);
479 if (RT_FAILURE(rc))
480 return PDMDRV_SET_ERROR(pDrvIns, rc,
481 N_("Configuration error: Failed to get the \"DevicePath\" value"));
482
483 rc = RTFileOpen(&pThis->hDeviceFile, pThis->pszDevicePath, RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
484 if (RT_FAILURE(rc))
485 return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS,
486 N_("DrvSCSIHost#%d: Failed to open device '%s'"), pDrvIns->iInstance, pThis->pszDevicePath);
487
488 /* Create I/O thread. */
489 rc = PDMDrvHlpThreadCreate(pDrvIns, &pThis->pAsyncIOThread, pThis, drvscsihostAsyncIOLoop,
490 drvscsihostAsyncIOLoopWakeup, 0, RTTHREADTYPE_IO, "SCSI async IO");
491 AssertMsgReturn(RT_SUCCESS(rc), ("Failed to create async I/O thread rc=%Rrc\n", rc), rc);
492
493 return VINF_SUCCESS;
494}
495
496/**
497 * SCSI driver registration record.
498 */
499const PDMDRVREG g_DrvSCSIHost =
500{
501 /* u32Version */
502 PDM_DRVREG_VERSION,
503 /* szName */
504 "SCSIHost",
505 /* szRCMod */
506 "",
507 /* szR0Mod */
508 "",
509 /* pszDescription */
510 "Host SCSI driver.",
511 /* fFlags */
512 PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT,
513 /* fClass. */
514 PDM_DRVREG_CLASS_SCSI,
515 /* cMaxInstances */
516 ~0U,
517 /* cbInstance */
518 sizeof(DRVSCSIHOST),
519 /* pfnConstruct */
520 drvscsihostConstruct,
521 /* pfnDestruct */
522 drvscsihostDestruct,
523 /* pfnRelocate */
524 NULL,
525 /* pfnIOCtl */
526 NULL,
527 /* pfnPowerOn */
528 NULL,
529 /* pfnReset */
530 NULL,
531 /* pfnSuspend */
532 NULL,
533 /* pfnResume */
534 NULL,
535 /* pfnAttach */
536 NULL,
537 /* pfnDetach */
538 NULL,
539 /* pfnPowerOff */
540 NULL,
541 /* pfnSoftReset */
542 NULL,
543 /* u32EndVersion */
544 PDM_DRVREG_VERSION
545};
546
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