VirtualBox

source: vbox/trunk/src/VBox/Devices/Serial/DrvHostSerial.cpp@ 4612

Last change on this file since 4612 was 4372, checked in by vboxsync, 17 years ago

Finally corrected the RTFileRead, RTFileReadAt, RTFileWrite and RTFileWriteAt APIs to size_t. This was long overdue.

File size: 20.9 KB
Line 
1/** $Id: $ */
2/** @file
3 * VBox stream I/O devices: Host serial driver
4 *
5 * Contributed by: Alexander Eichner
6 */
7
8/*
9 * Copyright (C) 2006-2007 innotek GmbH
10 *
11 * This file is part of VirtualBox Open Source Edition (OSE), as
12 * available from http://www.virtualbox.org. This file is free software;
13 * you can redistribute it and/or modify it under the terms of the GNU
14 * General Public License as published by the Free Software Foundation,
15 * in version 2 as it comes in the "COPYING" file of the VirtualBox OSE
16 * distribution. VirtualBox OSE is distributed in the hope that it will
17 * be useful, but WITHOUT ANY WARRANTY of any kind.
18 *
19 * If you received this file as part of a commercial VirtualBox
20 * distribution, then only the terms of your commercial VirtualBox
21 * license agreement apply instead of the previous paragraph.
22 */
23
24
25
26/*******************************************************************************
27* Header Files *
28*******************************************************************************/
29#define LOG_GROUP LOG_GROUP_DRV_HOST_SERIAL
30#include <VBox/pdm.h>
31#include <VBox/err.h>
32
33#include <VBox/log.h>
34#include <iprt/asm.h>
35#include <iprt/assert.h>
36#include <iprt/stream.h>
37#include <iprt/semaphore.h>
38#include <iprt/file.h>
39#include <iprt/alloc.h>
40
41#ifdef RT_OS_LINUX
42# include <termios.h>
43# include <sys/types.h>
44# include <fcntl.h>
45# include <string.h>
46# include <unistd.h>
47#elif defined(RT_OS_WINDOWS)
48# include <windows.h>
49#endif
50
51#include "Builtins.h"
52
53
54/** Size of the send fifo queue (in bytes) */
55#define CHAR_MAX_SEND_QUEUE 0x80
56#define CHAR_MAX_SEND_QUEUE_MASK 0x7f
57
58/*******************************************************************************
59* Structures and Typedefs *
60*******************************************************************************/
61
62/**
63 * Char driver instance data.
64 */
65typedef struct DRVHOSTSERIAL
66{
67 /** Pointer to the driver instance structure. */
68 PPDMDRVINS pDrvIns;
69 /** Pointer to the char port interface of the driver/device above us. */
70 PPDMICHARPORT pDrvCharPort;
71 /** Our char interface. */
72 PDMICHAR IChar;
73 /** Flag to notify the receive thread it should terminate. */
74 volatile bool fShutdown;
75 /** Receive thread ID. */
76 RTTHREAD ReceiveThread;
77 /** Send thread ID. */
78 RTTHREAD SendThread;
79 /** Send event semephore */
80 RTSEMEVENT SendSem;
81
82 /** the device path */
83 char *pszDevicePath;
84 /** the device handle */
85 RTFILE DeviceFile;
86
87 /** Internal send FIFO queue */
88 uint8_t aSendQueue[CHAR_MAX_SEND_QUEUE];
89 uint32_t iSendQueueHead;
90 uint32_t iSendQueueTail;
91
92 /** Read/write statistics */
93 STAMCOUNTER StatBytesRead;
94 STAMCOUNTER StatBytesWritten;
95} DRVHOSTSERIAL, *PDRVHOSTSERIAL;
96
97
98/** Converts a pointer to DRVCHAR::IChar to a PDRVHOSTSERIAL. */
99#define PDMICHAR_2_DRVHOSTSERIAL(pInterface) ( (PDRVHOSTSERIAL)((uintptr_t)pInterface - RT_OFFSETOF(DRVHOSTSERIAL, IChar)) )
100
101
102/* -=-=-=-=- IBase -=-=-=-=- */
103
104/**
105 * Queries an interface to the driver.
106 *
107 * @returns Pointer to interface.
108 * @returns NULL if the interface was not supported by the driver.
109 * @param pInterface Pointer to this interface structure.
110 * @param enmInterface The requested interface identification.
111 */
112static DECLCALLBACK(void *) drvHostSerialQueryInterface(PPDMIBASE pInterface, PDMINTERFACE enmInterface)
113{
114 PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface);
115 PDRVHOSTSERIAL pData = PDMINS2DATA(pDrvIns, PDRVHOSTSERIAL);
116 switch (enmInterface)
117 {
118 case PDMINTERFACE_BASE:
119 return &pDrvIns->IBase;
120 case PDMINTERFACE_CHAR:
121 return &pData->IChar;
122 default:
123 return NULL;
124 }
125}
126
127
128/* -=-=-=-=- IChar -=-=-=-=- */
129
130/** @copydoc PDMICHAR::pfnWrite */
131static DECLCALLBACK(int) drvHostSerialWrite(PPDMICHAR pInterface, const void *pvBuf, size_t cbWrite)
132{
133 PDRVHOSTSERIAL pData = PDMICHAR_2_DRVHOSTSERIAL(pInterface);
134 const uint8_t *pbBuffer = (const uint8_t *)pvBuf;
135
136 LogFlow(("%s: pvBuf=%#p cbWrite=%d\n", __FUNCTION__, pvBuf, cbWrite));
137
138 for (uint32_t i=0;i<cbWrite;i++)
139 {
140 uint32_t idx = pData->iSendQueueHead;
141
142 pData->aSendQueue[idx] = pbBuffer[i];
143 idx = (idx + 1) & CHAR_MAX_SEND_QUEUE_MASK;
144
145 STAM_COUNTER_INC(&pData->StatBytesWritten);
146 ASMAtomicXchgU32(&pData->iSendQueueHead, idx);
147 }
148 RTSemEventSignal(pData->SendSem);
149 return VINF_SUCCESS;
150}
151
152static DECLCALLBACK(int) drvHostSerialSetParameters(PPDMICHAR pInterface, unsigned Bps, char chParity, unsigned cDataBits, unsigned cStopBits)
153{
154 PDRVHOSTSERIAL pData = PDMICHAR_2_DRVHOSTSERIAL(pInterface);
155#ifdef RT_OS_LINUX
156 struct termios *termiosSetup;
157 int baud_rate;
158#elif defined(RT_OS_WINDOWS)
159 LPDCB comSetup;
160#endif
161
162 LogFlow(("%s: Bps=%u chParity=%c cDataBits=%u cStopBits=%u\n", __FUNCTION__, Bps, chParity, cDataBits, cStopBits));
163
164#ifdef RT_OS_LINUX
165 termiosSetup = (struct termios *)RTMemTmpAllocZ(sizeof(struct termios));
166
167 /* Enable receiver */
168 termiosSetup->c_cflag |= (CLOCAL | CREAD);
169
170 switch (Bps) {
171 case 50:
172 baud_rate = B50;
173 break;
174 case 75:
175 baud_rate = B75;
176 break;
177 case 110:
178 baud_rate = B110;
179 break;
180 case 134:
181 baud_rate = B134;
182 break;
183 case 150:
184 baud_rate = B150;
185 break;
186 case 200:
187 baud_rate = B200;
188 break;
189 case 300:
190 baud_rate = B300;
191 break;
192 case 600:
193 baud_rate = B600;
194 break;
195 case 1200:
196 baud_rate = B1200;
197 break;
198 case 1800:
199 baud_rate = B1800;
200 break;
201 case 2400:
202 baud_rate = B2400;
203 break;
204 case 4800:
205 baud_rate = B4800;
206 break;
207 case 9600:
208 baud_rate = B9600;
209 break;
210 case 19200:
211 baud_rate = B19200;
212 break;
213 case 38400:
214 baud_rate = B38400;
215 break;
216 case 57600:
217 baud_rate = B57600;
218 break;
219 case 115200:
220 baud_rate = B115200;
221 break;
222 default:
223 baud_rate = B9600;
224 }
225
226 cfsetispeed(termiosSetup, baud_rate);
227 cfsetospeed(termiosSetup, baud_rate);
228
229 switch (chParity) {
230 case 'E':
231 termiosSetup->c_cflag |= PARENB;
232 break;
233 case 'O':
234 termiosSetup->c_cflag |= (PARENB | PARODD);
235 break;
236 case 'N':
237 break;
238 default:
239 break;
240 }
241
242 switch (cDataBits) {
243 case 5:
244 termiosSetup->c_cflag |= CS5;
245 break;
246 case 6:
247 termiosSetup->c_cflag |= CS6;
248 break;
249 case 7:
250 termiosSetup->c_cflag |= CS7;
251 break;
252 case 8:
253 termiosSetup->c_cflag |= CS8;
254 break;
255 default:
256 break;
257 }
258
259 switch (cStopBits) {
260 case 2:
261 termiosSetup->c_cflag |= CSTOPB;
262 default:
263 break;
264 }
265
266 /* set serial port to raw input */
267 termiosSetup->c_lflag = ~(ICANON | ECHO | ECHOE | ISIG);
268
269 tcsetattr(pData->DeviceFile, TCSANOW, termiosSetup);
270 RTMemFree(termiosSetup);
271#elif defined(RT_OS_WINDOWS)
272 comSetup = (LPDCB)RTMemTmpAllocZ(sizeof(DCB));
273
274 comSetup->DCBlength = sizeof(DCB);
275
276 switch (Bps) {
277 case 110:
278 comSetup->BaudRate = CBR_110;
279 break;
280 case 300:
281 comSetup->BaudRate = CBR_300;
282 break;
283 case 600:
284 comSetup->BaudRate = CBR_600;
285 break;
286 case 1200:
287 comSetup->BaudRate = CBR_1200;
288 break;
289 case 2400:
290 comSetup->BaudRate = CBR_2400;
291 break;
292 case 4800:
293 comSetup->BaudRate = CBR_4800;
294 break;
295 case 9600:
296 comSetup->BaudRate = CBR_9600;
297 break;
298 case 14400:
299 comSetup->BaudRate = CBR_14400;
300 break;
301 case 19200:
302 comSetup->BaudRate = CBR_19200;
303 break;
304 case 38400:
305 comSetup->BaudRate = CBR_38400;
306 break;
307 case 57600:
308 comSetup->BaudRate = CBR_57600;
309 break;
310 case 115200:
311 comSetup->BaudRate = CBR_115200;
312 break;
313 default:
314 comSetup->BaudRate = CBR_9600;
315 }
316
317 comSetup->fBinary = TRUE;
318 comSetup->fOutxCtsFlow = FALSE;
319 comSetup->fOutxDsrFlow = FALSE;
320 comSetup->fDtrControl = DTR_CONTROL_DISABLE;
321 comSetup->fDsrSensitivity = FALSE;
322 comSetup->fTXContinueOnXoff = TRUE;
323 comSetup->fOutX = FALSE;
324 comSetup->fInX = FALSE;
325 comSetup->fErrorChar = FALSE;
326 comSetup->fNull = FALSE;
327 comSetup->fRtsControl = RTS_CONTROL_DISABLE;
328 comSetup->fAbortOnError = FALSE;
329 comSetup->wReserved = 0;
330 comSetup->XonLim = 5;
331 comSetup->XoffLim = 5;
332 comSetup->ByteSize = cDataBits;
333
334 switch (chParity) {
335 case 'E':
336 comSetup->Parity = EVENPARITY;
337 break;
338 case 'O':
339 comSetup->Parity = ODDPARITY;
340 break;
341 case 'N':
342 comSetup->Parity = NOPARITY;
343 break;
344 default:
345 break;
346 }
347
348 switch (cStopBits) {
349 case 1:
350 comSetup->StopBits = ONESTOPBIT;
351 break;
352 case 2:
353 comSetup->StopBits = TWOSTOPBITS;
354 break;
355 default:
356 break;
357 }
358
359 comSetup->XonChar = 0;
360 comSetup->XoffChar = 0;
361 comSetup->ErrorChar = 0;
362 comSetup->EofChar = 0;
363 comSetup->EvtChar = 0;
364
365 SetCommState((HANDLE)pData->DeviceFile, comSetup);
366 RTMemFree(comSetup);
367#endif /* RT_OS_WINDOWS */
368
369 return VINF_SUCCESS;
370}
371
372/* -=-=-=-=- receive thread -=-=-=-=- */
373
374/**
375 * Send thread loop.
376 *
377 * @returns VINF_SUCCESS.
378 * @param ThreadSelf Thread handle to this thread.
379 * @param pvUser User argument.
380 */
381static DECLCALLBACK(int) drvHostSerialSendLoop(RTTHREAD ThreadSelf, void *pvUser)
382{
383 PDRVHOSTSERIAL pData = (PDRVHOSTSERIAL)pvUser;
384
385 while (!pData->fShutdown)
386 {
387 int rc = RTSemEventWait(pData->SendSem, RT_INDEFINITE_WAIT);
388 if (VBOX_FAILURE(rc))
389 break;
390
391 /*
392 * Write the character to the host device.
393 */
394 while ( !pData->fShutdown
395 && pData->iSendQueueTail != pData->iSendQueueHead)
396 {
397 unsigned cbProcessed = 1;
398
399 rc = RTFileWrite(pData->DeviceFile, &pData->aSendQueue[pData->iSendQueueTail], cbProcessed, NULL);
400 if (VBOX_SUCCESS(rc))
401 {
402 Assert(cbProcessed);
403 pData->iSendQueueTail++;
404 pData->iSendQueueTail &= CHAR_MAX_SEND_QUEUE_MASK;
405 }
406 else if (VBOX_FAILURE(rc))
407 {
408 LogFlow(("Write failed with %Vrc; skipping\n", rc));
409 break;
410 }
411 }
412 }
413
414 return VINF_SUCCESS;
415}
416
417
418/* -=-=-=-=- receive thread -=-=-=-=- */
419
420/**
421 * Receive thread loop.
422 *
423 * This thread pushes data from the host serial device up the driver
424 * chain toward the serial device.
425 *
426 * @returns VINF_SUCCESS.
427 * @param ThreadSelf Thread handle to this thread.
428 * @param pvUser User argument.
429 */
430static DECLCALLBACK(int) drvHostSerialReceiveLoop(RTTHREAD ThreadSelf, void *pvUser)
431{
432 PDRVHOSTSERIAL pData = (PDRVHOSTSERIAL)pvUser;
433 uint8_t abBuffer[256];
434 uint8_t *pbBuffer = NULL;
435 size_t cbRemaining = 0; /* start by reading host data */
436 int rc = VINF_SUCCESS;
437
438 while (!pData->fShutdown)
439 {
440 if (!cbRemaining)
441 {
442 /* Get a block of data from the host serial device. */
443 size_t cbRead;
444 rc = RTFileRead(pData->DeviceFile, abBuffer, sizeof(abBuffer), &cbRead);
445 if (VBOX_FAILURE(rc))
446 {
447 LogRel(("Host Serial Driver: Read failed with %Vrc, terminating the worker thread.\n", rc));
448 break;
449 }
450 cbRemaining = cbRead;
451 pbBuffer = abBuffer;
452 }
453 else
454 {
455 /* Send data to the guest. */
456 size_t cbProcessed = cbRemaining;
457 rc = pData->pDrvCharPort->pfnNotifyRead(pData->pDrvCharPort, pbBuffer, &cbProcessed);
458 if (VBOX_SUCCESS(rc))
459 {
460 Assert(cbProcessed); Assert(cbProcessed <= cbRemaining);
461 pbBuffer += cbProcessed;
462 cbRemaining -= cbProcessed;
463 STAM_COUNTER_ADD(&pData->StatBytesRead, cbProcessed);
464 }
465 else if (rc == VERR_TIMEOUT)
466 {
467 /* Normal case, just means that the guest didn't accept a new
468 * character before the timeout elapsed. Just retry. */
469 rc = VINF_SUCCESS;
470 }
471 else
472 {
473 LogRel(("Host Serial Driver: NotifyRead failed with %Vrc, terminating the worker thread.\n", rc));
474 break;
475 }
476 }
477 }
478
479 return VINF_SUCCESS;
480}
481
482
483/* -=-=-=-=- driver interface -=-=-=-=- */
484
485/**
486 * Construct a char driver instance.
487 *
488 * @returns VBox status.
489 * @param pDrvIns The driver instance data.
490 * If the registration structure is needed,
491 * pDrvIns->pDrvReg points to it.
492 * @param pCfgHandle Configuration node handle for the driver. Use this to
493 * obtain the configuration of the driver instance. It's
494 * also found in pDrvIns->pCfgHandle as it's expected to
495 * be used frequently in this function.
496 */
497static DECLCALLBACK(int) drvHostSerialConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfgHandle)
498{
499 PDRVHOSTSERIAL pData = PDMINS2DATA(pDrvIns, PDRVHOSTSERIAL);
500 LogFlow(("%s: iInstance=%d\n", __FUNCTION__, pDrvIns->iInstance));
501
502 /*
503 * Init basic data members and interfaces.
504 */
505 pData->ReceiveThread = NIL_RTTHREAD;
506 pData->SendThread = NIL_RTTHREAD;
507 pData->fShutdown = false;
508 /* IBase. */
509 pDrvIns->IBase.pfnQueryInterface = drvHostSerialQueryInterface;
510 /* IChar. */
511 pData->IChar.pfnWrite = drvHostSerialWrite;
512 pData->IChar.pfnSetParameters = drvHostSerialSetParameters;
513
514 /*
515 * Query configuration.
516 */
517 /* Device */
518 int rc = CFGMR3QueryStringAlloc(pCfgHandle, "DevicePath", &pData->pszDevicePath);
519 if (VBOX_FAILURE(rc))
520 {
521 AssertMsgFailed(("Configuration error: query for \"DevicePath\" string returned %Vra.\n", rc));
522 return rc;
523 }
524
525 /*
526 * Open the device
527 */
528 rc = RTFileOpen(&pData->DeviceFile, pData->pszDevicePath, RTFILE_O_OPEN | RTFILE_O_READWRITE);
529
530 if (VBOX_FAILURE(rc)) {
531 pData->DeviceFile = NIL_RTFILE;
532 AssertMsgFailed(("Could not open host device %s, rc=%Vrc\n", pData->pszDevicePath, rc));
533 switch (rc) {
534 case VERR_ACCESS_DENIED:
535 return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS,
536#ifdef RT_OS_LINUX
537 N_("Cannot open host device '%s' for read/write access. Check the permissions "
538 "of that device ('/bin/ls -l %s'): Most probably you need to be member "
539 "of the device group. Make sure that you logout/login after changing "
540 "the group settings of the current user"),
541#else
542 N_("Cannot open host device '%s' for read/write access. Check the permissions "
543 "of that device"),
544#endif
545 pData->pszDevicePath, pData->pszDevicePath);
546 default:
547 return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS,
548 N_("Failed to open host device '%s'"),
549 pData->pszDevicePath);
550 }
551 }
552
553 /* Set to non blocking I/O */
554#ifdef RT_OS_LINUX
555 fcntl(pData->DeviceFile, F_SETFL, O_NONBLOCK);
556#elif defined(RT_OS_WINDOWS)
557 /* Set the COMMTIMEOUTS to get non blocking I/O */
558 COMMTIMEOUTS comTimeout;
559
560 comTimeout.ReadIntervalTimeout = MAXDWORD;
561 comTimeout.ReadTotalTimeoutMultiplier = 0;
562 comTimeout.ReadTotalTimeoutConstant = 0;
563 comTimeout.WriteTotalTimeoutMultiplier = 0;
564 comTimeout.WriteTotalTimeoutConstant = 0;
565
566 SetCommTimeouts((HANDLE)pData->DeviceFile, &comTimeout);
567#endif
568
569 /*
570 * Get the ICharPort interface of the above driver/device.
571 */
572 pData->pDrvCharPort = (PPDMICHARPORT)pDrvIns->pUpBase->pfnQueryInterface(pDrvIns->pUpBase, PDMINTERFACE_CHAR_PORT);
573 if (!pData->pDrvCharPort)
574 return PDMDrvHlpVMSetError(pDrvIns, VERR_PDM_MISSING_INTERFACE_ABOVE, RT_SRC_POS, N_("HostSerial#%d has no char port interface above"), pDrvIns->iInstance);
575
576 rc = RTThreadCreate(&pData->ReceiveThread, drvHostSerialReceiveLoop, (void *)pData, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, "Char Receive");
577 if (VBOX_FAILURE(rc))
578 return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, N_("HostSerial#%d cannot create receive thread"), pDrvIns->iInstance);
579
580 rc = RTSemEventCreate(&pData->SendSem);
581 AssertRC(rc);
582
583 rc = RTThreadCreate(&pData->SendThread, drvHostSerialSendLoop, (void *)pData, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, "Serial Send");
584 if (VBOX_FAILURE(rc))
585 return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, N_("HostSerial#%d cannot create send thread"), pDrvIns->iInstance);
586
587
588 PDMDrvHlpSTAMRegisterF(pDrvIns, &pData->StatBytesWritten, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, "Nr of bytes written", "/Devices/HostSerial%d/Written", pDrvIns->iInstance);
589 PDMDrvHlpSTAMRegisterF(pDrvIns, &pData->StatBytesRead, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, "Nr of bytes read", "/Devices/HostSerial%d/Read", pDrvIns->iInstance);
590
591 return VINF_SUCCESS;
592}
593
594
595/**
596 * Destruct a char driver instance.
597 *
598 * Most VM resources are freed by the VM. This callback is provided so that
599 * any non-VM resources can be freed correctly.
600 *
601 * @param pDrvIns The driver instance data.
602 */
603static DECLCALLBACK(void) drvHostSerialDestruct(PPDMDRVINS pDrvIns)
604{
605 PDRVHOSTSERIAL pData = PDMINS2DATA(pDrvIns, PDRVHOSTSERIAL);
606
607 LogFlow(("%s: iInstance=%d\n", __FUNCTION__, pDrvIns->iInstance));
608
609 ASMAtomicXchgBool(&pData->fShutdown, true);
610 if (pData->ReceiveThread != NIL_RTTHREAD)
611 {
612 int rc = RTThreadWait(pData->ReceiveThread, 15000, NULL);
613 if (RT_FAILURE(rc))
614 LogRel(("HostSerial%d: receive thread did not terminate (rc=%Rrc)\n", pDrvIns->iInstance, rc));
615 pData->ReceiveThread = NIL_RTTHREAD;
616 }
617
618 /* Empty the send queue */
619 pData->iSendQueueTail = pData->iSendQueueHead = 0;
620
621 RTSemEventSignal(pData->SendSem);
622 RTSemEventDestroy(pData->SendSem);
623 pData->SendSem = NIL_RTSEMEVENT;
624
625 if (pData->SendThread != NIL_RTTHREAD)
626 {
627 int rc = RTThreadWait(pData->SendThread, 15000, NULL);
628 if (RT_FAILURE(rc))
629 LogRel(("HostSerial%d: send thread did not terminate (rc=%Rrc)\n", pDrvIns->iInstance, rc));
630 pData->SendThread = NIL_RTTHREAD;
631 }
632}
633
634/**
635 * Char driver registration record.
636 */
637const PDMDRVREG g_DrvHostSerial =
638{
639 /* u32Version */
640 PDM_DRVREG_VERSION,
641 /* szDriverName */
642 "Host Serial",
643 /* pszDescription */
644 "Host serial driver.",
645 /* fFlags */
646 PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT,
647 /* fClass. */
648 PDM_DRVREG_CLASS_CHAR,
649 /* cMaxInstances */
650 ~0,
651 /* cbInstance */
652 sizeof(DRVHOSTSERIAL),
653 /* pfnConstruct */
654 drvHostSerialConstruct,
655 /* pfnDestruct */
656 drvHostSerialDestruct,
657 /* pfnIOCtl */
658 NULL,
659 /* pfnPowerOn */
660 NULL,
661 /* pfnReset */
662 NULL,
663 /* pfnSuspend */
664 NULL,
665 /* pfnResume */
666 NULL,
667 /* pfnDetach */
668 NULL,
669 /** pfnPowerOff */
670 NULL
671};
672
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