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

Jos Huybrighs 336 Reputation points
2020-04-26T17:05:18.52+00:00

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.

Universal Windows Platform (UWP)
{count} votes

Accepted answer
  1. Jos Huybrighs 336 Reputation points
    2020-04-29T07:38:39.827+00:00

    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.

    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Jos Huybrighs 336 Reputation points
    2020-04-27T20:15:15.443+00:00

    @Fay Wang - 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.