VirtualBox

source: vbox/trunk/src/VBox/Main/VFSExplorerImpl.cpp@ 28800

Last change on this file since 28800 was 28800, checked in by vboxsync, 14 years ago

Automated rebranding to Oracle copyright/license strings via filemuncher

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.4 KB
Line 
1/* $Id: VFSExplorerImpl.cpp 28800 2010-04-27 08:22:32Z vboxsync $ */
2/** @file
3 *
4 * IVFSExplorer COM class implementations.
5 */
6
7/*
8 * Copyright (C) 2009 Oracle Corporation
9 *
10 * This file is part of VirtualBox Open Source Edition (OSE), as
11 * available from http://www.virtualbox.org. This file is free software;
12 * you can redistribute it and/or modify it under the terms of the GNU
13 * General Public License (GPL) as published by the Free Software
14 * Foundation, in version 2 as it comes in the "COPYING" file of the
15 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
16 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
17 */
18
19#include <iprt/dir.h>
20#include <iprt/path.h>
21#include <iprt/file.h>
22#include <iprt/s3.h>
23
24#include <VBox/com/array.h>
25
26#include <VBox/param.h>
27#include <VBox/version.h>
28
29#include "VFSExplorerImpl.h"
30#include "VirtualBoxImpl.h"
31#include "ProgressImpl.h"
32
33#include "AutoCaller.h"
34#include "Logging.h"
35
36////////////////////////////////////////////////////////////////////////////////
37//
38// VFSExplorer definitions
39//
40////////////////////////////////////////////////////////////////////////////////
41
42/* opaque private instance data of VFSExplorer class */
43struct VFSExplorer::Data
44{
45 struct DirEntry
46 {
47 DirEntry(Utf8Str aName, VFSFileType_T aType)
48 : name(aName)
49 , type(aType) {}
50
51 Utf8Str name;
52 VFSFileType_T type;
53 };
54
55 VFSType_T storageType;
56 Utf8Str strUsername;
57 Utf8Str strPassword;
58 Utf8Str strHostname;
59 Utf8Str strPath;
60 Utf8Str strBucket;
61
62 std::list<DirEntry> entryList;
63};
64
65VFSExplorer::VFSExplorer()
66 : mVirtualBox(NULL)
67{
68}
69
70VFSExplorer::~VFSExplorer()
71{
72}
73
74
75/**
76 * VFSExplorer COM initializer.
77 * @param
78 * @return
79 */
80HRESULT VFSExplorer::init(VFSType_T aType, Utf8Str aFilePath, Utf8Str aHostname, Utf8Str aUsername, Utf8Str aPassword, VirtualBox *aVirtualBox)
81{
82 /* Enclose the state transition NotReady->InInit->Ready */
83 AutoInitSpan autoInitSpan(this);
84 AssertReturn(autoInitSpan.isOk(), E_FAIL);
85
86 /* Weak reference to a VirtualBox object */
87 unconst(mVirtualBox) = aVirtualBox;
88
89 /* initialize data */
90 m = new Data;
91
92 m->storageType = aType;
93 m->strPath = aFilePath;
94 m->strHostname = aHostname;
95 m->strUsername = aUsername;
96 m->strPassword = aPassword;
97
98 if (m->storageType == VFSType_S3)
99 {
100 size_t bpos = aFilePath.find("/", 1);
101 if (bpos != Utf8Str::npos)
102 {
103 m->strBucket = aFilePath.substr(1, bpos - 1); /* The bucket without any slashes */
104 aFilePath = aFilePath.substr(bpos); /* The rest of the file path */
105 }
106 }
107
108 /* Confirm a successful initialization */
109 autoInitSpan.setSucceeded();
110
111 return S_OK;
112}
113
114/**
115 * VFSExplorer COM uninitializer.
116 * @return
117 */
118void VFSExplorer::uninit()
119{
120 delete m;
121 m = NULL;
122}
123
124/**
125 * Public method implementation.
126 * @param
127 * @return
128 */
129STDMETHODIMP VFSExplorer::COMGETTER(Path)(BSTR *aPath)
130{
131 if (!aPath)
132 return E_POINTER;
133
134 AutoCaller autoCaller(this);
135 if (FAILED(autoCaller.rc())) return autoCaller.rc();
136
137 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
138
139 Bstr bstrPath(m->strPath);
140 bstrPath.cloneTo(aPath);
141
142 return S_OK;
143}
144
145STDMETHODIMP VFSExplorer::COMGETTER(Type)(VFSType_T *aType)
146{
147 if (!aType)
148 return E_POINTER;
149
150 AutoCaller autoCaller(this);
151 if (FAILED(autoCaller.rc())) return autoCaller.rc();
152
153 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
154
155 *aType = m->storageType;
156
157 return S_OK;
158}
159
160struct VFSExplorer::TaskVFSExplorer
161{
162 enum TaskType
163 {
164 Update,
165 Delete
166 };
167
168 TaskVFSExplorer(TaskType aTaskType, VFSExplorer *aThat, Progress *aProgress)
169 : taskType(aTaskType),
170 pVFSExplorer(aThat),
171 progress(aProgress),
172 rc(S_OK)
173 {}
174 ~TaskVFSExplorer() {}
175
176 int startThread();
177 static int taskThread(RTTHREAD aThread, void *pvUser);
178 static int uploadProgress(unsigned uPercent, void *pvUser);
179
180 TaskType taskType;
181 VFSExplorer *pVFSExplorer;
182 ComObjPtr<Progress> progress;
183 HRESULT rc;
184
185 /* task data */
186 std::list<Utf8Str> filenames;
187};
188
189int VFSExplorer::TaskVFSExplorer::startThread()
190{
191 int vrc = RTThreadCreate(NULL, VFSExplorer::TaskVFSExplorer::taskThread, this,
192 0, RTTHREADTYPE_MAIN_HEAVY_WORKER, 0,
193 "Explorer::Task");
194
195 ComAssertMsgRCRet(vrc,
196 ("Could not create taskThreadVFS (%Rrc)\n", vrc), E_FAIL);
197
198 return vrc;
199}
200
201/* static */
202DECLCALLBACK(int) VFSExplorer::TaskVFSExplorer::taskThread(RTTHREAD /* aThread */, void *pvUser)
203{
204 std::auto_ptr<TaskVFSExplorer> task(static_cast<TaskVFSExplorer*>(pvUser));
205 AssertReturn(task.get(), VERR_GENERAL_FAILURE);
206
207 VFSExplorer *pVFSExplorer = task->pVFSExplorer;
208
209 LogFlowFuncEnter();
210 LogFlowFunc(("VFSExplorer %p\n", pVFSExplorer));
211
212 HRESULT rc = S_OK;
213
214 switch(task->taskType)
215 {
216 case TaskVFSExplorer::Update:
217 {
218 if (pVFSExplorer->m->storageType == VFSType_File)
219 rc = pVFSExplorer->updateFS(task.get());
220 else if (pVFSExplorer->m->storageType == VFSType_S3)
221 rc = pVFSExplorer->updateS3(task.get());
222 break;
223 }
224 case TaskVFSExplorer::Delete:
225 {
226 if (pVFSExplorer->m->storageType == VFSType_File)
227 rc = pVFSExplorer->deleteFS(task.get());
228 else if (pVFSExplorer->m->storageType == VFSType_S3)
229 rc = pVFSExplorer->deleteS3(task.get());
230 break;
231 }
232 }
233
234 LogFlowFunc(("rc=%Rhrc\n", rc));
235 LogFlowFuncLeave();
236
237 return VINF_SUCCESS;
238}
239
240/* static */
241int VFSExplorer::TaskVFSExplorer::uploadProgress(unsigned uPercent, void *pvUser)
242{
243 VFSExplorer::TaskVFSExplorer* pTask = *(VFSExplorer::TaskVFSExplorer**)pvUser;
244
245 if (pTask &&
246 !pTask->progress.isNull())
247 {
248 BOOL fCanceled;
249 pTask->progress->COMGETTER(Canceled)(&fCanceled);
250 if (fCanceled)
251 return -1;
252 pTask->progress->SetCurrentOperationProgress(uPercent);
253 }
254 return VINF_SUCCESS;
255}
256
257VFSFileType_T VFSExplorer::RTToVFSFileType(int aType) const
258{
259 VFSFileType_T t;
260 switch(aType)
261 {
262 default:
263 case RTDIRENTRYTYPE_UNKNOWN: t = VFSFileType_Unknown; break;
264 case RTDIRENTRYTYPE_FIFO: t = VFSFileType_Fifo; break;
265 case RTDIRENTRYTYPE_DEV_CHAR: t = VFSFileType_DevChar; break;
266 case RTDIRENTRYTYPE_DIRECTORY: t = VFSFileType_Directory; break;
267 case RTDIRENTRYTYPE_DEV_BLOCK: t = VFSFileType_DevBlock; break;
268 case RTDIRENTRYTYPE_FILE: t = VFSFileType_File; break;
269 case RTDIRENTRYTYPE_SYMLINK: t = VFSFileType_SymLink; break;
270 case RTDIRENTRYTYPE_SOCKET: t = VFSFileType_Socket; break;
271 case RTDIRENTRYTYPE_WHITEOUT: t = VFSFileType_WhiteOut; break;
272 }
273 return t;
274}
275
276HRESULT VFSExplorer::updateFS(TaskVFSExplorer *aTask)
277{
278 LogFlowFuncEnter();
279
280 AutoCaller autoCaller(this);
281 if (FAILED(autoCaller.rc())) return autoCaller.rc();
282
283 AutoWriteLock appLock(this COMMA_LOCKVAL_SRC_POS);
284
285 HRESULT rc = S_OK;
286
287 std::list<VFSExplorer::Data::DirEntry> fileList;
288 char *pszPath = NULL;
289 PRTDIR pDir = NULL;
290 try
291 {
292 pszPath = RTStrDup(m->strPath.c_str());
293 RTPathStripFilename(pszPath);
294 int vrc = RTDirOpen(&pDir, pszPath);
295 if (RT_FAILURE(vrc))
296 throw setError(VBOX_E_FILE_ERROR, tr ("Can't open directory '%s' (%Rrc)"), pszPath, vrc);
297
298 if (aTask->progress)
299 aTask->progress->SetCurrentOperationProgress(33);
300 RTDIRENTRY entry;
301 while (RT_SUCCESS(vrc))
302 {
303 vrc = RTDirRead(pDir, &entry, NULL);
304 if (RT_SUCCESS(vrc))
305 {
306 Utf8Str name(entry.szName);
307 if (name != "." &&
308 name != "..")
309 fileList.push_back(VFSExplorer::Data::DirEntry(name, RTToVFSFileType(entry.enmType)));
310 }
311 }
312 if (aTask->progress)
313 aTask->progress->SetCurrentOperationProgress(66);
314 }
315 catch(HRESULT aRC)
316 {
317 rc = aRC;
318 }
319
320 /* Clean up */
321 if (pszPath)
322 RTStrFree(pszPath);
323 if (pDir)
324 RTDirClose(pDir);
325
326 if (aTask->progress)
327 aTask->progress->SetCurrentOperationProgress(99);
328
329 /* Assign the result on success (this clears the old list) */
330 if (rc == S_OK)
331 m->entryList.assign(fileList.begin(), fileList.end());
332
333 aTask->rc = rc;
334
335 if (!aTask->progress.isNull())
336 aTask->progress->notifyComplete(rc);
337
338 LogFlowFunc(("rc=%Rhrc\n", rc));
339 LogFlowFuncLeave();
340
341 return VINF_SUCCESS;
342}
343
344HRESULT VFSExplorer::deleteFS(TaskVFSExplorer *aTask)
345{
346 LogFlowFuncEnter();
347
348 AutoCaller autoCaller(this);
349 if (FAILED(autoCaller.rc())) return autoCaller.rc();
350
351 AutoWriteLock appLock(this COMMA_LOCKVAL_SRC_POS);
352
353 HRESULT rc = S_OK;
354
355 float fPercentStep = 100.0f / aTask->filenames.size();
356 try
357 {
358 char szPath[RTPATH_MAX];
359 std::list<Utf8Str>::const_iterator it;
360 size_t i = 0;
361 for (it = aTask->filenames.begin();
362 it != aTask->filenames.end();
363 ++it, ++i)
364 {
365 memcpy(szPath, m->strPath.c_str(), strlen(m->strPath.c_str()) + 1);
366 RTPathStripFilename(szPath);
367 RTPathAppend(szPath, sizeof(szPath), (*it).c_str());
368 int vrc = RTFileDelete(szPath);
369 if (RT_FAILURE(vrc))
370 throw setError(VBOX_E_FILE_ERROR, tr ("Can't delete file '%s' (%Rrc)"), szPath, vrc);
371 if (aTask->progress)
372 aTask->progress->SetCurrentOperationProgress((ULONG)(fPercentStep * i));
373 }
374 }
375 catch(HRESULT aRC)
376 {
377 rc = aRC;
378 }
379
380 aTask->rc = rc;
381
382 if (!aTask->progress.isNull())
383 aTask->progress->notifyComplete(rc);
384
385 LogFlowFunc(("rc=%Rhrc\n", rc));
386 LogFlowFuncLeave();
387
388 return VINF_SUCCESS;
389}
390
391HRESULT VFSExplorer::updateS3(TaskVFSExplorer *aTask)
392{
393 LogFlowFuncEnter();
394
395 AutoCaller autoCaller(this);
396 if (FAILED(autoCaller.rc())) return autoCaller.rc();
397
398 AutoWriteLock appLock(this COMMA_LOCKVAL_SRC_POS);
399
400 HRESULT rc = S_OK;
401
402 RTS3 hS3 = NULL;
403 std::list<VFSExplorer::Data::DirEntry> fileList;
404 try
405 {
406 int vrc = RTS3Create(&hS3, m->strUsername.c_str(), m->strPassword.c_str(), m->strHostname.c_str(), "virtualbox-agent/"VBOX_VERSION_STRING);
407 if (RT_FAILURE(vrc))
408 throw setError(E_FAIL, tr ("Can't open S3 storage service (%Rrc)"), vrc);
409
410 RTS3SetProgressCallback(hS3, VFSExplorer::TaskVFSExplorer::uploadProgress, &aTask);
411 /* Do we need the list of buckets or keys? */
412 if (m->strBucket.isEmpty())
413 {
414 PCRTS3BUCKETENTRY pBuckets = NULL;
415 vrc = RTS3GetBuckets(hS3, &pBuckets);
416 if (RT_FAILURE(vrc))
417 throw setError(E_FAIL, tr ("Can't get buckets (%Rrc)"), vrc);
418
419 PCRTS3BUCKETENTRY pTmpBuckets = pBuckets;
420 while (pBuckets)
421 {
422 fileList.push_back(VFSExplorer::Data::DirEntry(pBuckets->pszName, VFSFileType_Directory));
423 pBuckets = pBuckets->pNext;
424 }
425 RTS3BucketsDestroy(pTmpBuckets);
426 }
427 else
428 {
429 PCRTS3KEYENTRY pKeys = NULL;
430 vrc = RTS3GetBucketKeys(hS3, m->strBucket.c_str(), &pKeys);
431 if (RT_FAILURE(vrc))
432 throw setError(E_FAIL, tr ("Can't get keys for bucket (%Rrc)"), vrc);
433
434 PCRTS3KEYENTRY pTmpKeys = pKeys;
435 while (pKeys)
436 {
437 Utf8Str name(pKeys->pszName);
438 fileList.push_back(VFSExplorer::Data::DirEntry(pKeys->pszName, VFSFileType_File));
439 pKeys = pKeys->pNext;
440 }
441 RTS3KeysDestroy(pTmpKeys);
442 }
443 }
444 catch(HRESULT aRC)
445 {
446 rc = aRC;
447 }
448
449 if (hS3 != NULL)
450 RTS3Destroy(hS3);
451
452 /* Assign the result on success (this clears the old list) */
453 if (rc == S_OK)
454 m->entryList.assign(fileList.begin(), fileList.end());
455
456 aTask->rc = rc;
457
458 if (!aTask->progress.isNull())
459 aTask->progress->notifyComplete(rc);
460
461 LogFlowFunc(("rc=%Rhrc\n", rc));
462 LogFlowFuncLeave();
463
464 return VINF_SUCCESS;
465}
466
467HRESULT VFSExplorer::deleteS3(TaskVFSExplorer *aTask)
468{
469 LogFlowFuncEnter();
470
471 AutoCaller autoCaller(this);
472 if (FAILED(autoCaller.rc())) return autoCaller.rc();
473
474 AutoWriteLock appLock(this COMMA_LOCKVAL_SRC_POS);
475
476 HRESULT rc = S_OK;
477
478 RTS3 hS3 = NULL;
479 float fPercentStep = 100.0f / aTask->filenames.size();
480 try
481 {
482 int vrc = RTS3Create(&hS3, m->strUsername.c_str(), m->strPassword.c_str(), m->strHostname.c_str(), "virtualbox-agent/"VBOX_VERSION_STRING);
483 if (RT_FAILURE(vrc))
484 throw setError(E_FAIL, tr ("Can't open S3 storage service (%Rrc)"), vrc);
485
486 RTS3SetProgressCallback(hS3, VFSExplorer::TaskVFSExplorer::uploadProgress, &aTask);
487
488 std::list<Utf8Str>::const_iterator it;
489 size_t i = 0;
490 for (it = aTask->filenames.begin();
491 it != aTask->filenames.end();
492 ++it, ++i)
493 {
494 vrc = RTS3DeleteKey(hS3, m->strBucket.c_str(), (*it).c_str());
495 if (RT_FAILURE(vrc))
496 throw setError(VBOX_E_FILE_ERROR, tr ("Can't delete file '%s' (%Rrc)"), (*it).c_str(), vrc);
497 if (aTask->progress)
498 aTask->progress->SetCurrentOperationProgress((ULONG)(fPercentStep * i));
499 }
500 }
501 catch(HRESULT aRC)
502 {
503 rc = aRC;
504 }
505
506 aTask->rc = rc;
507
508 if (hS3 != NULL)
509 RTS3Destroy(hS3);
510
511 if (!aTask->progress.isNull())
512 aTask->progress->notifyComplete(rc);
513
514 LogFlowFunc(("rc=%Rhrc\n", rc));
515 LogFlowFuncLeave();
516
517 return VINF_SUCCESS;
518}
519
520STDMETHODIMP VFSExplorer::Update(IProgress **aProgress)
521{
522 CheckComArgOutPointerValid(aProgress);
523
524 AutoCaller autoCaller(this);
525 if (FAILED(autoCaller.rc())) return autoCaller.rc();
526
527 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
528
529 HRESULT rc = S_OK;
530
531 ComObjPtr<Progress> progress;
532 try
533 {
534 Bstr progressDesc = BstrFmt(tr("Update directory info for '%s'"),
535 m->strPath.raw());
536 /* Create the progress object */
537 progress.createObject();
538
539 rc = progress->init(mVirtualBox, static_cast<IVFSExplorer*>(this),
540 progressDesc,
541 TRUE /* aCancelable */);
542 if (FAILED(rc)) throw rc;
543
544 /* Initialize our worker task */
545 std::auto_ptr<TaskVFSExplorer> task(new TaskVFSExplorer(TaskVFSExplorer::Update, this, progress));
546
547 rc = task->startThread();
548 if (FAILED(rc)) throw rc;
549
550 /* Don't destruct on success */
551 task.release();
552 }
553 catch (HRESULT aRC)
554 {
555 rc = aRC;
556 }
557
558 if (SUCCEEDED(rc))
559 /* Return progress to the caller */
560 progress.queryInterfaceTo(aProgress);
561
562 return rc;
563}
564
565STDMETHODIMP VFSExplorer::Cd(IN_BSTR aDir, IProgress **aProgress)
566{
567 CheckComArgStrNotEmptyOrNull(aDir);
568 CheckComArgOutPointerValid(aProgress);
569
570 return E_NOTIMPL;
571}
572
573STDMETHODIMP VFSExplorer::CdUp(IProgress **aProgress)
574{
575 CheckComArgOutPointerValid(aProgress);
576
577 return E_NOTIMPL;
578}
579
580STDMETHODIMP VFSExplorer::EntryList(ComSafeArrayOut(BSTR, aNames), ComSafeArrayOut(VFSFileType_T, aTypes))
581{
582 if (ComSafeArrayOutIsNull(aNames) ||
583 ComSafeArrayOutIsNull(aTypes))
584 return E_POINTER;
585
586 AutoCaller autoCaller(this);
587 if (FAILED(autoCaller.rc())) return autoCaller.rc();
588
589 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
590
591 com::SafeArray<BSTR> sfaNames((ULONG)m->entryList.size());
592 com::SafeArray<ULONG> sfaTypes((VFSFileType_T)m->entryList.size());
593
594 std::list<VFSExplorer::Data::DirEntry>::const_iterator it;
595 size_t i = 0;
596 for (it = m->entryList.begin();
597 it != m->entryList.end();
598 ++it, ++i)
599 {
600 const VFSExplorer::Data::DirEntry &entry = (*it);
601 Bstr bstr(entry.name);
602 bstr.cloneTo(&sfaNames[i]);
603 sfaTypes[i] = entry.type;
604 }
605
606 sfaNames.detachTo(ComSafeArrayOutArg(aNames));
607 sfaTypes.detachTo(ComSafeArrayOutArg(aTypes));
608
609 return S_OK;
610}
611
612STDMETHODIMP VFSExplorer::Exists(ComSafeArrayIn(IN_BSTR, aNames), ComSafeArrayOut(BSTR, aExists))
613{
614 CheckComArgSafeArrayNotNull(aNames);
615
616 AutoCaller autoCaller(this);
617 if (FAILED(autoCaller.rc())) return autoCaller.rc();
618
619 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
620
621 com::SafeArray<IN_BSTR> sfaNames(ComSafeArrayInArg(aNames));
622 std::list<BSTR> listExists;
623
624 std::list<VFSExplorer::Data::DirEntry>::const_iterator it;
625 for (it = m->entryList.begin();
626 it != m->entryList.end();
627 ++it)
628 {
629 const VFSExplorer::Data::DirEntry &entry = (*it);
630 for (size_t a=0; a < sfaNames.size(); ++a)
631 {
632 if (entry.name == RTPathFilename(Utf8Str(sfaNames[a]).c_str()))
633 {
634 BSTR name;
635 Bstr tmp(sfaNames[a]); /* gcc-3.3 cruft */
636 tmp.cloneTo(&name);
637 listExists.push_back(name);
638 }
639 }
640 }
641
642 com::SafeArray<BSTR> sfaExists(listExists);
643 sfaExists.detachTo(ComSafeArrayOutArg(aExists));
644
645 return S_OK;
646}
647
648STDMETHODIMP VFSExplorer::Remove(ComSafeArrayIn(IN_BSTR, aNames), IProgress **aProgress)
649{
650 CheckComArgSafeArrayNotNull(aNames);
651 CheckComArgOutPointerValid(aProgress);
652
653 AutoCaller autoCaller(this);
654 if (FAILED(autoCaller.rc())) return autoCaller.rc();
655
656 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
657
658 HRESULT rc = S_OK;
659
660 com::SafeArray<IN_BSTR> sfaNames(ComSafeArrayInArg(aNames));
661
662 ComObjPtr<Progress> progress;
663 try
664 {
665 /* Create the progress object */
666 progress.createObject();
667
668 rc = progress->init(mVirtualBox, static_cast<IVFSExplorer*>(this),
669 Bstr(tr("Delete files")),
670 TRUE /* aCancelable */);
671 if (FAILED(rc)) throw rc;
672
673 /* Initialize our worker task */
674 std::auto_ptr<TaskVFSExplorer> task(new TaskVFSExplorer(TaskVFSExplorer::Delete, this, progress));
675
676 /* Add all filenames to delete as task data */
677 for (size_t a=0; a < sfaNames.size(); ++a)
678 task->filenames.push_back(Utf8Str(sfaNames[a]));
679
680 rc = task->startThread();
681 if (FAILED(rc)) throw rc;
682
683 /* Don't destruct on success */
684 task.release();
685 }
686 catch (HRESULT aRC)
687 {
688 rc = aRC;
689 }
690
691 if (SUCCEEDED(rc))
692 /* Return progress to the caller */
693 progress.queryInterfaceTo(aProgress);
694
695 return rc;
696}
697
Note: See TracBrowser for help on using the repository browser.

© 2023 Oracle
ContactPrivacy policyTerms of Use