UI-less Linux applications

24 posts / 0 new
Last post
jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45
UI-less Linux applications

Hi Jules,

The Juce messaging stuff is used at many places in JUCE code. Even in modules that are not related to graphical user interface (all the audio stuff for example). The problem on linux is that the messaging uses the X server to transmit internal juce messages, all none of this stuff works when you run it without a X server. I believe this is quite easily fixable by rewriting the juce_linux_Messaging.cpp file, in order to replace the XSendEvent stuff for internal juce messages by a custom message queue. The "juce_dispatchNextMessageOnSystemQueue" would then poll events from that queue, and from the X queue (when "display" is not null). That way we would be able to use more Juce classes in command-line apps.

This is something that I may try to do in the future, but I wanted to share my thoughts about this first.

kraken
kraken's picture
Offline
Last seen: 2 months 4 days ago
Joined: 9 Feb 2005 - 09:31

i would like to see how this can be done properly cause it will be more than useful ! and still you can use the juce power in the command line...

jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45

I do agree !

I wrote it yesterday, and so far it seems to work perfectly (no regressions, only improvements). Here is the drop-in replacement for juce_linux_Messaging.cpp

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-7 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330,
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/

#include "../../../juce_Config.h"

#include "linuxincludes.h"
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xresource.h>
#include <X11/Xutil.h>
#include "../../../src/juce_core/basics/juce_StandardHeader.h"

BEGIN_JUCE_NAMESPACE

#include "../../../src/juce_appframework/events/juce_MessageManager.h"
#include "../../../src/juce_core/threads/juce_WaitableEvent.h"
#include "../../../src/juce_core/threads/juce_Process.h"
#include "../../../src/juce_core/threads/juce_ScopedLock.h"
#include "../../../src/juce_core/containers/juce_OwnedArray.h"

#ifdef JUCE_DEBUG
  #define JUCE_DEBUG_XERRORS 1
#endif

Display* display = 0;     // This is also referenced from WindowDriver.cpp
Window juce_messageWindowHandle = None;

#define SpecialAtom         "JUCESpecialAtom"
#define BroadcastAtom       "JUCEBroadcastAtom"
#define SpecialCallbackAtom "JUCESpecialCallbackAtom"

static Atom specialId;
static Atom broadcastId;
static Atom specialCallbackId;

// This is referenced from WindowDriver.cpp
XContext improbableNumber;

// Defined in WindowDriver.cpp
extern void juce_windowMessageReceive (XEvent* event);

struct MessageThreadFuncCall
{
    MessageCallbackFunction* func;
    void* parameter;
    void* result;
    CriticalSection lock;
    WaitableEvent event;
};

struct InternalMessageQueue {
  CriticalSection mutex;
  OwnedArray<Message> queue;
  int fd[2];
public:
  InternalMessageQueue() {
    int ret = ::socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
    jassert(ret == 0);
  }
  ~InternalMessageQueue() {
    close(fd[0]);
    close(fd[1]);
  }
  void postMessage(Message *msg) { 
    ScopedLock lock(mutex); queue.add(msg); 
    unsigned char x = 0xff;
    write(fd[0], &x, 1);
  }
  bool isEmpty() { ScopedLock lock(mutex); return queue.size() == 0; }
  Message *popMessage() {
    ScopedLock lock(mutex);
    Message *m = 0;
    if (queue.size()) {
      unsigned char x;
      read(fd[1], &x, 1);
      m = queue.getUnchecked(0);
      queue.remove(0, false /* deleteObject */);
    }
    return m;
  }
  int getWaitHandle() { return fd[1]; }
};

static InternalMessageQueue *internal_message_queue;

static bool errorCondition = false;
static XErrorHandler oldErrorHandler = (XErrorHandler) 0;
static XIOErrorHandler oldIOErrorHandler = (XIOErrorHandler) 0;

// (defined in another file to avoid problems including certain headers in this one)
extern bool juce_isRunningAsApplication();
extern void juce_x11_handleSelectionRequest(XSelectionRequestEvent &evt);

// Usually happens when client-server connection is broken
static int ioErrorHandler (Display* display)
{
    DBG (T("ERROR: connection to X server broken.. terminating."));

    errorCondition = true;

    if (juce_isRunningAsApplication())
        Process::terminate();

    return 0;
}

