private async Task <SimpleSocket> SpawnServerFromService(string url, bool task) { // Ideally we would like to reuse Uri (or some other similar code), but it doesn't work without a Host var parameterIndex = url.IndexOf('?'); var urlWithoutParameters = parameterIndex != -1 ? url.Substring(0, parameterIndex) : url; string[] urlSegments; string urlParameters; RouterHelper.ParseUrl(url, out urlSegments, out urlParameters); // Find a matching server TaskCompletionSource <Service> serviceTcs; lock (registeredServices) { if (!registeredServices.TryGetValue(urlWithoutParameters, out serviceTcs)) { if (task) { throw new Exception("ConnectionRouter task not found, a task won't spawn a new service on demand instead must be started explicitly."); } serviceTcs = new TaskCompletionSource <Service>(); registeredServices.Add(urlWithoutParameters, serviceTcs); if (urlSegments.Length < 4) { Log.Error($"{RouterMessage.ClientRequestServer} action URL {url} is invalid"); throw new InvalidOperationException(); } var packageName = urlSegments[1]; var packageVersion = urlSegments[2]; var process = urlSegments[3]; // Find package var package = PackageStore.Instance.FindLocalPackage(packageName, new PackageVersionRange(new PackageVersion(packageVersion))); if (package == null) { Log.Error($"{RouterMessage.ClientRequestServer} action URL [{url}] could not locate NuGet package"); throw new InvalidOperationException(); } // Locate executable var servicePath = package.GetFiles().FirstOrDefault(x => string.Compare(Path.GetFileName(x.Path), process, true) == 0)?.FullPath; if (servicePath == null || !File.Exists(servicePath)) { Log.Error($"{RouterMessage.ClientRequestServer} action URL [{url}] references a process that doesn't seem to exist"); throw new InvalidOperationException(); } RunServiceProcessAndLog(servicePath); } } var service = await serviceTcs.Task; // Generate connection Guid var guid = Guid.NewGuid(); var serverSocketTcs = new TaskCompletionSource <SimpleSocket>(); lock (pendingServers) { pendingServers.Add(guid, serverSocketTcs); } await service.SendLock.WaitAsync(); try { // Notify service that we want it to establish back a new connection to us for this client await service.Socket.WriteStream.WriteInt16Async((short)RouterMessage.ServiceRequestServer); await service.Socket.WriteStream.WriteStringAsync(url); await service.Socket.WriteStream.WriteGuidAsync(guid); await service.Socket.WriteStream.FlushAsync(); } finally { service.SendLock.Release(); } // Should answer within 4 sec var ct = new CancellationTokenSource(4000); ct.Token.Register(() => { if (!serverSocketTcs.Task.IsCompleted) { serverSocketTcs.TrySetException(new TimeoutException($"{RouterMessage.ServiceRequestServer} action URL [{url}] could not connect back in time")); } }); // Wait for such a server to be available return(await serverSocketTcs.Task); }
/// <summary> /// Handles ClientRequestServer messages. /// It will try to find a matching service (spawn it if not started yet), and ask it to establish a new "server" connection back to us. /// </summary> /// <param name="clientSocket">The client socket context.</param> /// <returns></returns> private async Task HandleMessageClientRequestServer(SimpleSocket clientSocket) { // Check for an existing server // TODO: Proper Url parsing (query string) var url = await clientSocket.ReadStream.ReadStringAsync(); Log.Info($"Client {clientSocket.RemoteAddress}:{clientSocket.RemotePort} sent message ClientRequestServer with URL {url}"); string[] urlSegments; string urlParameters; RouterHelper.ParseUrl(url, out urlSegments, out urlParameters); if (urlSegments.Length == 0) { throw new InvalidOperationException("No URL Segments"); } SimpleSocket serverSocket = null; ExceptionDispatchInfo serverSocketCapturedException = null; try { // For now, we handle only "service" URL switch (urlSegments[0]) { case "service": { // From the URL, start service (if not started yet) and ask it to provide a server serverSocket = await SpawnServerFromService(url, false); break; } case "task": { // From the URL, start service (if not started yet) and ask it to provide a server serverSocket = await SpawnServerFromService(url, true); break; } case "redirect": { // Redirect to a IP/port serverSocket = new SimpleSocket(); var host = urlSegments[1]; var port = int.Parse(urlSegments[2]); // Note: for security reasons, we currently use a whitelist //if (host == "stridebuild.stride3d.net" && port == 1832) // await serverSocket.StartClient(host, port, false); //else throw new InvalidOperationException("Trying to redirect to a non-whitelisted host/port"); //break; } default: throw new InvalidOperationException("This type of URL is not supported"); } } catch (Exception e) { serverSocketCapturedException = ExceptionDispatchInfo.Capture(e); } if (serverSocketCapturedException != null) { try { // Notify client that there was an error await clientSocket.WriteStream.WriteInt16Async((short)RouterMessage.ClientServerStarted); await clientSocket.WriteStream.WriteInt32Async(1); // error code Failure await clientSocket.WriteStream.WriteStringAsync(serverSocketCapturedException.SourceException.Message); await clientSocket.WriteStream.FlushAsync(); } finally { serverSocketCapturedException.Throw(); } } try { // Notify client that we've found a server for it await clientSocket.WriteStream.WriteInt16Async((short)RouterMessage.ClientServerStarted); await clientSocket.WriteStream.WriteInt32Async(0); // error code OK await clientSocket.WriteStream.FlushAsync(); // Let's forward clientSocketContext and serverSocketContext await await Task.WhenAny( ForwardSocket(clientSocket, serverSocket), ForwardSocket(serverSocket, clientSocket)); } catch { serverSocket.Dispose(); throw; } }