question

JosHuybrighs-1920 avatar image
0 Votes"
JosHuybrighs-1920 asked ·

Does AppService connection supports 2-way communication between host and fulltrust process with BackgroundWorker in desktop bridge app?

Microsoft only supports AppService as communication vehicle between a uwp host and a win32 fulltrust process both packaged in a single desktop bridge app.
It is a client-server concept (with the client running in the fulltrust process) but also allows the server to spontaneously send messages as long as the connection is still active.
I am making use of this as follows:

  • The win32 process when launched by the uwp host creates an appservice connection with the host, sends a message and reacts on the response. When done with the response it returns a message (or multiple messages) to the host and then waits for a next message from the host.

  • Some time later the uwp host (triggered by the user) sends a message to the already active fulltrust process.

  • The fulltrust process again reacts, responds with a message and waits again for the next request.

  • This goes on until a specific request from the host causes the win32 process to exit.

A problem occurs when the fulltrust process must execute a host request in a BackgroundWorker thread within which it must send progress messages to the host. The worker thread is necessary in this specific case because execution of the request can take a long time and in my app it must be possible for the host to keep on sending messages to the fulltrust process.
The fulltrust process successfully sends its progress messages to the uwp side. But, at some point in time when the uwp host also sends a message, the background thread from then on is not able anymore to send its progress messages.
I tested the sending in 2 ways:

  1. By straigth sending on the appservice connection, i.e. by using await connection.SendMessageAsync(msg).

  2. By sending a response on the last received request, i.e. by using await request.SendResponseAsync(msg).

With the 1st approach, the call to SendMessageAsync hangs for all send requests and later (3 minutes) when the background task stops all calls return with 'Failure'.
With the 2nd approach, the call also hangs the first time but then successive send requests all generate an exception "A method was called at an unexpected time". That probably makes sense since the 1st call has not returned yet.

Thus, my question: How can I run a full duplex communication using an App Service connection considering that I must be able to send messages from within a BackgroundWorker?

I tried replacing the app service connection with a local loop socket channel (with the socket server running in the win32 space). That works perfectly in a debug environment but when I publish the desktop bridge app on Windows Store it doesn't work when other people install the app from the Store.

So, I am afraid I have to keep going the App Service way.

Looking forward to any help on this.

windows-uwpwindows-desktop-bridge
· 1
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Can you show a simple sample about what you have done in win32 process and uwp app for us to test? This will help us better understand your scenario.

0 Votes 0 ·
JosHuybrighs-1920 avatar image
0 Votes"
JosHuybrighs-1920 answered ·

The problem turned out to be caused by the uwp host, not the win32 fulltrust client.
Instead of directly sending messages on an AppService connection I had a sender queue to post messages on and a task running on the ThreadPool to handle requests on the queue and send the messages in the background.

Don't do that!

Don't understand why but it causes strange behavior:

  • When a message is sent after the host has received messages from the client, further client messages are not received anymore and stay queued up on the client side.

  • The next message that is sent causes all waiting messages to be received, but then again new messages stay blocked.

Solution:
Don't send messages from within a ThreadPool task but instead call connection.SendMessageAsync(msg) immediately from within the UI thread.

·
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

JosHuybrighs-1920 avatar image
0 Votes"
JosHuybrighs-1920 answered ·

@FayWang-MSFT

The most important code in the win32 fulltrust process looks as follows:
(RunAsync is invoked in the ApplicationContext that is started in 'main')

 async Task RunAsync()
 {
     _connection = new AppServiceConnection();
     _connection.PackageFamilyName = Package.Current.Id.FamilyName;
     _connection.AppServiceName = "SyncFolderWin32AppService";
     _connection.RequestReceived += OnAppServiceRequestReceived;
     _connection.ServiceClosed += OnAppServiceClosed;
     AppServiceConnectionStatus connectionStatus = await _connection.OpenAsync();
     string exitReason = null;
     if (connectionStatus == AppServiceConnectionStatus.Success)
     {
         // Report status
         Process[] processes = Process.GetProcessesByName("SyncFolder.Win32Task");
         ValueSet statusMessage = new ValueSet();
         statusMessage.Add("MsgType", "StatusIndication");
         statusMessage.Add("IsTaskRunning", processes.Length != 1);
         // Send StatusIndication and react on the response (if it comes immediately)
         AppServiceResponse appServiceResponse = await _connection.SendMessageAsync(statusMessage);
         if (appServiceResponse.Status == AppServiceResponseStatus.Success)
         {
             ValueSet resp = appServiceResponse.Message;
             if (resp.Count != 0)
             {
                 exitReason = await HandleAppServiceRequestAsync(resp, null);
             }
         }
         else
         {
             exitReason = "HostFailsToRespond";
         }
     }
     else
     {
         exitReason = "CantCreateAppServiceConnection";
     }
     if (exitReason != null)
     {
         await ExitAsync(exitReason);
     }
 }
    
 async void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
 {
     var messageDeferral = args.GetDeferral();
     ValueSet input = args.Request.Message;
     ConditionalFileLogger.Log($"Received message - {(string)input["Request"]}");
     _lastReceivedRequest = args.Request;
     var exitReason = await HandleAppServiceRequestAsync(input, args.Request);
     messageDeferral.Complete();
     if (exitReason != null)
     {
         // Prepare for exit
         await ExitAsync(exitReason);
     }
 }
    
 async Task<string> HandleAppServiceRequestAsync(ValueSet input, AppServiceRequest request)
 {
     string exitReason = null;
     switch ((string)input["Request"])
     {
         case "Ack":
             // Host has acknowledged, wait for a request
             break;
         case "Exit":
             ConditionalFileLogger.Log("Do exit");
             exitReason = "HostRequestsExit";
             break;
         case "ExecJob":
             {
                 // Load the backup task parameters
                 var tasks = (string)input["Tasks"];
                 bool isBackgroundTaskRequest = (bool)input["IsBGTaskRequest"];
                 bool notifyWhenFinished = (bool)input["NotifyWhenFinished"];
                 bool simulate = (bool)input["Simulate"];
                 bool force = (bool)input["Force"];
                 bool rescanTarget = (bool)input["RescanTarget"];
                 bool copyEmptyFolders = (bool)input["CopyEmptyFolders"];
    
                 _taskRequestsList = new List<TaskRequest>();
                 using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(tasks)))
                 {
                     // Deserialization from JSON  
                     DataContractJsonSerializer deserializer = new DataContractJsonSerializer(typeof(List<TaskRequest>));
                     _taskRequestsList = (List<TaskRequest>)deserializer.ReadObject(ms);
                 }
                 ConditionalFileLogger.Log($"Got backup request for {_taskRequestsList.Count} task(s), isBackground: {isBackgroundTaskRequest}");
    
                 ValueSet execJobRespMessage = new ValueSet();
                 execJobRespMessage.Add("MsgType", "ExecJobResponse");
                 execJobRespMessage.Add("Success", true);
                 ConditionalFileLogger.Log($"Send ExecJobResponse message");
                 await SendMessageToHostAsync(execJobRespMessage, request);
    
                 // Handle request in a seperate task in order to keep on receiving host requests
                 _syncCopyBackgroundWorker = new BackgroundWorker();
                 _syncCopyHandler = new SyncCopyRequestHandler(_syncCopyBackgroundWorker);
                 _syncCopyBackgroundWorker.DoWork += async (object sender, DoWorkEventArgs e) =>
                 {
                     _syncCopyHandler.Run(_taskRequestsList, simulate, isBackgroundTaskRequest, notifyWhenFinished, rescanTarget, copyEmptyFolders);
                     ConditionalFileLogger.Log($"SyncCopy thread stopped");
                 };
                 _syncCopyBackgroundWorker.ProgressChanged += async (object sender, ProgressChangedEventArgs e) =>
                 {
                     // Send message to host
                     ValueSet msg = e.UserState as ValueSet;
                     await SendMessageToHostAsync(msg, null);
                 };
                 _syncCopyBackgroundWorker.RunWorkerCompleted += async (object sender, RunWorkerCompletedEventArgs e) =>
                 {
                     if (_syncCopyHandler.ExitReason != null)
                     {
                         ConditionalFileLogger.Log($"Wait 1000ms to close app service connection and exit");
                         await Task.Delay(1000);
                         await ExitAsync(_syncCopyHandler.ExitReason);
                     }
                 };
                 _syncCopyBackgroundWorker.WorkerReportsProgress = true;
                 _syncCopyBackgroundWorker.WorkerSupportsCancellation = true;
                 _syncCopyBackgroundWorker.RunWorkerAsync();
                 break;
             }
         case "StopJob":
             ConditionalFileLogger.Log("Handle StopJob request");
             SyncCopyRequestHandler.sMustStop = true;
             break;
         case "GetUNCPath":
             {
                 ...
                 break;
             }
         case "TestNetwShareAccess":
             {
                 ...
                 break;
             }
         default:
             ConditionalFileLogger.Log("Unexpected request");
             exitReason = "UnexpectedRequest";
             break;
     }
     return exitReason;
 }
    
 async Task SendMessageToHostAsync(ValueSet msg, AppServiceRequest request)
 {
     try
     {
         AppServiceResponseStatus status;
         if (request == null)
         {
             string msgType = (string)msg["MsgType"];
             ConditionalFileLogger.Log($"  Send message '{msgType}' to host using SendMessageAsync");
             var appServiceResponse = await _connection.SendMessageAsync(msg);
             status = appServiceResponse.Status;
         }
         else
         {
             ConditionalFileLogger.Log("  Send message to host using SendResponseAsync");
             status = await _lastReceivedRequest.SendResponseAsync(msg);
         }
         if (status == AppServiceResponseStatus.Success)
         {
             ConditionalFileLogger.Log("  Message successfully sent");
         }
         else
         {
             ConditionalFileLogger.Log($"  Message not sent, error: {status.ToString()}");
         }
     }
     catch (Exception e)
     {
         ConditionalFileLogger.Log($"  Exception sending message, error: {e.Message}");
     }
 }

