VirtualBox

source: vbox/trunk/src/VBox/Main/src-client/EmulatedUSBImpl.cpp

Last change on this file was 98278, checked in by vboxsync, 16 months ago

Main/src-client: Some more rc -> hrc/vrc stuff found by grep. A build fix. bugref:10223

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.3 KB
Line 
1/* $Id: EmulatedUSBImpl.cpp 98278 2023-01-24 11:55:00Z vboxsync $ */
2/** @file
3 * Emulated USB manager implementation.
4 */
5
6/*
7 * Copyright (C) 2013-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#define LOG_GROUP LOG_GROUP_MAIN_EMULATEDUSB
29#include "LoggingNew.h"
30
31#include "EmulatedUSBImpl.h"
32#include "ConsoleImpl.h"
33
34#include <VBox/vmm/pdmusb.h>
35#include <VBox/vmm/vmmr3vtable.h>
36
37
38/*
39 * Emulated USB webcam device instance.
40 */
41typedef std::map <Utf8Str, Utf8Str> EUSBSettingsMap;
42
43typedef enum EUSBDEVICESTATUS
44{
45 EUSBDEVICE_CREATED,
46 EUSBDEVICE_ATTACHING,
47 EUSBDEVICE_ATTACHED
48} EUSBDEVICESTATUS;
49
50class EUSBWEBCAM /* : public EUSBDEVICE */
51{
52private:
53 int32_t volatile mcRefs;
54
55 EmulatedUSB *mpEmulatedUSB;
56
57 RTUUID mUuid;
58 char mszUuid[RTUUID_STR_LENGTH];
59
60 Utf8Str mPath;
61 Utf8Str mSettings;
62
63 EUSBSettingsMap mDevSettings;
64 EUSBSettingsMap mDrvSettings;
65
66 void *mpvObject;
67
68 static DECLCALLBACK(int) emulatedWebcamAttach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis, const char *pszDriver);
69 static DECLCALLBACK(int) emulatedWebcamDetach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis);
70
71 HRESULT settingsParse(void);
72
73 ~EUSBWEBCAM()
74 {
75 }
76
77public:
78 EUSBWEBCAM()
79 :
80 mcRefs(1),
81 mpEmulatedUSB(NULL),
82 mpvObject(NULL),
83 enmStatus(EUSBDEVICE_CREATED)
84 {
85 RT_ZERO(mUuid);
86 RT_ZERO(mszUuid);
87 }
88
89 int32_t AddRef(void)
90 {
91 return ASMAtomicIncS32(&mcRefs);
92 }
93
94 void Release(void)
95 {
96 int32_t c = ASMAtomicDecS32(&mcRefs);
97 if (c == 0)
98 {
99 delete this;
100 }
101 }
102
103 HRESULT Initialize(Console *pConsole,
104 EmulatedUSB *pEmulatedUSB,
105 const com::Utf8Str *aPath,
106 const com::Utf8Str *aSettings,
107 void *pvObject);
108 HRESULT Attach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM, const char *pszDriver);
109 HRESULT Detach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM);
110
111 bool HasId(const char *pszId) { return RTStrCmp(pszId, mszUuid) == 0;}
112
113 void *getObjectPtr() { return mpvObject; }
114
115 EUSBDEVICESTATUS enmStatus;
116};
117
118
119static int emulatedWebcamInsertSettings(PCFGMNODE pConfig, PCVMMR3VTABLE pVMM, EUSBSettingsMap *pSettings)
120{
121 for (EUSBSettingsMap::const_iterator it = pSettings->begin(); it != pSettings->end(); ++it)
122 {
123 /* Convert some well known settings for backward compatibility. */
124 int vrc;
125 if ( RTStrCmp(it->first.c_str(), "MaxPayloadTransferSize") == 0
126 || RTStrCmp(it->first.c_str(), "MaxFramerate") == 0)
127 {
128 uint32_t u32 = 0;
129 vrc = RTStrToUInt32Full(it->second.c_str(), 10, &u32);
130 if (vrc == VINF_SUCCESS)
131 vrc = pVMM->pfnCFGMR3InsertInteger(pConfig, it->first.c_str(), u32);
132 else if (RT_SUCCESS(vrc)) /* VWRN_* */
133 vrc = VERR_INVALID_PARAMETER;
134 }
135 else
136 vrc = pVMM->pfnCFGMR3InsertString(pConfig, it->first.c_str(), it->second.c_str());
137 if (RT_FAILURE(vrc))
138 return vrc;
139 }
140
141 return VINF_SUCCESS;
142}
143
144/*static*/ DECLCALLBACK(int)
145EUSBWEBCAM::emulatedWebcamAttach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis, const char *pszDriver)
146{
147 PCFGMNODE pInstance = pVMM->pfnCFGMR3CreateTree(pUVM);
148 PCFGMNODE pConfig;
149 int vrc = pVMM->pfnCFGMR3InsertNode(pInstance, "Config", &pConfig);
150 AssertRCReturn(vrc, vrc);
151 vrc = emulatedWebcamInsertSettings(pConfig, pVMM, &pThis->mDevSettings);
152 AssertRCReturn(vrc, vrc);
153
154 PCFGMNODE pEUSB;
155 vrc = pVMM->pfnCFGMR3InsertNode(pConfig, "EmulatedUSB", &pEUSB);
156 AssertRCReturn(vrc, vrc);
157 vrc = pVMM->pfnCFGMR3InsertString(pEUSB, "Id", pThis->mszUuid);
158 AssertRCReturn(vrc, vrc);
159
160 PCFGMNODE pLunL0;
161 vrc = pVMM->pfnCFGMR3InsertNode(pInstance, "LUN#0", &pLunL0);
162 AssertRCReturn(vrc, vrc);
163 vrc = pVMM->pfnCFGMR3InsertString(pLunL0, "Driver", pszDriver);
164 AssertRCReturn(vrc, vrc);
165 vrc = pVMM->pfnCFGMR3InsertNode(pLunL0, "Config", &pConfig);
166 AssertRCReturn(vrc, vrc);
167 vrc = pVMM->pfnCFGMR3InsertString(pConfig, "DevicePath", pThis->mPath.c_str());
168 AssertRCReturn(vrc, vrc);
169 vrc = pVMM->pfnCFGMR3InsertString(pConfig, "Id", pThis->mszUuid);
170 AssertRCReturn(vrc, vrc);
171 vrc = emulatedWebcamInsertSettings(pConfig, pVMM, &pThis->mDrvSettings);
172 AssertRCReturn(vrc, vrc);
173
174 /* pInstance will be used by PDM and deallocated on error. */
175 vrc = pVMM->pfnPDMR3UsbCreateEmulatedDevice(pUVM, "Webcam", pInstance, &pThis->mUuid, NULL);
176 LogRelFlowFunc(("PDMR3UsbCreateEmulatedDevice %Rrc\n", vrc));
177 return vrc;
178}
179
180/*static*/ DECLCALLBACK(int)
181EUSBWEBCAM::emulatedWebcamDetach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis)
182{
183 return pVMM->pfnPDMR3UsbDetachDevice(pUVM, &pThis->mUuid);
184}
185
186HRESULT EUSBWEBCAM::Initialize(Console *pConsole,
187 EmulatedUSB *pEmulatedUSB,
188 const com::Utf8Str *aPath,
189 const com::Utf8Str *aSettings,
190 void *pvObject)
191{
192 HRESULT hrc = S_OK;
193
194 int vrc = RTUuidCreate(&mUuid);
195 AssertRCReturn(vrc, pConsole->setError(vrc, EmulatedUSB::tr("Init emulated USB webcam (RTUuidCreate -> %Rrc)"), vrc));
196
197 RTStrPrintf(mszUuid, sizeof(mszUuid), "%RTuuid", &mUuid);
198 hrc = mPath.assignEx(*aPath);
199 if (SUCCEEDED(hrc))
200 {
201 hrc = mSettings.assignEx(*aSettings);
202 if (SUCCEEDED(hrc))
203 {
204 hrc = settingsParse();
205 if (SUCCEEDED(hrc))
206 {
207 mpEmulatedUSB = pEmulatedUSB;
208 mpvObject = pvObject;
209 }
210 }
211 }
212
213 return hrc;
214}
215
216HRESULT EUSBWEBCAM::settingsParse(void)
217{
218 HRESULT hr = S_OK;
219
220 /* Parse mSettings string:
221 * "[dev:|drv:]Name1=Value1;[dev:|drv:]Name2=Value2"
222 */
223 char *pszSrc = mSettings.mutableRaw();
224
225 if (pszSrc)
226 {
227 while (*pszSrc)
228 {
229 /* Does the setting belong to device of driver. Default is both. */
230 bool fDev = true;
231 bool fDrv = true;
232 if (RTStrNICmp(pszSrc, RT_STR_TUPLE("drv:")) == 0)
233 {
234 pszSrc += sizeof("drv:")-1;
235 fDev = false;
236 }
237 else if (RTStrNICmp(pszSrc, RT_STR_TUPLE("dev:")) == 0)
238 {
239 pszSrc += sizeof("dev:")-1;
240 fDrv = false;
241 }
242
243 char *pszEq = strchr(pszSrc, '=');
244 if (!pszEq)
245 {
246 hr = E_INVALIDARG;
247 break;
248 }
249
250 char *pszEnd = strchr(pszEq, ';');
251 if (!pszEnd)
252 pszEnd = pszEq + strlen(pszEq);
253
254 *pszEq = 0;
255 char chEnd = *pszEnd;
256 *pszEnd = 0;
257
258 /* Empty strings not allowed. */
259 if (*pszSrc != 0 && pszEq[1] != 0)
260 {
261 if (fDev)
262 mDevSettings[pszSrc] = &pszEq[1];
263 if (fDrv)
264 mDrvSettings[pszSrc] = &pszEq[1];
265 }
266
267 *pszEq = '=';
268 *pszEnd = chEnd;
269
270 pszSrc = pszEnd;
271 if (*pszSrc == ';')
272 pszSrc++;
273 }
274
275 if (SUCCEEDED(hr))
276 {
277 EUSBSettingsMap::const_iterator it;
278 for (it = mDevSettings.begin(); it != mDevSettings.end(); ++it)
279 LogRelFlowFunc(("[dev:%s] = [%s]\n", it->first.c_str(), it->second.c_str()));
280 for (it = mDrvSettings.begin(); it != mDrvSettings.end(); ++it)
281 LogRelFlowFunc(("[drv:%s] = [%s]\n", it->first.c_str(), it->second.c_str()));
282 }
283 }
284
285 return hr;
286}
287
288HRESULT EUSBWEBCAM::Attach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM, const char *pszDriver)
289{
290 int vrc = pVMM->pfnVMR3ReqCallWaitU(pUVM, 0 /* idDstCpu (saved state, see #6232) */,
291 (PFNRT)emulatedWebcamAttach, 4,
292 pUVM, pVMM, this, pszDriver);
293 if (RT_SUCCESS(vrc))
294 return S_OK;
295 LogFlowThisFunc(("%Rrc\n", vrc));
296 return pConsole->setErrorBoth(VBOX_E_VM_ERROR, vrc, EmulatedUSB::tr("Attach emulated USB webcam (%Rrc)"), vrc);
297}
298
299HRESULT EUSBWEBCAM::Detach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM)
300{
301 int vrc = pVMM->pfnVMR3ReqCallWaitU(pUVM, 0 /* idDstCpu (saved state, see #6232) */,
302 (PFNRT)emulatedWebcamDetach, 3,
303 pUVM, pVMM, this);
304 if (RT_SUCCESS(vrc))
305 return S_OK;
306 LogFlowThisFunc(("%Rrc\n", vrc));
307 return pConsole->setErrorBoth(VBOX_E_VM_ERROR, vrc, EmulatedUSB::tr("Detach emulated USB webcam (%Rrc)"), vrc);
308}
309
310
311/*
312 * EmulatedUSB implementation.
313 */
314DEFINE_EMPTY_CTOR_DTOR(EmulatedUSB)
315
316HRESULT EmulatedUSB::FinalConstruct()
317{
318 return BaseFinalConstruct();
319}
320
321void EmulatedUSB::FinalRelease()
322{
323 uninit();
324
325 BaseFinalRelease();
326}
327
328/*
329 * Initializes the instance.
330 *
331 * @param pConsole The owner.
332 */
333HRESULT EmulatedUSB::init(ComObjPtr<Console> pConsole)
334{
335 LogFlowThisFunc(("\n"));
336
337 ComAssertRet(!pConsole.isNull(), E_INVALIDARG);
338
339 /* Enclose the state transition NotReady->InInit->Ready */
340 AutoInitSpan autoInitSpan(this);
341 AssertReturn(autoInitSpan.isOk(), E_FAIL);
342
343 m.pConsole = pConsole;
344
345 mEmUsbIf.pvUser = this;
346 mEmUsbIf.pfnQueryEmulatedUsbDataById = EmulatedUSB::i_QueryEmulatedUsbDataById;
347
348 /* Confirm a successful initialization */
349 autoInitSpan.setSucceeded();
350
351 return S_OK;
352}
353
354/*
355 * Uninitializes the instance.
356 * Called either from FinalRelease() or by the parent when it gets destroyed.
357 */
358void EmulatedUSB::uninit()
359{
360 LogFlowThisFunc(("\n"));
361
362 m.pConsole.setNull();
363
364 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
365 for (WebcamsMap::iterator it = m.webcams.begin(); it != m.webcams.end(); ++it)
366 {
367 EUSBWEBCAM *p = it->second;
368 if (p)
369 {
370 it->second = NULL;
371 p->Release();
372 }
373 }
374 m.webcams.clear();
375 alock.release();
376
377 /* Enclose the state transition Ready->InUninit->NotReady */
378 AutoUninitSpan autoUninitSpan(this);
379 if (autoUninitSpan.uninitDone())
380 return;
381}
382
383HRESULT EmulatedUSB::getWebcams(std::vector<com::Utf8Str> &aWebcams)
384{
385 HRESULT hrc = S_OK;
386
387 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
388
389 try
390 {
391 aWebcams.resize(m.webcams.size());
392 }
393 catch (std::bad_alloc &)
394 {
395 hrc = E_OUTOFMEMORY;
396 }
397 catch (...)
398 {
399 hrc = E_FAIL;
400 }
401
402 if (SUCCEEDED(hrc))
403 {
404 size_t i;
405 WebcamsMap::const_iterator it;
406 for (i = 0, it = m.webcams.begin(); it != m.webcams.end(); ++it)
407 aWebcams[i++] = it->first;
408 }
409
410 return hrc;
411}
412
413PEMULATEDUSBIF EmulatedUSB::i_getEmulatedUsbIf()
414{
415 return &mEmUsbIf;
416}
417
418static const Utf8Str s_pathDefault(".0");
419
420HRESULT EmulatedUSB::webcamAttach(const com::Utf8Str &aPath,
421 const com::Utf8Str &aSettings)
422{
423 return i_webcamAttachInternal(aPath, aSettings, "HostWebcam", NULL);
424}
425
426HRESULT EmulatedUSB::i_webcamAttachInternal(const com::Utf8Str &aPath,
427 const com::Utf8Str &aSettings,
428 const char *pszDriver,
429 void *pvObject)
430{
431 HRESULT hrc = S_OK;
432
433 const Utf8Str &path = aPath.isEmpty() || aPath == "."? s_pathDefault: aPath;
434
435 Console::SafeVMPtr ptrVM(m.pConsole);
436 if (ptrVM.isOk())
437 {
438 EUSBWEBCAM *p = new EUSBWEBCAM();
439 if (p)
440 {
441 hrc = p->Initialize(m.pConsole, this, &path, &aSettings, pvObject);
442 if (SUCCEEDED(hrc))
443 {
444 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
445 WebcamsMap::const_iterator it = m.webcams.find(path);
446 if (it == m.webcams.end())
447 {
448 p->AddRef();
449 try
450 {
451 m.webcams[path] = p;
452 }
453 catch (std::bad_alloc &)
454 {
455 hrc = E_OUTOFMEMORY;
456 }
457 catch (...)
458 {
459 hrc = E_FAIL;
460 }
461 p->enmStatus = EUSBDEVICE_ATTACHING;
462 }
463 else
464 {
465 hrc = E_FAIL;
466 }
467 }
468
469 if (SUCCEEDED(hrc))
470 hrc = p->Attach(m.pConsole, ptrVM.rawUVM(), ptrVM.vtable(), pszDriver);
471
472 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
473 if (SUCCEEDED(hrc))
474 p->enmStatus = EUSBDEVICE_ATTACHED;
475 else if (p->enmStatus != EUSBDEVICE_CREATED)
476 m.webcams.erase(path);
477 alock.release();
478
479 p->Release();
480 }
481 else
482 {
483 hrc = E_OUTOFMEMORY;
484 }
485 }
486 else
487 {
488 hrc = VBOX_E_INVALID_VM_STATE;
489 }
490
491 return hrc;
492}
493
494HRESULT EmulatedUSB::webcamDetach(const com::Utf8Str &aPath)
495{
496 return i_webcamDetachInternal(aPath);
497}
498
499HRESULT EmulatedUSB::i_webcamDetachInternal(const com::Utf8Str &aPath)
500{
501 HRESULT hrc = S_OK;
502
503 const Utf8Str &path = aPath.isEmpty() || aPath == "."? s_pathDefault: aPath;
504
505 Console::SafeVMPtr ptrVM(m.pConsole);
506 if (ptrVM.isOk())
507 {
508 EUSBWEBCAM *p = NULL;
509
510 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
511 WebcamsMap::iterator it = m.webcams.find(path);
512 if (it != m.webcams.end())
513 {
514 if (it->second->enmStatus == EUSBDEVICE_ATTACHED)
515 {
516 p = it->second;
517 m.webcams.erase(it);
518 }
519 }
520 alock.release();
521
522 if (p)
523 {
524 hrc = p->Detach(m.pConsole, ptrVM.rawUVM(), ptrVM.vtable());
525 p->Release();
526 }
527 else
528 {
529 hrc = E_INVALIDARG;
530 }
531 }
532 else
533 {
534 hrc = VBOX_E_INVALID_VM_STATE;
535 }
536
537 return hrc;
538}
539
540/*static*/ DECLCALLBACK(int)
541EmulatedUSB::eusbCallbackEMT(EmulatedUSB *pThis, char *pszId, uint32_t iEvent, void *pvData, uint32_t cbData)
542{
543 LogRelFlowFunc(("id %s event %d, data %p %d\n", pszId, iEvent, pvData, cbData));
544
545 NOREF(cbData);
546
547 int vrc = VINF_SUCCESS;
548 if (iEvent == 0)
549 {
550 com::Utf8Str path;
551 HRESULT hrc = pThis->webcamPathFromId(&path, pszId);
552 if (SUCCEEDED(hrc))
553 {
554 hrc = pThis->webcamDetach(path);
555 if (FAILED(hrc))
556 {
557 vrc = VERR_INVALID_STATE;
558 }
559 }
560 else
561 {
562 vrc = VERR_NOT_FOUND;
563 }
564 }
565 else
566 {
567 vrc = VERR_INVALID_PARAMETER;
568 }
569
570 RTMemFree(pszId);
571 RTMemFree(pvData);
572
573 LogRelFlowFunc(("vrc %Rrc\n", vrc));
574 return vrc;
575}
576
577/* static */ DECLCALLBACK(int)
578EmulatedUSB::i_eusbCallback(void *pv, const char *pszId, uint32_t iEvent, const void *pvData, uint32_t cbData)
579{
580 /* Make a copy of parameters, forward to EMT and leave the callback to not hold any lock in the device. */
581 int vrc = VINF_SUCCESS;
582 void *pvDataCopy = NULL;
583 if (cbData > 0)
584 {
585 pvDataCopy = RTMemDup(pvData, cbData);
586 if (!pvDataCopy)
587 vrc = VERR_NO_MEMORY;
588 }
589 if (RT_SUCCESS(vrc))
590 {
591 void *pvIdCopy = RTMemDup(pszId, strlen(pszId) + 1);
592 if (pvIdCopy)
593 {
594 if (RT_SUCCESS(vrc))
595 {
596 EmulatedUSB *pThis = (EmulatedUSB *)pv;
597 Console::SafeVMPtr ptrVM(pThis->m.pConsole);
598 if (ptrVM.isOk())
599 {
600 /* No wait. */
601 vrc = ptrVM.vtable()->pfnVMR3ReqCallNoWaitU(ptrVM.rawUVM(), 0 /* idDstCpu */,
602 (PFNRT)EmulatedUSB::eusbCallbackEMT, 5,
603 pThis, pvIdCopy, iEvent, pvDataCopy, cbData);
604 if (RT_SUCCESS(vrc))
605 return vrc;
606 }
607 else
608 vrc = VERR_INVALID_STATE;
609 }
610 RTMemFree(pvIdCopy);
611 }
612 else
613 vrc = VERR_NO_MEMORY;
614 RTMemFree(pvDataCopy);
615 }
616 return vrc;
617}
618
619/*static*/
620DECLCALLBACK(int) EmulatedUSB::i_QueryEmulatedUsbDataById(void *pvUser, const char *pszId, void **ppvEmUsbCb, void **ppvEmUsbCbData, void **ppvObject)
621{
622 EmulatedUSB *pEmUsb = (EmulatedUSB *)pvUser;
623
624 AutoReadLock alock(pEmUsb COMMA_LOCKVAL_SRC_POS);
625 WebcamsMap::const_iterator it;
626 for (it = pEmUsb->m.webcams.begin(); it != pEmUsb->m.webcams.end(); ++it)
627 {
628 EUSBWEBCAM *p = it->second;
629 if (p->HasId(pszId))
630 {
631 if (ppvEmUsbCb)
632 *ppvEmUsbCb = (void *)EmulatedUSB::i_eusbCallback;
633 if (ppvEmUsbCbData)
634 *ppvEmUsbCbData = pEmUsb;
635 if (ppvObject)
636 *ppvObject = p->getObjectPtr();
637
638 return VINF_SUCCESS;
639 }
640 }
641
642 return VERR_NOT_FOUND;
643}
644
645HRESULT EmulatedUSB::webcamPathFromId(com::Utf8Str *pPath, const char *pszId)
646{
647 HRESULT hrc = S_OK;
648
649 Console::SafeVMPtr ptrVM(m.pConsole);
650 if (ptrVM.isOk())
651 {
652 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
653 WebcamsMap::const_iterator it;
654 for (it = m.webcams.begin(); it != m.webcams.end(); ++it)
655 {
656 EUSBWEBCAM *p = it->second;
657 if (p->HasId(pszId))
658 {
659 *pPath = it->first;
660 break;
661 }
662 }
663
664 if (it == m.webcams.end())
665 {
666 hrc = E_FAIL;
667 }
668 alock.release();
669 }
670 else
671 {
672 hrc = VBOX_E_INVALID_VM_STATE;
673 }
674
675 return hrc;
676}
677
678/* vi: set tabstop=4 shiftwidth=4 expandtab: */
Note: See TracBrowser for help on using the repository browser.

© 2023 Oracle
ContactPrivacy policyTerms of Use