public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook) { var request = new ApiRequest(info, collectionSettings, currentBook); try { if(Program.RunningUnitTests) { endpointRegistration.Handler(request); } else { var formForSynchronizing = Application.OpenForms.Cast<Form>().Last(); if (endpointRegistration.HandleOnUIThread && formForSynchronizing.InvokeRequired) { formForSynchronizing.Invoke(endpointRegistration.Handler, request); } else { endpointRegistration.Handler(request); } } if(!info.HaveOutput) { throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString())); } } catch (Exception e) { SIL.Reporting.ErrorReport.ReportNonFatalExceptionWithMessage(e, info.RawUrl); return false; } return true; }
// If you just Invoke(), the stack trace of any generated exception gets lost. // The stacktrace instead just ends with the invoke(), which isn't useful. So here we wrap // the call to the handler in a delegate that catches the exception and saves it // in our local scope, where we can then use it for error reporting. private static bool InvokeWithErrorHandling(EndpointRegistration endpointRegistration, Form formForSynchronizing, ApiRequest request) { Exception handlerException = null; BloomServer._theOneInstance.RegisterThreadBlocking(); // This will block until the UI thread is done invoking this. formForSynchronizing.Invoke(new Action <ApiRequest>((req) => { try { endpointRegistration.Handler(req); } catch (Exception error) { handlerException = error; } }), request); BloomServer._theOneInstance.RegisterThreadUnblocked(); if (handlerException != null) { ExceptionDispatchInfo.Capture(handlerException).Throw(); } return(true); }
public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook) { var request = new ApiRequest(info, collectionSettings, currentBook); try { if (Program.RunningUnitTests) { endpointRegistration.Handler(request); } else { var label = ""; if (endpointRegistration.DoMeasure && (endpointRegistration.FunctionToGetLabel != null)) { label = endpointRegistration.FunctionToGetLabel(); } else if (endpointRegistration.DoMeasure) { label = endpointRegistration.MeasurementLabel; } using (endpointRegistration.DoMeasure ? PerformanceMeasurement.Global?.Measure(label) : null) { // Note: If the user is still interacting with the application, openForms could change and become empty var formForSynchronizing = Application.OpenForms.Cast <Form>().LastOrDefault(); if (endpointRegistration.HandleOnUIThread && formForSynchronizing != null && formForSynchronizing.InvokeRequired) { InvokeWithErrorHandling(endpointRegistration, formForSynchronizing, request); } else { endpointRegistration.Handler(request); } } } if (!info.HaveOutput) { throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString())); } } catch (System.IO.IOException e) { var shortMsg = String.Format(L10NSharp.LocalizationManager.GetDynamicString("Bloom", "Errors.CannotAccessFile", "Cannot access {0}"), info.RawUrl); var longMsg = String.Format("Bloom could not access {0}. The file may be open in another program.", info.RawUrl); NonFatalProblem.Report(ModalIf.None, PassiveIf.All, shortMsg, longMsg, e); request.Failed(shortMsg); return(false); } catch (Exception e) { //Hard to reproduce, but I got one of these supertooltip disposal errors in a yellow box //while switching between publish tabs (e.g. /bloom/api/publish/android/cleanup). //I don't think these are worth alarming the user about, so let's be sensitive to what channel we're on. NonFatalProblem.Report(ModalIf.Alpha, PassiveIf.All, "Error in " + info.RawUrl, exception: e); request.Failed("Error in " + info.RawUrl); return(false); } return(true); }
/// <summary> /// Get called when a client (i.e. javascript) does an HTTP api call /// </summary> /// <param name="pattern">Simple string or regex to match APIs that this can handle. This must match what comes after the ".../api/" of the URL</param> /// <param name="handler">The method to call</param> /// <param name="handleOnUiThread">If true, the current thread will suspend until the UI thread can be used to call the method. /// This deliberately no longer has a default. It's something that should be thought about. /// Making it true can kill performance if you don't need it (BL-3452), and complicates exception handling and problem reporting (BL-4679). /// There's also danger of deadlock if something in the UI thread is somehow waiting for this request to complete. /// But, beware of race conditions or anything that manipulates UI controls if you make it false.</param> /// <param name="requiresSync">True if the handler wants the server to ensure no other thread is doing an api /// call while this one is running. This is our default behavior, ensuring that no API request can interfere with any /// other in any unexpected way...essentially all Bloom's data is safe from race conditions arising from /// server threads manipulating things on background threads. However, it makes it impossible for a new /// api call to interrupt a previous one. For example, when one api call is creating an epub preview /// and we get a new one saying we need to abort that (because one of the property buttons has changed), /// the epub that is being generated is obsolete and we want the new api call to go ahead so it can set a flag /// to abort the one in progress. To avoid race conditions, api calls that set requiresSync false should be kept small /// and simple and be very careful about touching objects that other API calls might interact with.</param> public EndpointRegistration RegisterEndpointHandler(string pattern, EndpointHandler handler, bool handleOnUiThread, bool requiresSync = true) { var registration = new EndpointRegistration() { Handler = handler, HandleOnUIThread = handleOnUiThread, RequiresSync = requiresSync, MeasurementLabel = pattern, // can be overridden... this is just a default }; _endpointRegistrations[pattern.ToLowerInvariant().Trim(new char[] { '/' })] = registration; return(registration); // return it so the caller can say RegisterEndpointHandler().Measurable(); }
public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook) { var request = new ApiRequest(info, collectionSettings, currentBook); try { if (Program.RunningUnitTests) { endpointRegistration.Handler(request); } else { var formForSynchronizing = Application.OpenForms.Cast <Form>().Last(); if (endpointRegistration.HandleOnUIThread && formForSynchronizing.InvokeRequired) { InvokeWithErrorHandling(endpointRegistration, formForSynchronizing, request); } else { endpointRegistration.Handler(request); } } if (!info.HaveOutput) { throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString())); } } catch (System.IO.IOException e) { var shortMsg = String.Format(L10NSharp.LocalizationManager.GetDynamicString("Bloom", "Errors.CannotAccessFile", "Cannot access {0}"), info.RawUrl); var longMsg = String.Format("Bloom could not access {0}. The file may be open in another program.", info.RawUrl); NonFatalProblem.Report(ModalIf.None, PassiveIf.All, shortMsg, longMsg, e); return(false); } catch (Exception e) { SIL.Reporting.ErrorReport.ReportNonFatalExceptionWithMessage(e, info.RawUrl); return(false); } return(true); }
// If you just Invoke(), the stack trace of any generated exception gets lost. // The stacktrace instead just ends with the invoke(), which isn't useful. So here we wrap // the call to the handler in a delegate that catches the exception and saves it // in our local scope, where we can then use it for error reporting. private static bool InvokeWithErrorHandling(EndpointRegistration endpointRegistration, Form formForSynchronizing, ApiRequest request) { Exception handlerException = null; formForSynchronizing.Invoke(new Action <ApiRequest>((req) => { try { endpointRegistration.Handler(req); } catch (Exception error) { handlerException = error; } }), request); if (handlerException != null) { ExceptionDispatchInfo.Capture(handlerException).Throw(); } return(true); }
private bool ProcessRequest(EndpointRegistration endpointRegistration, IRequestInfo info, string localPathLc) { if (endpointRegistration.RequiresSync) { // A single synchronization object won't do, because when processing a request to create a thumbnail or update a preview, // we have to load the HTML page the thumbnail is based on, or other HTML pages (like one used to figure what's // visible in a preview). If the page content somehow includes // an api request (api/branding/image is one example), that request will deadlock if the // api/pageTemplateThumbnail request already has the main lock. // Another case is the Bloom Reader preview, where the whole UI is rebuilt at the same time as the preview. // This leads to multiple api requests racing with the preview one, and it was possible for all // the server threads to be processing these and waiting for SyncObject while the updatePreview // request held the lock...and the request for the page that would free the lock was sitting in // the queue, waiting for a thread. // To the best of my knowledge, there's no shared data between the thumbnailing and preview processes and any // other api requests, so it seems safe to have one lock that prevents working on multiple // thumbnails/previews at the same time, and one that prevents working on other api requests at the same time. var syncOn = SyncObj; if (localPathLc.StartsWith("api/pagetemplatethumbnail", StringComparison.InvariantCulture) || localPathLc == "api/publish/android/thumbnail" || localPathLc == "api/publish/android/updatepreview") { syncOn = ThumbnailsAndPreviewsSyncObj; } else if (localPathLc.StartsWith("api/i18n/")) { syncOn = I18NLock; } // Basically what lock(syncObj) {} is syntactic sugar for (see its documentation), // but we wrap RegisterThreadBlocking/Unblocked around acquiring the lock. // We need the more complicated structure because we would like RegisterThreadUnblocked // to be called immediately after acquiring the lock (notably, before Handle() is called), // but we also want to handle the case where Monitor.Enter throws an exception. bool lockAcquired = false; try { // Try to acquire lock BloomServer._theOneInstance.RegisterThreadBlocking(); try { // Blocks until it either succeeds (lockAcquired will then always be true) or throws (lockAcquired will stay false) Monitor.Enter(syncOn, ref lockAcquired); } finally { BloomServer._theOneInstance.RegisterThreadUnblocked(); } // Lock has been acquired. ApiRequest.Handle(endpointRegistration, info, CurrentCollectionSettings, _bookSelection.CurrentSelection); // Even if ApiRequest.Handle() fails, return true to indicate that the request was processed and there // is no further need for the caller to continue trying to process the request as a filename. // See https://issues.bloomlibrary.org/youtrack/issue/BL-6763. return(true); } finally { if (lockAcquired) { Monitor.Exit(syncOn); } } } else { // Up to api's that request no sync to do things right! ApiRequest.Handle(endpointRegistration, info, CurrentCollectionSettings, _bookSelection.CurrentSelection); // Even if ApiRequest.Handle() fails, return true to indicate that the request was processed and there // is no further need for the caller to continue trying to process the request as a filename. // See https://issues.bloomlibrary.org/youtrack/issue/BL-6763. return(true); } }