0

New to Threading/Tasks and async processing... I have a process that attempts to acquire a file, however, if the file is pwd protected, the call to GetDocument never returns, and hangs the service.
In all of the "sample" code and tutorials I've looked at, the threaded process seems to be multiple lines wrapped within a loop of some sort, thus allowing for the ability to cancel within a while clause or whatever.
Would a Task be more suited due to a single line of code trying to be executed? Any other suggestions?

public class ServerClass
{
    public static PageData pageData;
    public static ImageDataProvider idp;
    public static Rendition rend;
    
    public static void AcquireRendition(object obj) 
    {
        CancellationToken ct = (CancellationToken)obj;
        while ((!ct.IsCancellationRequested) || (pageData == null)) 
        {
            pageData = idp.GetDocument(rend);   ////line failing to return
        }
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Todd Jones
  • 17
  • 1
  • 1
    What is this `ImageDataProvider` you speak of? How are you _supposed_ to cancel the `GetDocument` call? – Wyck Jan 11 '23 at 19:04
  • 1
    What .NET platform are you targeting? .NET Core or .NET Framework? – Theodor Zoulias Jan 11 '23 at 19:53
  • Wyck - this is a 3rd party API call. It is not intended to be cancelled as it "should" return the document as an object, however, I believe, even from an API call, is waiting on password entry (as when opened manually). Theodor - Framework 4.5 – Todd Jones Jan 11 '23 at 20:11
  • You might be interested in [this answer](https://stackoverflow.com/a/4692809/2791540). The last part of the answer goes over the solution that uses a loop. – John Wu Jan 12 '23 at 08:39

2 Answers2

1

Are you sure there isn't an API that allows you to pass a cancellation token? Are there alternative clients/libraries you could use? There is no generally safe way to stop a "hung" call. Even having a synchronous blocking I/O method is a very bad idea. Then you make it even worse by having the whole object as a static field - and accessed with no regard to sharing (is ImageDataProvider thread-safe?).

If you really can't get a better API, you will probably have to separate the whole thing into a different process - that you can terminate.

Tasks definitely cannot be terminated (they rely entirely on cooperative cancellation) and Threads are very unsafe and unreliable with "rude" aborts (again, cooperative cancellation is vastly preferred). You have no idea what kind of corruption that can cause, and making code that can handle asynchronous exceptions reasonably well is pretty much impossible. You certainly can't expect it from a library that doesn't even provide an opportunity for cooperative cancellation.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • Thanks Luaan. ImageDataProvider does not have provisions for cooperative cancellation unfortunately. How were you thinking about separating into a process that can be terminated? – Todd Jones Jan 11 '23 at 19:05
  • @ToddJones The basic idea is that you create another application and start it from your main program in its own process. Then you open some sort of communication channel (many to choose from) to communicate whatever data you need to work with - that entirely depends on what your application actually needs to do. However, based on your comments it seems that the library you're using actually expects to run in a normal desktop application with a potential to show a modal dialog, right? Which is going to cause serious trouble anyway (and probably means the problem is you're not pumping messages). – Luaan Jan 11 '23 at 20:19
  • actually no, there is no user input. The "GetDocument" call is intended to access a document file, MSWord, PDF, Tiff, MSExcel, etc. and provide an image version of it (Tiff) for combining with other files. There should always be a "document" object returned, however, the call hangs when a password protected file is acquired. I'm trying to figure out a way to kill that hanging call if it lasts for more than a set period of time. ...and to do it without the use of Abort. – Todd Jones Jan 11 '23 at 20:49
  • @ToddJones Is the library using the Office COM objects? Those are explicitly only designed for use as part of an UI application (and with all the proper COM setup at that, e.g. thread context). The COM objects absolutely _do_ invoke UI for some operations. You can actually check pretty easily with ILSpy. – Luaan Jan 11 '23 at 21:12
  • No, Hyland OnBase's Unity API. – Todd Jones Jan 11 '23 at 21:28
  • @ToddJones Ouch, software made for universities? That can get quite painful :D I'd go with separating this part into a separate process, if you can do that. If not, at least to test if the `GetDocument` hangs/fails before executing it in the application proper. And put a feature request for proper cancellation and asynchronous API. Also, try looking around for a `Cancel` method somewhere - those old-school applications would often expect you to run a synchronously blocking method in a separate thread and then cancel if it needed from the main thread. And make sure to check the documentation. – Luaan Jan 12 '23 at 09:28
0

You could try interrupting the potentially stuck thread with the Thread.Interrupt method. Below is a helper method RunInterruptible that observes a CancellationToken, interrupts the current thread in case the token is canceled, and propagates an OperationCanceledException. It has identical signature with the new API ControlledExecution.Run (.NET 7, source code):

public static void RunInterruptible(Action action,
    CancellationToken cancellationToken)
{
    if (action == null) throw new ArgumentNullException("action");
    cancellationToken.ThrowIfCancellationRequested();
    bool completedSuccessfully = false;
    try
    {
        using (CancellationTokenRegistration _ = cancellationToken
            .Register(arg => ((Thread)arg).Interrupt(), Thread.CurrentThread))
                action();
        completedSuccessfully = true;
        Thread.Sleep(0); // Last chance to observe the effect of Interrupt
    }
    catch (ThreadInterruptedException)
    {
        if (completedSuccessfully) return;
        cancellationToken.ThrowIfCancellationRequested();
        throw;
    }
}

Usage example:

RunInterruptible(() => pageData = idp.GetDocument(rend), ct);

In case the thread is not stuck in a waiting state, and instead it spins uncontrollably, the Thread.Interrupt will have no effect. In that case you could try using the RunAbortable method below. Please be sure that you are well aware of the implications of using the Thread.Abort method in a production environment, before adopting this drastic measure.

// .NET Framework only
[Obsolete("The RunAbortable method may prevent the execution of static" +
    " constructors and the release of managed or unmanaged resources," +
    " and may leave the application in an invalid state.")]
public static void RunAbortable(Action action,
    CancellationToken cancellationToken)
{
    if (action == null) throw new ArgumentNullException("action");
    cancellationToken.ThrowIfCancellationRequested();
    bool completedSuccessfully = false;
    try
    {
        using (CancellationTokenRegistration _ = cancellationToken
            .Register(arg => ((Thread)arg).Abort(), Thread.CurrentThread))
                action();
        completedSuccessfully = true;
        Thread.Sleep(0); // Last chance to observe the effect of Abort
    }
    catch (ThreadAbortException)
    {
        if (completedSuccessfully)
        {
            Thread.ResetAbort();
            return;
        }
        if (cancellationToken.IsCancellationRequested)
        {
            Thread.ResetAbort();
            throw new OperationCanceledException(cancellationToken);
        }
        throw; // Redundant, the ThreadAbortException is rethrown anyway.
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104