Beispiel #1
0
 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;
 }
Beispiel #2
0
        // 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);
        }
Beispiel #3
0
        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);
        }
Beispiel #4
0
        /// <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();
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
        // 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);
        }
Beispiel #7
0
        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);
            }
        }