Messages to be sent by the backgroundworker are reported using the 'progress' event.

As I explained all works well except when during the lifetime of the backgroundworker the host is sending a message, e.g. "StopJob", "GetUNCPath", etc. The progress event keeps firing but each call to connection.SendMessageAsync doesn't return anymore.

The host sends "StopJob", etc. as a result of the user pressing a button. It is standard code, as described in all documentation about AppServices running on the 'server' side.

Hope this helps someone to figure out why out of bound messages received from the host block the sending of messages in a background worker.

· 3 ·
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Since the code snippet you provided in win32 process is not complete, in the DoWork event of BackgroundWorker, I created a for loop and report it, then in ProgressChanged event, I called the SendMessageAsync() to send the progress information to the host. In the process of sending progress, I send a message "StopJob" from host to win32, the backgroundworker can continue send the progress information and the connection.SendMessageAsync can return Succcess status. So I can't reproduce this issue. What did you do in your DoWork event? And what did you do when the win32 received "StopJob" message? Or can you provide a simple complete sample that can be reproduced for us to test?

0 Votes 0 ·

Thanks for the quick reply and the effort to test this.
I did some further tests and replaced my background method by a loop such as yours. The error was still there but then I realized that the reason for holding the progress messages could be inside the UWP app. That turned out to be the case.

In the uwp app I have a background message sender task for the AppService connection with an outgoing send queue. It allows requests to be triggered at various places in my app without having to check whether another request is still on its way.
That turned out to be the problem!

I dropped the sender task now on the uwp side and everything works as expected. Don't understand why a dedicated sender task concept doesn't work for an AppService connection but I can live with that.

Thanks for all the support. The issue can be closed.

0 Votes 0 ·

Glad you solved this issue. You can post an answer and mark it to help others who meet the same issue.

0 Votes 0 ·