// A protocol error has occurred
static int errorHandler (Display* display, XErrorEvent* event)
{
#ifdef JUCE_DEBUG_XERRORS
    char errorStr[64] = { 0 };
    char requestStr[64] = { 0 };

    XGetErrorText (display, event->error_code, errorStr, 64);

    XGetErrorDatabaseText (display,
                           "XRequest",
                           (const char*) String (event->request_code),
                           "Unknown",
                           requestStr,
                           64);

    DBG (T("ERROR: X returned ") + String (errorStr) + T(" for operation ") + String (requestStr));
#endif

    return 0;
}

static bool breakIn = false;

// Breakin from keyboard
static void sig_handler (int sig)
{
    if (sig == SIGINT)
    {
        breakIn = true;
        return;
    }

    static bool reentrant = false;

    if (reentrant == false)
    {
        reentrant = true;

        // Illegal instruction
        fflush (stdout);
        Logger::outputDebugString ("ERROR: Program executed illegal instruction.. terminating");

        errorCondition = true;

        if (juce_isRunningAsApplication())
            Process::terminate();
    }
    else
    {
        if (juce_isRunningAsApplication())
            exit(0);
    }
}

//==============================================================================
void MessageManager::doPlatformSpecificInitialisation()
{
    // Initialise xlib for multiple thread support
    static bool initThreadCalled = false;

    if (! initThreadCalled)
    {
        if (! XInitThreads())
        {
            // This is fatal!  Print error and closedown
            Logger::outputDebugString ("Failed to initialise xlib thread support.");

            if (juce_isRunningAsApplication())
                Process::terminate();

            return;
        }

        initThreadCalled = true;
    }

    // This is called if the client/server connection is broken
    oldIOErrorHandler = XSetIOErrorHandler (ioErrorHandler);

    // This is called if a protocol error occurs
    oldErrorHandler = XSetErrorHandler (errorHandler);

    // Install signal handler for break-in
    struct sigaction saction;
    sigset_t maskSet;
    sigemptyset (&maskSet);
    saction.sa_handler = sig_handler;
    saction.sa_mask = maskSet;
    saction.sa_flags = 0;
    sigaction (SIGINT, &saction, NULL);

#ifndef _DEBUG
    // Setup signal handlers for various fatal errors
    sigaction (SIGILL, &saction, NULL);
    sigaction (SIGBUS, &saction, NULL);
    sigaction (SIGFPE, &saction, NULL);
    sigaction (SIGSEGV, &saction, NULL);
    sigaction (SIGSYS, &saction, NULL);
#endif

    internal_message_queue = new InternalMessageQueue();

    String displayName (getenv ("DISPLAY"));
    if (displayName.isEmpty())
        displayName = T(":0.0");

    display = XOpenDisplay (displayName);

    if (display == 0)
    {
        // This is no more fatal! check if display was opened with Desktop::isAvailable()
        /*Logger::outputDebugString ("Failed to open the X display.");

        if (juce_isRunningAsApplication())
            Process::terminate();*/

        return;
    }

    // Get defaults for various properties
    int screen = DefaultScreen (display);
    Window root = RootWindow (display, screen);
    Visual* visual = DefaultVisual (display, screen);

    // Create atoms for our ClientMessages (these cannot be deleted)
    specialId = XInternAtom (display, SpecialAtom, false);
    broadcastId = XInternAtom (display, BroadcastAtom, false);
    specialCallbackId = XInternAtom (display, SpecialCallbackAtom, false);

    // Create a context to store user data associated with Windows we
    // create in WindowDriver
    improbableNumber = XUniqueContext();

    // We're only interested in client messages for this window
    // which are always sent
    XSetWindowAttributes swa;
    swa.event_mask = NoEventMask;

    // Create our message window (this will never be mapped)
    juce_messageWindowHandle = XCreateWindow (display, root,
                                              0, 0, 1, 1, 0, 0, InputOnly,
                                              visual, CWEventMask, &swa);
}

