VirtualBox

Ignore:
Timestamp:
Mar 17, 2016 6:40:28 PM (9 years ago)
Author:
vboxsync
Message:

bugref:8288: Additions/x11: rework VBoxClient video mode hint handling: reworked the display parts of VBoxClient. Now they only use standard X11 mechanisms for any recent server version, which means that the same code works with the kernel and the user-space driver, and also wait for about two seconds before changing the mode, cancelling if some one else is faster, to avoid interfering with mechanisms provided by the desktop environment. With old servers we get mode hints from a property updated by the driver. The user space driver code has been updated to match, and has also dropped pre-HGSMI mechanisms for getting mode hints.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/VBox/Additions/x11/VBoxClient/display.cpp

    r59943 r60083  
    1616 */
    1717
     18#include "VBoxClient.h"
     19
     20#include <iprt/err.h>
     21#include <iprt/file.h>
     22#include <iprt/mem.h>
     23#include <iprt/string.h>
     24
     25#include <X11/Xlib.h>
     26#include <X11/Xatom.h>
     27#include <X11/extensions/Xrandr.h>
     28
    1829/** @todo this should probably be replaced by something IPRT */
    1930/* For system() and WEXITSTATUS() */
     
    2233#include <sys/wait.h>
    2334#include <errno.h>
    24 
    25 #include <X11/Xlib.h>
    26 #include <X11/Xatom.h>
    27 
    28 #include <iprt/asm.h>
    29 #include <iprt/assert.h>
    30 #include <iprt/err.h>
    31 #include <iprt/file.h>
    32 #include <iprt/mem.h>
    33 #include <iprt/string.h>
    34 #include <iprt/thread.h>
    35 #include <VBox/log.h>
    36 #include <VBox/VMMDev.h>
    37 #include <VBox/VBoxGuestLib.h>
    38 
    39 #include "VBoxClient.h"
     35#include <limits.h>
     36#include <poll.h>
     37#include <time.h>
    4038
    4139/* TESTING: Dynamic resizing and mouse integration toggling should work
     
    4442 * virtual terminal while a user session is in place should disable dynamic
    4543 * resizing and cursor integration, switching back should re-enable them. */
    46 
    47 /** Most recent information received for a particular screen. */
    48 struct screenInformation
    49 {
    50     unsigned cx;
    51     unsigned cy;
    52     unsigned cBPP;
    53     unsigned x;
    54     unsigned y;
    55     bool fEnabled;
    56     bool fUpdateSize;
    57     volatile bool fUpdatePosition;
    58 };
    5944
    6045/** Display magic number, start of a UUID. */
     
    7358    /** The connection to the server. */
    7459    Display *pDisplay;
     60    /** The RandR extension base event number. */
     61    int cRREventBase;
    7562    /** Can we use version 1.2 or later of the RandR protocol here? */
    7663    bool fHaveRandR12;
     
    7966     * would it make sense to use absolute paths on all systems? */
    8067    const char *pcszXrandr;
    81     /** The number of screens we are currently aware of. */
    82     unsigned cScreensTracked;
    83     /** Array of information about different screens. */
    84     struct screenInformation *paScreenInformation;
     68    /** Was there a recent mode hint with no following root window resize, and
     69     *  if so, have we waited for a reasonable time? */
     70    time_t timeLastModeHint;
    8571};
    8672
    87 /** Thread to monitor and react to X server VT switches and exits. */
    88 static DECLCALLBACK(int) vboxClientMonitorThread(RTTHREAD self, void *pvUser)
    89 {
    90     struct DISPLAYSTATE *pState = (struct DISPLAYSTATE *)pvUser;
    91     Display *pDisplay;
    92     bool fHasVT = true;
    93 
    94     pDisplay = XOpenDisplay(NULL);
    95     if (!pDisplay)
    96         VBClFatalError(("Failed to open the X11 display\n"));
    97     XSelectInput(pDisplay, DefaultRootWindow(pDisplay), PropertyChangeMask);
     73static unsigned char *getRootProperty(struct DISPLAYSTATE *pState, const char *pszName,
     74                                      long cItems, Atom type)
     75{
     76    Atom actualType = None;
     77    int iFormat = 0;
     78    ulong cReturned = 0;
     79    ulong cAfter = 0;
     80    unsigned char *pData = 0;
     81   
     82    if (XGetWindowProperty(pState->pDisplay, DefaultRootWindow(pState->pDisplay),
     83                           XInternAtom(pState->pDisplay, pszName, 0), 0, cItems,
     84                           False /* delete */, type, &actualType, &iFormat,
     85                           &cReturned, &cAfter, &pData))
     86        return NULL;
     87    return pData;
     88}
     89
     90static void doResize(struct DISPLAYSTATE *pState)
     91{
     92    /** @note The xrandr command can fail if something else accesses RandR at
     93     *  the same time.  We just ignore failure for now as we do not know what
     94     *  someone else is doing. */
     95    if (!pState->fHaveRandR12)
     96    {
     97        char szCommand[256];
     98        unsigned char *pData;
     99
     100        pData = getRootProperty(pState, "VBOXVIDEO_PREFERRED_MODE", 1, XA_INTEGER);
     101        if (pData != NULL)
     102        {
     103            RTStrPrintf(szCommand, sizeof(szCommand), "%s -s %ux%u",
     104                        pState->pcszXrandr, ((unsigned long *)pData)[0] >> 16, ((unsigned long *)pData)[0] & 0xFFFF);
     105            system(szCommand);
     106            XFree(pData);
     107        }
     108    }
     109    else
     110    {
     111        const char szCommandBase[] =
     112            "%s --output VGA-0 --auto --output VGA-1 --auto --right-of VGA-0 "
     113               "--output VGA-2 --auto --right-of VGA-1 --output VGA-3 --auto --right-of VGA-2 "
     114               "--output VGA-4 --auto --right-of VGA-3 --output VGA-5 --auto --right-of VGA-4 "
     115               "--output VGA-6 --auto --right-of VGA-5 --output VGA-7 --auto --right-of VGA-6 "
     116               "--output VGA-8 --auto --right-of VGA-7 --output VGA-9 --auto --right-of VGA-8 "
     117               "--output VGA-10 --auto --right-of VGA-9 --output VGA-11 --auto --right-of VGA-10 "
     118               "--output VGA-12 --auto --right-of VGA-11 --output VGA-13 --auto --right-of VGA-12 "
     119               "--output VGA-14 --auto --right-of VGA-13 --output VGA-15 --auto --right-of VGA-14 "
     120               "--output VGA-16 --auto --right-of VGA-15 --output VGA-17 --auto --right-of VGA-16 "
     121               "--output VGA-18 --auto --right-of VGA-17 --output VGA-19 --auto --right-of VGA-18 "
     122               "--output VGA-20 --auto --right-of VGA-19 --output VGA-21 --auto --right-of VGA-20 "
     123               "--output VGA-22 --auto --right-of VGA-21 --output VGA-23 --auto --right-of VGA-22 "
     124               "--output VGA-24 --auto --right-of VGA-23 --output VGA-25 --auto --right-of VGA-24 "
     125               "--output VGA-26 --auto --right-of VGA-25 --output VGA-27 --auto --right-of VGA-26 "
     126               "--output VGA-28 --auto --right-of VGA-27 --output VGA-29 --auto --right-of VGA-28 "
     127               "--output VGA-30 --auto --right-of VGA-29 --output VGA-31 --auto --right-of VGA-30";
     128        char szCommand[sizeof(szCommandBase) + 256];
     129        RTStrPrintf(szCommand, sizeof(szCommand), szCommandBase, pState->pcszXrandr);
     130        system(szCommand);
     131    }
     132}
     133
     134/** Main loop: handle display hot-plug events, property updates (which can
     135 *  signal VT switches hot-plug in old X servers). */
     136static void runDisplay(struct DISPLAYSTATE *pState)
     137{
     138    Display *pDisplay = pState->pDisplay;
     139    long cValue = 1;
     140
     141    /* One way or another we want the preferred mode at server start-up. */
     142    doResize(pState);
     143    XSelectInput(pDisplay, DefaultRootWindow(pDisplay), PropertyChangeMask | StructureNotifyMask);
     144    if (pState->fHaveRandR12)
     145        XRRSelectInput(pDisplay, DefaultRootWindow(pDisplay), RRScreenChangeNotifyMask);
     146    /* Semantics: when VBOXCLIENT_STARTED is set, pre-1.3 X.Org Server driver
     147     * assumes that a client capable of handling mode hints will be present for the
     148     * rest of the X session.  If we crash things will not work as they should.
     149     * I thought that preferable to implementing complex crash-handling logic.
     150     */
     151    XChangeProperty(pState->pDisplay, DefaultRootWindow(pState->pDisplay), XInternAtom(pState->pDisplay, "VBOXCLIENT_STARTED", 0),
     152                    XA_INTEGER, 32, PropModeReplace, (unsigned char *)&cValue, 1);
     153    /* Interrupting this cleanly will be more work than making it robust
     154     * against spontaneous termination, especially as it will never get
     155     * properly tested, so I will go for the second. */
    98156    while (true)
    99157    {
    100158        XEvent event;
    101 
    102         XNextEvent(pDisplay, &event);
    103         /* This property is deleted when the server regains the virtual
    104          * terminal.  Force the main thread to call xrandr again, as old X
    105          * servers could not handle it while switched out. */
    106         if (pState->fHaveRandR12)
    107             continue;
    108         if (   event.type != PropertyNotify
    109             || event.xproperty.state != PropertyDelete
    110             || event.xproperty.window != DefaultRootWindow(pDisplay)
    111             || event.xproperty.atom != XInternAtom(pDisplay, "VBOXVIDEO_NO_VT", False))
    112             continue;
    113         LogRel(("VBoxClient/Display: entered virtual terminal.\n"));
    114         ASMAtomicWriteBool(&pState->paScreenInformation[0].fUpdateSize, true);
    115         VbglR3InterruptEventWaits();
     159        struct pollfd PollFd;
     160        int pollTimeOut = -1;
     161        int cFds;
     162
     163        /* Do not handle overflow. */
     164        if (pState->timeLastModeHint > 0 && pState->timeLastModeHint < INT_MAX - 2)
     165            pollTimeOut = 2 - (time(0) - pState->timeLastModeHint);
     166        PollFd.fd = ConnectionNumber(pDisplay);
     167        PollFd.events = POLLIN;  /* Hang-up is always reported. */
     168        XFlush(pDisplay);
     169        cFds = poll(&PollFd, 1, pollTimeOut >= 0 ? pollTimeOut * 1000 : -1);
     170        while (XPending(pDisplay))
     171        {
     172            XNextEvent(pDisplay, &event);
     173            /* This property is deleted when the server regains the virtual
     174             * terminal.  Force the main thread to call xrandr again, as old X
     175             * servers could not handle it while switched out. */
     176            if (   !pState->fHaveRandR12
     177                && event.type == PropertyNotify
     178                && event.xproperty.state == PropertyDelete
     179                && event.xproperty.window == DefaultRootWindow(pDisplay)
     180                && event.xproperty.atom == XInternAtom(pDisplay, "VBOXVIDEO_NO_VT", False))
     181                doResize(pState);
     182            if (   !pState->fHaveRandR12
     183                && event.type == PropertyNotify
     184                && event.xproperty.state == PropertyNewValue
     185                && event.xproperty.window == DefaultRootWindow(pDisplay)
     186                && event.xproperty.atom == XInternAtom(pDisplay, "VBOXVIDEO_PREFERRED_MODE", False))
     187                doResize(pState);
     188            if (   pState->fHaveRandR12
     189                && event.type == pState->cRREventBase + RRScreenChangeNotify)
     190                pState->timeLastModeHint = time(0);
     191            if (   event.type == ConfigureNotify
     192                && event.xproperty.window == DefaultRootWindow(pDisplay))
     193                pState->timeLastModeHint = 0;
     194        }
     195        if (cFds == 0 && pState->timeLastModeHint > 0)
     196            doResize(pState);
    116197    }
    117     return VINF_SUCCESS;  /* Should never be reached. */
    118 }
    119 
    120 static int startMonitorThread(struct DISPLAYSTATE *pState)
    121 {
    122     int rc;
    123 
    124     rc = RTThreadCreate(NULL, vboxClientMonitorThread, (void *)pState, 0, RTTHREADTYPE_INFREQUENT_POLLER, 0, "VT_MONITOR");
    125     if (rc != VINF_SUCCESS)
    126         VBClFatalError(("Failed to start the VT monitor thread, rc=%Rrc\n", rc));
    127     return VINF_SUCCESS;
    128198}
    129199
     
    133203    int status;
    134204
    135     /* Initialise the guest library. */
    136     int rc = VbglR3InitUser();
    137     if (RT_FAILURE(rc))
    138         VBClFatalError(("Failed to connect to the VirtualBox kernel service, rc=%Rrc\n", rc));
    139205    pState->pDisplay = XOpenDisplay(NULL);
    140206    if (!pState->pDisplay)
     207        return VERR_NOT_FOUND;
     208    if (!XRRQueryExtension(pState->pDisplay, &pState->cRREventBase, &status))
    141209        return VERR_NOT_FOUND;
    142210    pState->fHaveRandR12 = false;
     
    151219    if (WEXITSTATUS(status) == 0)
    152220        pState->fHaveRandR12 = true;
    153     pState->cScreensTracked = 0;
    154     pState->paScreenInformation = NULL;
    155221    return VINF_SUCCESS;
    156 }
    157 
    158 static void updateScreenInformation(struct DISPLAYSTATE *pState, unsigned cx, unsigned cy, unsigned cBPP, unsigned iDisplay,
    159                                     unsigned x, unsigned y, bool fEnabled, bool fUpdatePosition)
    160 {
    161     uint32_t i;
    162 
    163     if (iDisplay >= pState->cScreensTracked)
    164     {
    165         pState->paScreenInformation =
    166                 (struct screenInformation *)RTMemRealloc(pState->paScreenInformation,
    167                                                          (iDisplay + 1) * sizeof(*pState->paScreenInformation));
    168         if (!pState->paScreenInformation)
    169             VBClFatalError(("Failed to re-allocate screen information.\n"));
    170         for (i = pState->cScreensTracked; i < iDisplay + 1; ++i)
    171             RT_ZERO(pState->paScreenInformation[i]);
    172         pState->cScreensTracked = iDisplay + 1;
    173     }
    174     pState->paScreenInformation[iDisplay].cx = cx;
    175     pState->paScreenInformation[iDisplay].cy = cy;
    176     pState->paScreenInformation[iDisplay].cBPP = cBPP;
    177     pState->paScreenInformation[iDisplay].x = x;
    178     pState->paScreenInformation[iDisplay].y = y;
    179     pState->paScreenInformation[iDisplay].fEnabled = fEnabled;
    180     pState->paScreenInformation[iDisplay].fUpdateSize = true;
    181     pState->paScreenInformation[iDisplay].fUpdatePosition = fUpdatePosition;
    182 }
    183 
    184 static void updateSizeHintsProperty(struct DISPLAYSTATE *pState)
    185 {
    186     long *paSizeHints = (long *)RTMemTmpAllocZ(pState->cScreensTracked * sizeof(long) * 2);
    187     unsigned i;
    188 
    189     if (paSizeHints == NULL)
    190         VBClFatalError(("Failed to allocate size hint property memory.\n"));
    191     for (i = 0; i < pState->cScreensTracked; ++i)
    192     {
    193         if (pState->paScreenInformation[i].fEnabled)
    194             paSizeHints[2 * i] =   (pState->paScreenInformation[i].cx & 0x8fff) << 16
    195                                  | (pState->paScreenInformation[i].cy & 0x8fff);
    196         else if (pState->paScreenInformation[i].cx != 0 && pState->paScreenInformation[i].cy != 0)
    197             paSizeHints[2 * i] = -1;
    198         if (   pState->paScreenInformation[i].fEnabled
    199             && pState->paScreenInformation[i].fUpdatePosition)
    200             paSizeHints[2 * i + 1] =   (pState->paScreenInformation[i].x & 0x8fff) << 16
    201                                      | (pState->paScreenInformation[i].y & 0x8fff);
    202         else
    203             paSizeHints[2 * i + 1] = -1;
    204     }
    205     XChangeProperty(pState->pDisplay, DefaultRootWindow(pState->pDisplay), XInternAtom(pState->pDisplay, "VBOX_SIZE_HINTS", 0),
    206                     XA_INTEGER, 32, PropModeReplace, (unsigned char *)paSizeHints, pState->cScreensTracked * 2);
    207     XFlush(pState->pDisplay);
    208     RTMemTmpFree(paSizeHints);
    209 }
    210 
    211 static void notifyXServerRandR11(struct DISPLAYSTATE *pState)
    212 {
    213     char szCommand[256];
    214 
    215     /** @note The xrandr command can fail if something else accesses RandR at
    216      *  the same time.  We just ignore failure for now and let the user try
    217      *  again as we do not know what someone else is doing. */
    218     if (   pState->paScreenInformation[0].fUpdateSize
    219         && pState->paScreenInformation[0].cx > 0 && pState->paScreenInformation[0].cy > 0)
    220     {
    221         int ret;
    222 
    223         RTStrPrintf(szCommand, sizeof(szCommand), "%s -s %ux%u",
    224                     pState->pcszXrandr, pState->paScreenInformation[0].cx, pState->paScreenInformation[0].cy);
    225         ret = system(szCommand);
    226         LogRel(("VBoxClient/Display: executed \"%s\", returned %d.\n", szCommand, ret));
    227         pState->paScreenInformation[0].fUpdateSize = false;
    228     }
    229 }
    230 
    231 static void updateMouseCapabilities(struct DISPLAYSTATE *pState)
    232 {
    233     uint32_t fFeatures = 0;
    234     int rc;
    235 
    236     rc = VbglR3GetMouseStatus(&fFeatures, NULL, NULL);
    237 
    238     if (rc != VINF_SUCCESS)
    239         VBClFatalError(("Failed to get mouse status, rc=%Rrc\n", rc));
    240     XChangeProperty(pState->pDisplay, DefaultRootWindow(pState->pDisplay),
    241                     XInternAtom(pState->pDisplay, "VBOX_MOUSE_CAPABILITIES", 0), XA_INTEGER, 32, PropModeReplace,
    242                     (unsigned char *)&fFeatures, 1);
    243     XFlush(pState->pDisplay);
    244 }
    245 
    246 /**
    247  * Display change request monitor thread function.
    248  */
    249 static void runDisplay(struct DISPLAYSTATE *pState)
    250 {
    251     int rc;
    252     unsigned i, cScreensTracked;
    253     uint32_t fModeSet = false;
    254 
    255     LogRelFlowFunc(("\n"));
    256     rc = VbglR3VideoModeGetHighestSavedScreen(&cScreensTracked);
    257     if (rc != VINF_SUCCESS && rc != VERR_NOT_SUPPORTED)
    258         VBClFatalError(("Failed to get the number of saved screen modes, rc=%Rrc\n", rc));
    259     /* Make sure that we have an entry for screen 1 at least. */
    260     updateScreenInformation(pState, 1024, 768, 0, 1, 0, 0, true, false);
    261     if (rc == VINF_SUCCESS)
    262     {
    263         /* The "8" is for the sanity test below. */
    264         for (i = 0; i < RT_MAX(cScreensTracked + 1, 8); ++i)
    265         {
    266             unsigned cx = 0, cy = 0, cBPP = 0, x = 0, y = 0;
    267             bool fEnabled = true;
    268 
    269             rc = VbglR3RetrieveVideoMode(i, &cx, &cy, &cBPP, &x, &y,
    270                                          &fEnabled);
    271             /* Sanity test for VbglR3VideoModeGetHighestSavedScreen(). */
    272             if (i > cScreensTracked && rc != VERR_NOT_FOUND)
    273                 VBClFatalError(("Internal error retrieving the number of saved screen modes.\n"));
    274             if (rc == VINF_SUCCESS)
    275                 updateScreenInformation(pState, cx, cy, cBPP, i, x, y, fEnabled, true);
    276         }
    277     }
    278     /* Semantics: when VBOX_HAS_GRAPHICS is set, the X server driver assumes
    279      * that a client capable of forwarding mode hints will be present for the
    280      * rest of the X session.  If we crash things will not work as they should.
    281      * I thought that preferable to implementing complex crash-handling logic.
    282      */
    283     XChangeProperty(pState->pDisplay, DefaultRootWindow(pState->pDisplay), XInternAtom(pState->pDisplay, "VBOX_HAS_GRAPHICS", 0),
    284                     XA_INTEGER, 32, PropModeReplace, (unsigned char *)&fModeSet, 1);
    285     while (true)
    286     {
    287         uint32_t fEvents;
    288         updateMouseCapabilities(pState);
    289         updateSizeHintsProperty(pState);
    290         if (!pState->fHaveRandR12)
    291             notifyXServerRandR11(pState);
    292         rc = VbglR3WaitEvent(VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST | VMMDEV_EVENT_MOUSE_CAPABILITIES_CHANGED, RT_INDEFINITE_WAIT,
    293                              &fEvents);
    294         /* Interrupted is used to re-set the mode without changing it. */
    295         if (rc == VERR_INTERRUPTED)
    296             rc = VINF_SUCCESS;
    297         if (RT_FAILURE(rc))  /* VERR_NO_MEMORY? */
    298             VBClFatalError(("event wait failed, rc=%Rrc\n", rc));
    299         /* If it is a size hint, set the new size. */
    300         if (fEvents & VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST)
    301         {
    302             uint32_t cx = 0, cy = 0, cBPP = 0, iDisplay = 0, x = 0, y = 0;
    303             bool fEnabled = true, fUpdatePosition = true;
    304             VMMDevSeamlessMode Mode;
    305 
    306             rc = VbglR3GetDisplayChangeRequest(&cx, &cy, &cBPP, &iDisplay,
    307                                                &x, &y, &fEnabled,
    308                                                &fUpdatePosition, true);
    309             if (rc != VINF_SUCCESS)
    310                 VBClFatalError(("Failed to get display change request, rc=%Rrc\n",
    311                                 rc));
    312             else
    313                 LogRelFlowFunc(("Got size hint from host cx=%d, cy=%d, bpp=%d, iDisplay=%d, x=%d, y=%d fEnabled=%d\n",
    314                                 cx, cy, cBPP, iDisplay, x, y, fEnabled));
    315             if (iDisplay > INT32_MAX)
    316                 VBClFatalError(("Received a size hint for too high display number %u\n",
    317                             (unsigned) iDisplay));
    318             updateScreenInformation(pState, cx, cy, cBPP, iDisplay, x, y, fEnabled, fUpdatePosition);
    319             rc = VbglR3SeamlessGetLastEvent(&Mode);
    320             if (RT_FAILURE(rc))
    321                 VBClFatalError(("Failed to check seamless mode, rc=%Rrc\n", rc));
    322             if (Mode == VMMDev_Seamless_Disabled)
    323             {
    324                 rc = VbglR3SaveVideoMode(iDisplay, cx, cy, cBPP, x, y,
    325                                          fEnabled);
    326                 if (RT_FAILURE(rc) && rc != VERR_NOT_SUPPORTED)
    327                     VBClFatalError(("Failed to save size hint, rc=%Rrc\n", rc));
    328             }
    329         }
    330     }
    331222}
    332223
     
    354245    if (RT_FAILURE(rc))
    355246        return rc;
    356     rc = VbglR3CtlFilterMask(VMMDEV_EVENT_MOUSE_CAPABILITIES_CHANGED | VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, 0);
    357     if (RT_FAILURE(rc))
    358         VBClFatalError(("Failed to set filter mask, rc=%Rrc.\n", rc));
    359247    if (RT_SUCCESS(rc))
    360248        pSelf->mfInit = true;
     
    369257    if (!pSelf->mfInit)
    370258        return VERR_WRONG_ORDER;
    371     rc = startMonitorThread(pSelf);
    372     if (RT_FAILURE(rc))
    373         VBClFatalError(("Failed to start the VT monitor thread: %Rrc\n", rc));
    374259    runDisplay(pSelf);
    375260    return VERR_INTERNAL_ERROR;  /* "Should never reach here." */
    376 }
    377 
    378 static void cleanup(struct VBCLSERVICE **ppInterface)
    379 {
    380     NOREF(ppInterface);
    381     VbglR3CtlFilterMask(0, VMMDEV_EVENT_MOUSE_CAPABILITIES_CHANGED | VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST);
    382     VbglR3Term();
    383261}
    384262
     
    388266    init,
    389267    run,
    390     cleanup
     268    VBClServiceDefaultCleanup
    391269};
    392270
Note: See TracChangeset for help on using the changeset viewer.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette