1

I've been trying to debug a problem I've had with loading a font from file (a .ttf file) with the java.nio.file.Paths import, using a combination of Paths.get() and loadFromFile(), but can't seem to find a solution.

Here's the problem code:

import java.io.IOException;
import java.nio.file.Paths;

    public final Font FONT_UI_BAR = new Font();
    public final Font FONT_FREESANS = new Font();

    try {
                System.out.println("We get here, before loading");
                FONT_UI_BAR.loadFromFile(Paths.get("Game/resources/UI/Font.ttf"));
                System.out.println("I've loaded the first font");
                FONT_FREESANS.loadFromFile(Paths.get("Game/resources/fonts/freesans/freesans.ttf"));
            } catch (IOException e2) {
                System.out.println("[ERROR] Could not load font");
                e.printStackTrace();
            }

The program gets to the first print statement but never reaches the second.

I did a thread dump and found there seems to be a deadlock within the code itself that occurs:

"main@1" prio=5 tid=0x1 nid=NA waiting
  java.lang.Thread.State: WAITING
      at jdk.internal.misc.Unsafe.park(Unsafe.java:-1)
      at java.util.concurrent.locks.LockSupport.park(LockSupport.java:194)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:885)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1039)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1345)
      at java.util.concurrent.Semaphore.acquire(Semaphore.java:318)
      at org.jsfml.internal.SFMLErrorCapture.start(Unknown Source:-1)
      at org.jsfml.graphics.Font.loadFromFile(Unknown Source:-1)
      at assets.FontCatalogue.<init>(FontCatalogue.java:32)
      at assets.FontCatalogue.get(FontCatalogue.java:15)
      at screens.HomeScreen.<init>(HomeScreen.java:51)
      at controllers.Game.<init>(Game.java:74)
      at Main.main(Main.java:16)

I'm not exactly sure how to proceed from here. My program won't function how I want it to without loading these fonts. I've tried loading other kinds of fonts and the problem persists.

Weirdly enough the problem didn't occur with loading other files in the past, such as this code:

TEMP_BG_01.loadFromFile(Paths.get("Game/resources/placeholder/full-moon_bg.png"));

It only started once I started trying to load these fonts.

Ideally I'd like to find a solution that still allows me to use this package because otherwise I have a fair amount of code to rewrite. Not the biggest deal but suggesting simply using another package should be a last resort.

Any ideas appreciated.

EDIT: Interesting to note this issue DOES NOT occur on a Windows machine, only my ubuntu-linux one. The rest of my team on Windows have no issues. Obviously one solution is to go and use Windows instead, but who wants to do that :p

EDIT #2: Turns out I'm now getting this error even with loading from the Texture class in JSFML. I have a feeling I updated my JVM when I updated my ubuntu sometime recently and that's suddenly introduced problems. I can't say for sure because I don't recall updating very recently, but it seems as of 21/02/2021 loading from file with JSFML causes a deadlock :/

MoonMan
  • 13
  • 3
  • What JVM are you running on? Per [the JSFML Github site](https://github.com/pdinklag/JSFML): "Development on JSFML has ceased in December 2015. This project is outdated and no longer maintained or supported in any way and is hosted here solely for reference." Unless you have an updated version of the product, or you are running it entirely on a 2015-vintage JVM, you're likely to encounter significant issues like this. Can you can simply not use those fonts? – Andrew Henle Feb 21 '21 at 02:38
  • @AndrewHenle doing java -version gives me this: openjdk version "11.0.10" 2021-01-19 OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.18.04) OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.18.04, mixed mode, sharing) so not the 2015-vintage. Unfortunately we have to use JSFML, it's a specification requirement (stupidly). you think going back to 2015-vintage would fix the issue? Most likely introduce new ones I bet though. I'm gonna try get a work around for the fonts but thought I'd post the question. Updated the post with an edit too. – MoonMan Feb 21 '21 at 02:44
  • Looks to me like @AndrewHenle might be right. The deadlock occurs in the library code which uses concurrency. There is a `jdk.internal` access in your stack trace, it is conceivable this exhibits different behavior than the JDK it was written for. You could look into the library code or fork it to suit your needs, however this also possibly collides with your specification requirement (?): https://github.com/pdinklag/JSFML/blob/master/src/java/org/jsfml/internal/SFMLErrorCapture.java – Koenigsberg Feb 21 '21 at 03:01
  • @Koenigsberg Looks like that's the explanation. Something so core will lack an easy workaround. I'll see about forking and what that might mean for the specification I've been given. Unfortunately I'll need some sort of way to load fonts because the game needs text, and other loading methods won't interface with the jsfml module I imagine. Thanks for the suggestion. – MoonMan Feb 21 '21 at 03:07
  • @Koenigsberg From a cursory examination of that `SFMLErrorCapture`, I see all kinds of potential issues in the `start()` method should a capture fail. If a capture fails, there's no cleanup code and the semaphore never gets released. Seems like a real poor design - async execution limited to only one at a time? What's the point? Just do it serially using one `synchronized` block. There should be some error logged for the initial failure, though, and solving that might allow this to work well enough. – Andrew Henle Feb 21 '21 at 13:22
  • This might be a thread blocking, but it is not a deadlock. Deadlock means you have two or more threads and two or more locks. – user207421 Feb 22 '21 at 00:12

1 Answers1

0

The first thing you need to do if you want to continue using JSFML is to determine the initial failure that leaves you in a deadlock state.

The code in the SFMLErrorCapture class is not robust. Should SFMLErrorCapture.start() fail in any way, it will leave the semaphore locked. I suspect this is the initial failure that breaks your application and leaves it deadlocked.

I'd recommend adding logging to the class, such as:

public static void start() {
    try {
        semaphore.acquire();
        capturing = true;

        nativeStart();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    } catch (Throwable t) {
        t.printStackTrace();

        // lots of other logging, probably to a file in /tmp

        // rethrow so original program flow isn't changed
        throw t;
    }
}

You might also want to add more logging to see if you get any InterruptedExceptions. That's another way the semaphore will never get released, but I don't think a simple upgrade is likely to trigger that kind of behavior change.

And, since it's also possible for finish() to fail in the same manner (such as if nativeFinish() returns null, which I'd think is also a likely failure mode...):

public static String finish() {
    try {
        final String str;
        if (capturing) {
            str = nativeFinish().trim();

            capturing = false;
            semaphore.release();
        } else {
            str = null;
        }

        return str;
    } catch (Throwable t) {
        t.printStackTrace();
        // lots of logging
        throw t;
    }
}

You might need to add throws Throwable to both methods.

This might also help:

public static String finish() {
    try {
        final String str;
        if (capturing) {
            // chaining calls is BAD CODE!!!!
            // Say hello to NPE if you insist cramming
            // multiple calls in one line!!
            str = nativeFinish();
            if ( str != null ) {
                str = str.trim();
            }

            capturing = false;
            semaphore.release();
        } else {
            str = null;
        }

        return str;
    }
}

Limiting asynchronous actions like this to one at a time is fundamentally broken. If only one action can happen at once, the code complexity added to do actions asynchronously is worse than wasted because such complex code is much more bug-prone and when bugs do happen that complexity makes unrecoverable failures much more likely.

If you can only do one at a time, just do the actions serially with one static synchronized method or in one synchronized block on a static final object.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56