void MessageManager::doPlatformSpecificShutdown()
{
    if (internal_message_queue) {
      deleteAndZero(internal_message_queue);
    }
    if (errorCondition == false && display)
    {
        XDestroyWindow (display, juce_messageWindowHandle);
        XCloseDisplay (display);

        // reset pointers
        juce_messageWindowHandle = 0;
        display = 0;

        // Restore original error handlers
        XSetIOErrorHandler (oldIOErrorHandler);
        oldIOErrorHandler = 0;
        XSetErrorHandler (oldErrorHandler);
        oldErrorHandler = 0;
    }
}

bool juce_postMessageToSystemQueue (void* message)
{
    if (errorCondition)
        return false;

    internal_message_queue->postMessage((Message*)message);
    return true;
}

bool juce_postMessageToX11Queue(void *message) 
{
    XClientMessageEvent clientMsg;
    clientMsg.display = display;
    clientMsg.window = juce_messageWindowHandle;
    clientMsg.type = ClientMessage;
    clientMsg.format = 32;
    clientMsg.message_type = specialId;
#if JUCE_64BIT
    clientMsg.data.l[0] = (long) (0x00000000ffffffff & (((uint64) message) >> 32));
    clientMsg.data.l[1] = (long) (0x00000000ffffffff & (long) message);
#else
    clientMsg.data.l[0] = (long) message;
#endif

    XSendEvent (display, juce_messageWindowHandle, false,
                NoEventMask, (XEvent*) &clientMsg);

    XFlush (display); // This is necessary to ensure the event is delivered
    return true;
}

void MessageManager::broadcastMessage (const String& value) throw()
{
    /* TODO */
}

struct InternalCallFunctionMessage : public Message {
    MessageThreadFuncCall *ctx;
    InternalCallFunctionMessage(MessageThreadFuncCall *ctx_) : ctx(ctx_) {
        intParameter1 = 0x73774623;
    }
};

void* MessageManager::callFunctionOnMessageThread (MessageCallbackFunction* func,
                                                   void* parameter)
{
    void* retVal = 0;

    if (! errorCondition)
    {
        if (! isThisTheMessageThread())
        {
            static MessageThreadFuncCall messageFuncCallContext;

            const ScopedLock sl (messageFuncCallContext.lock);

            messageFuncCallContext.func = func;
            messageFuncCallContext.parameter = parameter;
            internal_message_queue->postMessage(new InternalCallFunctionMessage(&messageFuncCallContext));

            // Wait for it to complete before continuing
            messageFuncCallContext.event.wait();

            retVal = messageFuncCallContext.result;
        }
        else
        {
            // Just call the function directly
            retVal = func (parameter);
        }
    }

    return retVal;
}

/* wait for an event (either XEvent, or an internal Message) */
static bool juce_sleepUntilEvent(int timeout_ms)
{
    if (display && XPending(display)) return true;
    if (!internal_message_queue->isEmpty()) return true;

    struct timeval tv;
    tv.tv_sec=0;
    tv.tv_usec=timeout_ms * 1000;
    int fd0   = internal_message_queue->getWaitHandle();
    int fdmax = fd0;

    fd_set readset;
    FD_ZERO(&readset);
    FD_SET(fd0, &readset);
    if (display) {
      int fd1 = XConnectionNumber(display);
      FD_SET(fd1, &readset);
      fdmax = jmax(fd0, fd1);
    }
    int ret = select( fdmax+1, &readset, 0, 0, &tv );
    
    return (ret > 0); // ret <= 0 if error or timeout
}

/* handle next XEvent (if any) */
static bool juce_dispatchNextXEvent()
{
    if (!display || !XPending(display)) return false;
    XEvent evt;
    XNextEvent(display, &evt);    
    /* requires my version of juce_linux_Clipboard.cpp */
    //if (evt.type == SelectionRequest && evt.xany.window == juce_messageWindowHandle) {
    //  juce_x11_handleSelectionRequest(evt.xselectionrequest);
    //} else if (evt.type == SelectionClear && evt.xany.window == juce_messageWindowHandle) {   
    //  /* another window just grabbed the selection -- we just don't care */ 
    //} else 
    if (evt.xany.window != juce_messageWindowHandle) {
      juce_windowMessageReceive (&evt);
    }
    return true;
}

/* handle next internal Message (if any) */
static bool juce_dispatchNextInternalMessage()
{
    if (internal_message_queue->isEmpty()) return false;
    Message *m = internal_message_queue->popMessage();
    InternalCallFunctionMessage *cfm = 0;
    /* handle function call */
    if (m->intParameter1 == 0x73774623 && (cfm = dynamic_cast<InternalCallFunctionMessage*>(m))) {
      MessageThreadFuncCall* const call = cfm->ctx;
      MessageCallbackFunction* func = call->func;
      call->result = (*func) (call->parameter);
      call->event.signal();
      deleteAndZero(m);
    } else {
      /* handle "normal" messages */
      MessageManager::getInstance()->deliverMessage(m);
    }
    return true;
}

/* this function expects that it will NEVER be called simultaneously for two concurrent threads */
bool juce_dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages)
{
    while (1) {
      if (errorCondition)
        return false;
      
      if (breakIn) {
        errorCondition = true;
        
        if (juce_isRunningAsApplication())
          Process::terminate();
        
        return false;
      }
      
      static int event_cnt=0;
      ++event_cnt;
      /* the purpose here is to give either priority to XEvents or
         to internal messages This is necessary to keep a "good"
         behaviour when the cpu is overloaded
      */
      if ((event_cnt & 1)) {
        if (juce_dispatchNextXEvent() || juce_dispatchNextInternalMessage()) return true;
      } else {
        if (juce_dispatchNextInternalMessage() || juce_dispatchNextXEvent()) return true;
      }
      
      if (returnIfNoPendingMessages) // early exit
        return false;
      
      juce_sleepUntilEvent(4000 /* milliseconds */); // the timeout is to be on the safe side, but it does not seem to be useful
    }
    return true;
}

END_JUCE_NAMESPACE

(I don't claim any copyright on anything in this file, so anyone is free to do whatever they want with my changes)

As you can see , it is really simple indeed. Just an OwnedArray for the queue of internal juce messages, a pair of sockets for notifying the event loop. The juce_dispatchNextMessageOnSystemQueue now waits for 2 types of events instead of only XEvents. The only significant change, is that the order of events is not the same as it used to be since there are two events queues instead of one, and no queue should starve the other when they are both flooded with events (see the comment about the "event_cnt" variable). I have built the juce demo, the juce plugin host, and my own application and did not notice any regression.

A side effect is that is fixes the issue that I mentionned here:
http://www.rawmaterialsoftware.com/juceforum/viewtopic.php?t=4000 (I have spent days on this one)

(As a side note, I now believe that this issue was due to the non-threadsafe use of X11 functions -- as far as I understand, when more that one thread may simultaneously use the same 'display*' object, each X function call should be protected by XLockDisplay()/XUnlockDisplay() . By default these function do nothing, except when XInitThreads has been called before XOpenDisplay)

Other small changes that I had to do to allow my app to run "headless", is to add a function Desktop::isAvailable() function which always return juce_isRunningAsApplication() on windows and mac, and also tests if display != 0 on linux:

bool Desktop::isAvailable() throw() {
  return juce_isRunningAsApplication() && display != 0;
}

And to allow the app to startup when no DISPLAY is available, I had to add "if (!display) return;" at the beginning of juce_updateMultiMonitorInfo .

(this is not the part i am most proud of , to be honest)

Jules: when you have time, (and after a few weeks in order to let me test this a bit more), I hope that you will review this change to the juce_linux_Messaging.cpp file. I think that fixing the thread-safety problem of the original juce_postMessageToSystemQueue is enough to justify it.

jules
Offline
Last seen: 4 hours 31 min ago
Joined: 29 Apr 2013 - 18:37

Quote:
Jules: when you have time, (and after a few weeks in order to let me test this a bit more), I hope that you will review this change to the juce_linux_Messaging.cpp file. I think that fixing the thread-safety problem of the original juce_postMessageToSystemQueue is enough to justify it.

Cool - thanks, keep me posted if you make any changes!

jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45

It looks like there is an issue with repaints, some elements of the gui are sometimes repainted at the wrong locations. There is probably something between the LinuxRepaintManager and the LinuxComponentPeer that assumes a relative ordering between X11 messages and juce messages. Too bad...

jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45

Now I'm understanding the issue with repaints. It happens only when the XSHM extension is used. The LinuxRepaintManager is then using a shared memory ximage to hold the stuff that is repainted. What happens is that XShmPutImage does not display the content of the image immediately. So if one modifies the content of the image buffer just *after* XShmPutImage has been called, then it is the modified content that will be drawn !

Since the LinuxRepaintManager::performAnyPendingRepaintsNow() re-uses the same xshm image to draw various parts of the window, sometimes (on slow computers, with a slow Xorg) it happens that one overwrites the content of the image before it has been displayed with another content.

I see two solutions:
- (solution 1) the current test that determines if one can re-use the current ximage checks only that the drawing area can fit in the image:

 if (image == 0 || image->getWidth() < totalArea.getWidth()
                        || image->getHeight() < totalArea.getHeight()) {
   //delete image and create a fresh one
   ...
}

if could be replaced by a check that the drawing area lies *inside* the area affected to the image:

if (image == 0 || !Rectangle(image_x, image_y, image->getWidth(), image->getHeight()).contains(totalArea)) {
   //delete image and create a fresh one
  ...
  image_x = totalArea.getX();
  image_y = totalArea.getY();
}

that way the race condition will still occur, but no part of the image will get drawn at the wrong place.

- (solution 2) do not allow performAnyPendingRepaintsNow to execute until the XShmPutImage has completed. That is done by setting to True the last argument of the call to XShmPutImage , and check for the completion event in the window event handler:

   void handleWindowMessage (XEvent* event)
    {
        int CompletionNotify = XShmGetEventBase(display);

        switch (event->xany.type)
        {
           /* ...(snip).... */

            default:
              if (event->xany.type == CompletionNotify) {
                repainter->shmCompleted();
              }
                break;
        }

I believe this later approach is better from a performance point of view. This is the one I am testing right now.

I don't know why this bug does not show up with the regular the juce_linux_Messaging.cpp , I believe it should also happen.

valley
Offline
Last seen: 12 hours 31 min ago
Joined: 4 Sep 2004 - 04:32

jpo wrote:

I don't know why this bug does not show up with the regular the juce_linux_Messaging.cpp , I believe it should also happen.

I actually think I may have seen it happen, but it was on an old Mandrake box now retired. At the time it was one of those "I've never seen this happen before, and if I never see it happen again then I'm going to pretend I didn't see it happen here" moments.

Since it only ever happened once, I chose not to care. IIRC, and this was a while ago, a list box was partially drawn at some arbitrary location on screen. Causing a repaint made it go away, and that was end of it.

--
why?

jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45

Ah, good to know that it does happen :)

jules
Offline
Last seen: 4 hours 31 min ago
Joined: 29 Apr 2013 - 18:37

Thanks - let me know if your fix seems to be stable, and I'll add it.

jpo
Offline
Last seen: 14 hours 44 min ago
Joined: 20 Mar 2008 - 13:45

Quote:
As a side note, I now believe that this issue was due to the non-threadsafe use of X11 functions -- as far as I understand, when more that one thread may simultaneously use the same 'display*' object, each X function call should be protected by XLockDisplay()/XUnlockDisplay() . By default these function do nothing, except when XInitThreads has been called before XOpenDisplay

Mmmm now I changed my mind, it looks like, in a perfect world, calling XInitThread *is* sufficient, and XLockDisplay / XUnlockDisplay should be used only when one need to execute atomically a block of X calls. The locking issues I am seeing on some machines (a debian lenny for example) are due to bugs in XCB 1.1, which is unfortunately the version used by debian lenny, and ubuntu 8.04 ..

https://bugs.freedesktop.org/show_bug.cgi?id=16617
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=463159

Sometimes it is also causing a crash, just like in:
https://bugs.launchpad.net/ubuntu/+source/libx11/+bug/232476/comments/3

(in case of crash (very very hard to reproduce indeed, I have seen it two or three times only so far), the backtrace shows that it is due to the call to MessageManager::inactivityCheckCallback(); in InternalTimerThread, which ends up calling XQueryPointer ).

I don't understand why debian lenny and ubuntu LTQ 8.04 are still using xcb 1.1 since it is so much broken.

X-Ryl669
Offline
Last seen: 1 month 1 day ago
Joined: 24 Apr 2005 - 17:30

So finally, does the code on the first post works ?

X-Ryl669

Pages