VirtualBox

source: vbox/trunk/src/VBox/Main/src-server/freebsd/HostHardwareFreeBSD.cpp@ 98110

Last change on this file since 98110 was 98103, checked in by vboxsync, 23 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 22.1 KB
Line 
1/* $Id: HostHardwareFreeBSD.cpp 98103 2023-01-17 14:15:46Z vboxsync $ */
2/** @file
3 * VirtualBox Main - Code for handling hardware detection under FreeBSD, VBoxSVC.
4 */
5
6/*
7 * Copyright (C) 2008-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28
29/*********************************************************************************************************************************
30* Header Files *
31*********************************************************************************************************************************/
32#define LOG_GROUP LOG_GROUP_MAIN
33#include "HostHardwareLinux.h"
34
35#include <VBox/log.h>
36
37#include <iprt/dir.h>
38#include <iprt/env.h>
39#include <iprt/file.h>
40#include <iprt/mem.h>
41#include <iprt/param.h>
42#include <iprt/path.h>
43#include <iprt/string.h>
44
45#include <sys/param.h>
46#include <sys/types.h>
47#include <sys/stat.h>
48#include <unistd.h>
49#include <stdio.h>
50#include <sys/ioctl.h>
51#include <fcntl.h>
52#include <cam/cam.h>
53#include <cam/cam_ccb.h>
54#include <camlib.h>
55#include <cam/scsi/scsi_pass.h>
56
57#include <vector>
58
59
60/*********************************************************************************************************************************
61* Typedefs and Defines *
62*********************************************************************************************************************************/
63typedef enum DriveType_T
64{
65 Fixed,
66 DVD,
67 Any
68} DriveType_T;
69
70
71/*********************************************************************************************************************************
72* Internal Functions *
73*********************************************************************************************************************************/
74static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_DEF;
75static int getDriveInfoFromCAM(DriveInfoList *pList, DriveType_T enmDriveType, bool *pfSuccess) RT_NOTHROW_DEF;
76
77
78/** Find the length of a string, ignoring trailing non-ascii or control
79 * characters
80 * @note Code duplicated in HostHardwareLinux.cpp */
81static size_t strLenStripped(const char *pcsz) RT_NOTHROW_DEF
82{
83 size_t cch = 0;
84 for (size_t i = 0; pcsz[i] != '\0'; ++i)
85 if (pcsz[i] > 32 /*space*/ && pcsz[i] < 127 /*delete*/)
86 cch = i;
87 return cch + 1;
88}
89
90
91/**
92 * Initialize the device description for a drive based on vendor and model name
93 * strings.
94 *
95 * @param pcszVendor The raw vendor ID string.
96 * @param pcszModel The raw product ID string.
97 * @param pszDesc Where to store the description string (optional)
98 * @param cbDesc The size of the buffer in @pszDesc
99 *
100 * @note Used for disks as well as DVDs.
101 */
102/* static */
103void dvdCreateDeviceString(const char *pcszVendor, const char *pcszModel, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
104{
105 AssertPtrReturnVoid(pcszVendor);
106 AssertPtrReturnVoid(pcszModel);
107 AssertPtrNullReturnVoid(pszDesc);
108 AssertReturnVoid(!pszDesc || cbDesc > 0);
109 size_t cchVendor = strLenStripped(pcszVendor);
110 size_t cchModel = strLenStripped(pcszModel);
111
112 /* Construct the description string as "Vendor Product" */
113 if (pszDesc)
114 {
115 if (cchVendor > 0)
116 RTStrPrintf(pszDesc, cbDesc, "%.*s %s", cchVendor, pcszVendor,
117 cchModel > 0 ? pcszModel : "(unknown drive model)");
118 else
119 RTStrPrintf(pszDesc, cbDesc, "%s", pcszModel);
120 RTStrPurgeEncoding(pszDesc);
121 }
122}
123
124
125int VBoxMainDriveInfo::updateDVDs() RT_NOEXCEPT
126{
127 LogFlowThisFunc(("entered\n"));
128 int rc;
129 try
130 {
131 mDVDList.clear();
132 /* Always allow the user to override our auto-detection using an
133 * environment variable. */
134 bool fSuccess = false; /* Have we succeeded in finding anything yet? */
135 rc = getDriveInfoFromEnv("VBOX_CDROM", &mDVDList, true /* isDVD */, &fSuccess);
136 if (RT_SUCCESS(rc) && !fSuccess)
137 rc = getDriveInfoFromCAM(&mDVDList, DVD, &fSuccess);
138 }
139 catch (std::bad_alloc &)
140 {
141 rc = VERR_NO_MEMORY;
142 }
143 LogFlowThisFunc(("rc=%Rrc\n", rc));
144 return rc;
145}
146
147int VBoxMainDriveInfo::updateFloppies() RT_NOEXCEPT
148{
149 LogFlowThisFunc(("entered\n"));
150 int rc;
151 try
152 {
153 /* Only got the enviornment variable here... */
154 mFloppyList.clear();
155 bool fSuccess = false; /* ignored */
156 rc = getDriveInfoFromEnv("VBOX_FLOPPY", &mFloppyList, false /* isDVD */, &fSuccess);
157 }
158 catch (std::bad_alloc &)
159 {
160 rc = VERR_NO_MEMORY;
161 }
162 LogFlowThisFunc(("rc=%Rrc\n", rc));
163 return rc;
164}
165
166int VBoxMainDriveInfo::updateFixedDrives() RT_NOEXCEPT
167{
168 LogFlowThisFunc(("entered\n"));
169 int rc;
170 try
171 {
172 mFixedDriveList.clear();
173 bool fSuccess = false; /* ignored */
174 rc = getDriveInfoFromCAM(&mFixedDriveList, Fixed, &fSuccess);
175 }
176 catch (std::bad_alloc &)
177 {
178 rc = VERR_NO_MEMORY;
179 }
180 LogFlowThisFunc(("rc=%Rrc\n", rc));
181 return rc;
182}
183
184static void strDeviceStringSCSI(device_match_result *pDevResult, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
185{
186 char szVendor[128];
187 cam_strvis((uint8_t *)szVendor, (const uint8_t *)pDevResult->inq_data.vendor,
188 sizeof(pDevResult->inq_data.vendor), sizeof(szVendor));
189 char szProduct[128];
190 cam_strvis((uint8_t *)szProduct, (const uint8_t *)pDevResult->inq_data.product,
191 sizeof(pDevResult->inq_data.product), sizeof(szProduct));
192 dvdCreateDeviceString(szVendor, szProduct, pszDesc, cbDesc);
193}
194
195static void strDeviceStringATA(device_match_result *pDevResult, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
196{
197 char szProduct[256];
198 cam_strvis((uint8_t *)szProduct, (const uint8_t *)pDevResult->ident_data.model,
199 sizeof(pDevResult->ident_data.model), sizeof(szProduct));
200 dvdCreateDeviceString("", szProduct, pszDesc, cbDesc);
201}
202
203static void strDeviceStringSEMB(device_match_result *pDevResult, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
204{
205 sep_identify_data *pSid = (sep_identify_data *)&pDevResult->ident_data;
206
207 char szVendor[128];
208 cam_strvis((uint8_t *)szVendor, (const uint8_t *)pSid->vendor_id,
209 sizeof(pSid->vendor_id), sizeof(szVendor));
210 char szProduct[128];
211 cam_strvis((uint8_t *)szProduct, (const uint8_t *)pSid->product_id,
212 sizeof(pSid->product_id), sizeof(szProduct));
213 dvdCreateDeviceString(szVendor, szProduct, pszDesc, cbDesc);
214}
215
216static void strDeviceStringMMCSD(device_match_result *pDevResult, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
217{
218 struct cam_device *pDev = cam_open_btl(pDevResult->path_id, pDevResult->target_id,
219 pDevResult->target_lun, O_RDWR, NULL);
220 if (pDev == NULL)
221 {
222 Log(("Error while opening drive device. Error: %s\n", cam_errbuf));
223 return;
224 }
225
226 union ccb *pCcb = cam_getccb(pDev);
227 if (pCcb != NULL)
228 {
229 struct mmc_params mmcIdentData;
230 RT_ZERO(mmcIdentData);
231
232 struct ccb_dev_advinfo *pAdvi = &pCcb->cdai;
233 pAdvi->ccb_h.flags = CAM_DIR_IN;
234 pAdvi->ccb_h.func_code = XPT_DEV_ADVINFO;
235 pAdvi->flags = CDAI_FLAG_NONE;
236 pAdvi->buftype = CDAI_TYPE_MMC_PARAMS;
237 pAdvi->bufsiz = sizeof(mmcIdentData);
238 pAdvi->buf = (uint8_t *)&mmcIdentData;
239
240 if (cam_send_ccb(pDev, pCcb) >= 0)
241 {
242 if (strlen((char *)mmcIdentData.model) > 0)
243 dvdCreateDeviceString("", (const char *)mmcIdentData.model, pszDesc, cbDesc);
244 else
245 dvdCreateDeviceString("", mmcIdentData.card_features & CARD_FEATURE_SDIO ? "SDIO card" : "Unknown card",
246 pszDesc, cbDesc);
247 }
248 else
249 Log(("error sending XPT_DEV_ADVINFO CCB\n"));
250
251 cam_freeccb(pCcb);
252 }
253 else
254 Log(("Could not allocate CCB\n"));
255 cam_close_device(pDev);
256}
257
258/** @returns boolean success indicator (true/false). */
259static int nvmeGetCData(struct cam_device *pDev, struct nvme_controller_data *pCData) RT_NOTHROW_DEF
260{
261 bool fSuccess = false;
262 union ccb *pCcb = cam_getccb(pDev);
263 if (pCcb != NULL)
264 {
265 struct ccb_dev_advinfo *pAdvi = &pCcb->cdai;
266 pAdvi->ccb_h.flags = CAM_DIR_IN;
267 pAdvi->ccb_h.func_code = XPT_DEV_ADVINFO;
268 pAdvi->flags = CDAI_FLAG_NONE;
269 pAdvi->buftype = CDAI_TYPE_NVME_CNTRL;
270 pAdvi->bufsiz = sizeof(struct nvme_controller_data);
271 pAdvi->buf = (uint8_t *)pCData;
272 RT_BZERO(pAdvi->buf, pAdvi->bufsiz);
273
274 if (cam_send_ccb(pDev, pCcb) >= 0)
275 {
276 if (pAdvi->ccb_h.status == CAM_REQ_CMP)
277 fSuccess = true;
278 else
279 Log(("Got CAM error %#x\n", pAdvi->ccb_h.status));
280 }
281 else
282 Log(("Error sending XPT_DEV_ADVINFO CC\n"));
283 cam_freeccb(pCcb);
284 }
285 else
286 Log(("Could not allocate CCB\n"));
287 return fSuccess;
288}
289
290static void strDeviceStringNVME(device_match_result *pDevResult, char *pszDesc, size_t cbDesc) RT_NOTHROW_DEF
291{
292 struct cam_device *pDev = cam_open_btl(pDevResult->path_id, pDevResult->target_id,
293 pDevResult->target_lun, O_RDWR, NULL);
294 if (pDev)
295 {
296 struct nvme_controller_data CData;
297 if (nvmeGetCData(pDev, &CData))
298 {
299 char szVendor[128];
300 cam_strvis((uint8_t *)szVendor, CData.mn, sizeof(CData.mn), sizeof(szVendor));
301 char szProduct[128];
302 cam_strvis((uint8_t *)szProduct, CData.fr, sizeof(CData.fr), sizeof(szProduct));
303 dvdCreateDeviceString(szVendor, szProduct, pszDesc, cbDesc);
304 }
305 else
306 Log(("Error while getting NVME drive info\n"));
307 cam_close_device(pDev);
308 }
309 else
310 Log(("Error while opening drive device. Error: %s\n", cam_errbuf));
311}
312
313
314/**
315 * Search for available drives using the CAM layer.
316 *
317 * @returns iprt status code
318 * @param pList the list to append the drives found to
319 * @param enmDriveType search drives of specified type
320 * @param pfSuccess this will be set to true if we found at least one drive
321 * and to false otherwise. Optional.
322 */
323static int getDriveInfoFromCAM(DriveInfoList *pList, DriveType_T enmDriveType, bool *pfSuccess) RT_NOTHROW_DEF
324{
325 RTFILE hFileXpt = NIL_RTFILE;
326 int rc = RTFileOpen(&hFileXpt, "/dev/xpt0", RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
327 if (RT_SUCCESS(rc))
328 {
329 union ccb DeviceCCB;
330 struct dev_match_pattern DeviceMatchPattern;
331 struct dev_match_result *paMatches = NULL;
332
333 RT_ZERO(DeviceCCB);
334 RT_ZERO(DeviceMatchPattern);
335
336 /* We want to get all devices. */
337 DeviceCCB.ccb_h.func_code = XPT_DEV_MATCH;
338 DeviceCCB.ccb_h.path_id = CAM_XPT_PATH_ID;
339 DeviceCCB.ccb_h.target_id = CAM_TARGET_WILDCARD;
340 DeviceCCB.ccb_h.target_lun = CAM_LUN_WILDCARD;
341
342 /* Setup the pattern */
343 DeviceMatchPattern.type = DEV_MATCH_DEVICE;
344 DeviceMatchPattern.pattern.device_pattern.path_id = CAM_XPT_PATH_ID;
345 DeviceMatchPattern.pattern.device_pattern.target_id = CAM_TARGET_WILDCARD;
346 DeviceMatchPattern.pattern.device_pattern.target_lun = CAM_LUN_WILDCARD;
347 DeviceMatchPattern.pattern.device_pattern.flags = DEV_MATCH_INQUIRY;
348
349#if __FreeBSD_version >= 900000
350# define INQ_PAT data.inq_pat
351#else
352 #define INQ_PAT inq_pat
353#endif
354 DeviceMatchPattern.pattern.device_pattern.INQ_PAT.type = enmDriveType == Fixed ? T_DIRECT
355 : enmDriveType == DVD ? T_CDROM : T_ANY;
356 DeviceMatchPattern.pattern.device_pattern.INQ_PAT.media_type = SIP_MEDIA_REMOVABLE | SIP_MEDIA_FIXED;
357 DeviceMatchPattern.pattern.device_pattern.INQ_PAT.vendor[0] = '*'; /* Matches anything */
358 DeviceMatchPattern.pattern.device_pattern.INQ_PAT.product[0] = '*'; /* Matches anything */
359 DeviceMatchPattern.pattern.device_pattern.INQ_PAT.revision[0] = '*'; /* Matches anything */
360#undef INQ_PAT
361 DeviceCCB.cdm.num_patterns = 1;
362 DeviceCCB.cdm.pattern_buf_len = sizeof(struct dev_match_result);
363 DeviceCCB.cdm.patterns = &DeviceMatchPattern;
364
365 /*
366 * Allocate the buffer holding the matches.
367 * We will allocate for 10 results and call
368 * CAM multiple times if we have more results.
369 */
370 paMatches = (struct dev_match_result *)RTMemAllocZ(10 * sizeof(struct dev_match_result));
371 if (paMatches)
372 {
373 DeviceCCB.cdm.num_matches = 0;
374 DeviceCCB.cdm.match_buf_len = 10 * sizeof(struct dev_match_result);
375 DeviceCCB.cdm.matches = paMatches;
376
377 do
378 {
379 rc = RTFileIoCtl(hFileXpt, CAMIOCOMMAND, &DeviceCCB, sizeof(union ccb), NULL);
380 if (RT_FAILURE(rc))
381 {
382 Log(("Error while querying available CD/DVD devices rc=%Rrc\n", rc));
383 break;
384 }
385
386 for (unsigned i = 0; i < DeviceCCB.cdm.num_matches; i++)
387 {
388 if (paMatches[i].type == DEV_MATCH_DEVICE)
389 {
390 /*
391 * The result list can contain some empty entries with DEV_RESULT_UNCONFIGURED
392 * flag set, e.g. in case of T_DIRECT. Ignore them.
393 */
394 if ( (paMatches[i].result.device_result.flags & DEV_RESULT_UNCONFIGURED)
395 == DEV_RESULT_UNCONFIGURED)
396 continue;
397
398 /* We have the drive now but need the appropriate device node */
399 struct device_match_result *pDevResult = &paMatches[i].result.device_result;
400 union ccb PeriphCCB;
401 struct dev_match_pattern PeriphMatchPattern;
402 struct dev_match_result aPeriphMatches[2];
403 struct periph_match_result *pPeriphResult = NULL;
404 unsigned iPeriphMatch = 0;
405
406 RT_ZERO(PeriphCCB);
407 RT_ZERO(PeriphMatchPattern);
408 RT_ZERO(aPeriphMatches);
409
410 /* This time we only want the specific nodes for the device. */
411 PeriphCCB.ccb_h.func_code = XPT_DEV_MATCH;
412 PeriphCCB.ccb_h.path_id = paMatches[i].result.device_result.path_id;
413 PeriphCCB.ccb_h.target_id = paMatches[i].result.device_result.target_id;
414 PeriphCCB.ccb_h.target_lun = paMatches[i].result.device_result.target_lun;
415
416 /* Setup the pattern */
417 PeriphMatchPattern.type = DEV_MATCH_PERIPH;
418 PeriphMatchPattern.pattern.periph_pattern.path_id = paMatches[i].result.device_result.path_id;
419 PeriphMatchPattern.pattern.periph_pattern.target_id = paMatches[i].result.device_result.target_id;
420 PeriphMatchPattern.pattern.periph_pattern.target_lun = paMatches[i].result.device_result.target_lun;
421 PeriphMatchPattern.pattern.periph_pattern.flags = (periph_pattern_flags)( PERIPH_MATCH_PATH
422 | PERIPH_MATCH_TARGET
423 | PERIPH_MATCH_LUN);
424 PeriphCCB.cdm.num_patterns = 1;
425 PeriphCCB.cdm.pattern_buf_len = sizeof(struct dev_match_result);
426 PeriphCCB.cdm.patterns = &PeriphMatchPattern;
427 PeriphCCB.cdm.num_matches = 0;
428 PeriphCCB.cdm.match_buf_len = sizeof(aPeriphMatches);
429 PeriphCCB.cdm.matches = aPeriphMatches;
430
431 do
432 {
433 rc = RTFileIoCtl(hFileXpt, CAMIOCOMMAND, &PeriphCCB, sizeof(union ccb), NULL);
434 if (RT_FAILURE(rc))
435 {
436 Log(("Error while querying available periph devices rc=%Rrc\n", rc));
437 break;
438 }
439
440 for (iPeriphMatch = 0; iPeriphMatch < PeriphCCB.cdm.num_matches; iPeriphMatch++)
441 {
442 /* Ignore "passthrough mode" paths */
443 if ( aPeriphMatches[iPeriphMatch].type == DEV_MATCH_PERIPH
444 && strcmp(aPeriphMatches[iPeriphMatch].result.periph_result.periph_name, "pass"))
445 {
446 pPeriphResult = &aPeriphMatches[iPeriphMatch].result.periph_result;
447 break; /* We found the periph device */
448 }
449 }
450
451 if (iPeriphMatch < PeriphCCB.cdm.num_matches)
452 break;
453
454 } while ( DeviceCCB.ccb_h.status == CAM_REQ_CMP
455 && DeviceCCB.cdm.status == CAM_DEV_MATCH_MORE);
456
457 if (pPeriphResult)
458 {
459 char szPath[RTPATH_MAX];
460 RTStrPrintf(szPath, sizeof(szPath), "/dev/%s%d",
461 pPeriphResult->periph_name, pPeriphResult->unit_number);
462
463 char szDesc[256] = { 0 };
464 switch (pDevResult->protocol)
465 {
466 case PROTO_SCSI: strDeviceStringSCSI( pDevResult, szDesc, sizeof(szDesc)); break;
467 case PROTO_ATA: strDeviceStringATA( pDevResult, szDesc, sizeof(szDesc)); break;
468 case PROTO_MMCSD: strDeviceStringMMCSD(pDevResult, szDesc, sizeof(szDesc)); break;
469 case PROTO_SEMB: strDeviceStringSEMB( pDevResult, szDesc, sizeof(szDesc)); break;
470 case PROTO_NVME: strDeviceStringNVME( pDevResult, szDesc, sizeof(szDesc)); break;
471 default: break;
472 }
473
474 try
475 {
476 pList->push_back(DriveInfo(szPath, "", szDesc));
477 }
478 catch (std::bad_alloc &)
479 {
480 pList->clear();
481 rc = VERR_NO_MEMORY;
482 break;
483 }
484 if (pfSuccess)
485 *pfSuccess = true;
486 }
487 }
488 }
489 } while ( DeviceCCB.ccb_h.status == CAM_REQ_CMP
490 && DeviceCCB.cdm.status == CAM_DEV_MATCH_MORE
491 && RT_SUCCESS(rc));
492
493 RTMemFree(paMatches);
494 }
495 else
496 rc = VERR_NO_MEMORY;
497
498 RTFileClose(hFileXpt);
499 }
500
501 return rc;
502}
503
504
505/**
506 * Extract the names of drives from an environment variable and add them to a
507 * list if they are valid.
508 *
509 * @returns iprt status code
510 * @param pcszVar the name of the environment variable. The variable
511 * value should be a list of device node names, separated
512 * by ':' characters.
513 * @param pList the list to append the drives found to
514 * @param isDVD are we looking for DVD drives or for floppies?
515 * @param pfSuccess this will be set to true if we found at least one drive
516 * and to false otherwise. Optional.
517 *
518 * @note This is duplicated in HostHardwareLinux.cpp.
519 */
520static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_DEF
521{
522 AssertPtrReturn(pcszVar, VERR_INVALID_POINTER);
523 AssertPtrReturn(pList, VERR_INVALID_POINTER);
524 AssertPtrNullReturn(pfSuccess, VERR_INVALID_POINTER);
525 LogFlowFunc(("pcszVar=%s, pList=%p, isDVD=%d, pfSuccess=%p\n", pcszVar, pList, isDVD, pfSuccess));
526 int rc = VINF_SUCCESS;
527 bool success = false;
528 char *pszFreeMe = RTEnvDupEx(RTENV_DEFAULT, pcszVar);
529
530 try
531 {
532 char *pszCurrent = pszFreeMe;
533 while (pszCurrent && *pszCurrent != '\0')
534 {
535 char *pszNext = strchr(pszCurrent, ':');
536 if (pszNext)
537 *pszNext++ = '\0';
538
539 char szReal[RTPATH_MAX];
540 char szDesc[1] = "", szUdi[1] = ""; /* differs on freebsd because no devValidateDevice */
541 if ( RT_SUCCESS(RTPathReal(pszCurrent, szReal, sizeof(szReal)))
542 /*&& devValidateDevice(szReal, isDVD, NULL, szDesc, sizeof(szDesc), szUdi, sizeof(szUdi)) - linux only */)
543 {
544 pList->push_back(DriveInfo(szReal, szUdi, szDesc));
545 success = true;
546 }
547 pszCurrent = pszNext;
548 }
549 if (pfSuccess != NULL)
550 *pfSuccess = success;
551 }
552 catch (std::bad_alloc &)
553 {
554 rc = VERR_NO_MEMORY;
555 }
556 RTStrFree(pszFreeMe);
557 LogFlowFunc(("rc=%Rrc, success=%d\n", rc, success));
558 return rc;
559}
560
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