//////////////////////////////////////////////////////////////////////////////// // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // // File: Services/Smb/ClientManager/SmbClientManager.sg // // Note: // // This file contains most of the implementation of the SMB Client Manager Service. // This service is a system-wide singleton process, started during system boot by // the Service Manager Service (SMS). (In the future, it may make sense for one // instance of this service to exist per session. However, that's for future study.) // // This service registers a single control channel, at /dev/smb-client-manager. // Clients, such as the command-line tool net.exe (Services/Smb/Control) open // this channel and use it to send control requests and queries to this service. // These requests allow the user to mount remote SMB filesystems at local namespaces // (creates a mapping), unmount them, enumerate them, and query their status. // // This service is entirely single-threaded. Some requests received from the control // channel are processed immediately (synchronously); the results are sent back to // the originator of the request without performing any blocking I/O or waiting for // any long period of time. A few other requests are asynchronous; when the service // receives such a control request, it allocates memory in the local (non-exchange) // heap to track the status of the request, and future channel messages advance the // state of the request. // using System; using System.Collections; using System.Diagnostics; using Microsoft.Contracts; using Microsoft.Singularity; using Microsoft.Singularity.Channels; using Microsoft.Singularity.Directory; using Microsoft.Singularity.Io; using Microsoft.SingSharp; using Smb.PublicChannels; using Smb.PrivateChannels; [assembly: Microsoft.Singularity.Security.ApplicationPublisherAttribute("singularity.microsoft.com")] [assembly: Microsoft.Singularity.Security.AssertPrivilegeAttribute("$register-privilege.localhost")] namespace Smb.ClientManager { class Manager { public static int Main(string[] args) { // // Register the control endpoint for this service. // ServiceProviderContract.Imp! provider_imp; ServiceProviderContract.Exp! controlprovider; ServiceProviderContract.NewChannel(out provider_imp, out controlprovider); DirectoryServiceContract.Imp! rootds = DirectoryService.NewClientEndpoint(); rootds.SendRegister(Bitter.FromString2(SmbClientManagerContract.ManagerControlPath), provider_imp); switch receive { case rootds.AckRegister(): delete rootds; break; case rootds.NakRegister(ServiceProviderContract.Imp:Start imp, ErrorCode error): DebugLine("The service failed to register its control channel."); DebugLine(" Control channel path: " + SmbClientManagerContract.ManagerControlPath); DebugLine(" Error code: " + error.ToString()); DebugLine("Service will now terminate."); delete rootds; delete imp; delete controlprovider; return -1; } ESet! managers = new ESet(); EMap! clientsConfiguring = new EMap(); EMap! notifiers = new EMap(); DebugLine("SMB service is ready, listening on control channel: " + SmbClientManagerContract.ManagerControlPath); for (;;) { switch receive { case controlprovider.Connect(ServiceContract.Exp:Start! exp): SmbClientManagerContract.Exp manager = exp as SmbClientManagerContract.Exp; if (manager != null) { manager.SendSuccess(); managers.Add(manager); controlprovider.SendAckConnect(); } else { controlprovider.SendNackConnect(exp); } break; case manager.CreateClient(SmbClientConfig exconfig) in managers: // This is a request to create a new SMB client process and start it. string! mountPath = Bitter.ToString2(exconfig.MountPath); if (TraceSwitches.ShowManagerMessages) { DebugLine("CreateClient: mountpath = '{0}'", mountPath); DebugLine(" UncPath: " + Bitter.ToString2(exconfig.UncPath)); } // DebugStub.Break(); // Check to see if we already have a client using the same mount point. // It's much better to detect this now, rather than to spin up a client // process and have it discover the error. ClientProcess client = FindClientByMountPath(mountPath); if (client != null) { exconfig.Dispose(); manager.SendRequestFailed(SmbErrorCode.MountPathAlreadyExists, null); managers.Add(manager); break; } SmbClientControllerContract.Imp! controller; SmbClientControllerContract.Exp! controller_exp; SmbClientControllerContract.NewChannel(out controller, out controller_exp); SmbMuxNotifier.Imp! notifier_imp; SmbMuxNotifier.Exp! notifier_exp; SmbMuxNotifier.NewChannel(out notifier_imp, out notifier_exp); Process process; try { string[] process_args = { "smbclient" }; process = new Process(process_args); } catch (Exception ex) { DebugLine("FAILED to create smbclient process: " + ex.Message); delete controller; delete controller_exp; delete notifier_imp; delete notifier_exp; manager.SendRequestFailed(SmbErrorCode.InternalError, Bitter.FromString2("Failed to create process: " + ex.Message)); managers.Add(manager); break; } process.SetStartupEndpoint(0, controller_exp); process.SetStartupEndpoint(1, notifier_imp); if (TraceSwitches.ShowProcessActivity) DebugLine("Starting client process..."); process.Start(); if (TraceSwitches.ShowProcessActivity) DebugLine("Process started. Sending startup parameters..."); client = new ClientProcess(mountPath, exconfig, process); client.State = ClientState.Starting; client.StartingManagerEndpoint = new TRef(manager); client.UncPath = Bitter.ToString2(exconfig.UncPath); client.CredentialsName = Bitter.ToString2(exconfig.CredentialsName); client.Tag = Bitter.ToString2(exconfig.Tag); client.ConnectionStatus = SmbClientConnectionStatus.Disconnected; client.ReasonDisconnected = SmbReasonDisconnected.Idle; client.ServerOperatingSystem = null; client.ServerDomainName = null; client.ServerMachineName = null; controller.SendConfigure(exconfig); // Further progress will be made when we receive AckConnect or NakConnect from the // client process (or a ChannelClosed). notifiers.Add(notifier_exp, client); clientsConfiguring.Add(controller, client); _clients[mountPath] = client; break; case manager.QueryClientConfig(char[]! in ExHeap exclientId) in managers: { string! clientId = Bitter.ToString2(exclientId); delete exclientId; if (TraceSwitches.ShowManagerMessages) DebugLine("Received QueryClientConfig - id = " + clientId); ClientProcess client = FindClientByMountPath(clientId); if (client != null) { SmbClientConfig config = new SmbClientConfig(); config.MountPath = Bitter.FromString2(client.MountPath); config.UncPath = Bitter.FromString2(client.UncPath); config.CredentialsName = Bitter.FromString2(client.CredentialsName); config.Tag = Bitter.FromString2(client.Tag); if (TraceSwitches.ShowManagerMessages) DebugLine(" Sending config report."); manager.SendClientConfigReport(config); } else { if (TraceSwitches.ShowManagerMessages) DebugLine(" No such client."); manager.SendRequestFailed(SmbErrorCode.MountPathNotFound, null); } managers.Add(manager); break; } case manager.QueryClientStatus(char[]! in ExHeap exclientId) in managers: { string! clientId = Bitter.ToString2(exclientId); delete exclientId; if (TraceSwitches.ShowManagerMessages) DebugLine("Received QueryClientStatus - id = " + clientId); ClientProcess client = FindClientByMountPath(clientId); if (client != null) { SmbClientStatus status = new SmbClientStatus(); status.ConnectionStatus = client.ConnectionStatus; status.ReasonDisconnected = client.ReasonDisconnected; status.ServerOperatingSystem = Bitter.FromString(client.ServerOperatingSystem); status.ServerDomainName = Bitter.FromString(client.ServerDomainName); status.ServerMachineName = Bitter.FromString(client.ServerMachineName); status.ActiveCredentialsName = Bitter.FromString(client.ActiveCredentialsName); if (TraceSwitches.ShowManagerMessages) DebugLine(" Sending status report."); manager.SendClientStatusReport(status); } else { if (TraceSwitches.ShowManagerMessages) DebugLine(" No such client."); manager.SendRequestFailed(SmbErrorCode.MountPathNotFound, null); } managers.Add(manager); break; } case controller.Ok() in clientsConfiguring ~> client: if (TraceSwitches.ShowManagerMessages) DebugLine(client, "Received AckConfigure from client process"); assert client.StartingManagerEndpoint != null; SmbClientManagerContract.Exp! manager = client.StartingManagerEndpoint.Acquire(); client.StartingManagerEndpoint = null; client.State = ClientState.Running; client.Controller = new TRef(controller); if (TraceSwitches.ShowManagerMessages) DebugLine("Sending Ok (for CreateClient) to manager."); manager.SendOk(); managers.Add(manager); break; case controller.RequestFailed(error, errortext) in clientsConfiguring ~> client: // One of the client processes that we started has indicated that it // did not start successfully. Tear down the client process and send an // error message to the manager that requested that the process to be created. if (TraceSwitches.ShowManagerMessages || true) DebugLine(client, "Received NakConfigure from client process: {0} {1}", error.ToString(), Bitter.ToString(errortext)); delete controller; assert client.StartingManagerEndpoint != null; SmbClientManagerContract.Exp! manager = client.StartingManagerEndpoint.Acquire(); client.StartingManagerEndpoint = null; client.State = ClientState.Running; client.Process.Stop(); client.Process.Join(); client.Process.Dispose(false); manager.SendRequestFailed(error, errortext); managers.Add(manager); break; case controller.ChannelClosed() in clientsConfiguring ~> client: // A client process terminated before responding to the NakConnect message. // Treat this like NakConnect without an error code. if (TraceSwitches.ShowManagerMessages || true) DebugLine(client, "Client process has closed its controller channel."); assert client.StartingManagerEndpoint != null; SmbClientManagerContract.Exp! manager = client.StartingManagerEndpoint.Acquire(); client.StartingManagerEndpoint = null; client.State = ClientState.Running; client.Process.Stop(); client.Process.Join(); client.Process.Dispose(false); manager.SendRequestFailed(SmbErrorCode.InternalError, Bitter.FromString2("The SMB client process failed to initialize.")); managers.Add(manager); delete controller; break; case manager.StopClient(char[]! in ExHeap exmountPath) in managers: string! mountPath = Util.ToStringDelete(exmountPath); if (TraceSwitches.ShowManagerMessages) DebugLine("StopClient - mount path = " + mountPath); ClientProcess client = FindClientByMountPath(mountPath); if (client != null) { switch (client.State) { case ClientState.Starting: // This is extremely rude, of course. DebugLine(" Client is Starting - will simply terminate the process."); break; case ClientState.Running: assert client.Controller != null; SmbClientControllerContract.Imp! controller = client.Controller.Acquire(); controller.SendStop(); // -XXX- This is synchronous for now! DebugLine("Waiting to SMB client to respond to Stop request"); switch receive { case controller.Ok(): break; case controller.ChannelClosed(): break; } client.Controller.Release(controller); break; default: DebugLine("ILLEGAL STATE FOR CLIENT PROCESS!"); break; } TerminateClient(client, true); manager.SendOk(); } else { if (TraceSwitches.ShowManagerMessages) DebugLine("Cannot delete client -- there is no client with mount path = '{0}'", mountPath); manager.SendRequestFailed(SmbErrorCode.MountPathNotFound, null); } managers.Add(manager); break; case controlprovider.ChannelClosed(): DebugLine("Control provider channel has closed."); goto terminate; case manager.EnumClients() in managers: { int count = _clients.Values.Count; char[][]! in ExHeap mountPaths = new[ExHeap] char[][count]; int i = 0; foreach (ClientProcess! client in _clients.Values) { if (i >= count) break; char[]! in ExHeap exmount = Bitter.FromString2(client.MountPath); expose (mountPaths[i]) { delete mountPaths[i]; mountPaths[i] = exmount; } i++; } manager.SendClientList(mountPaths); managers.Add(manager); break; } case manager.BindClient(char[]! in ExHeap exmountPath, ServiceContract.Exp:Start! exp) in managers: { string! mountPath = Util.ToStringDelete(exmountPath); if (TraceSwitches.ShowManagerMessages) DebugLine("BindClient - mount path = " + mountPath); ClientProcess client = FindClientByMountPath(mountPath); if (client != null) { switch (client.State) { case ClientState.Running: assert client.Controller != null; SmbClientControllerContract.Imp! controller = client.Controller.Acquire(); try { controller.SendBind(exp); // -XXX- Yes, I know this prevents SmbClientManager from receiving // -XXX- any other messages. Need to deal with that. switch receive { case controller.Ok(): if (TraceSwitches.ShowManagerMessages) DebugLine("Bind successful"); manager.SendOk(); break; case controller.RequestFailed(error, optionalErrorText): if (TraceSwitches.ShowManagerMessages) DebugLine("SmbClient process rejected bind request"); manager.SendRequestFailed(error, optionalErrorText); break; case controller.ChannelClosed(): if (TraceSwitches.ShowManagerMessages) DebugLine("SmbClient channel closed!"); manager.SendRequestFailed(SmbErrorCode.InternalError, null); break; } } finally { client.Controller.Release(controller); } break; default: if (TraceSwitches.ShowManagerMessages) DebugLine("Client is not in a state that allows binding"); manager.SendRequestFailed(SmbErrorCode.InvalidState, null); delete exp; break; } } else { delete exp; manager.SendRequestFailed(SmbErrorCode.MountPathNotFound, null); } managers.Add(manager); break; } case notifier.StatusChanged(SmbClientStatus status) in notifiers ~> client: notifier.SendAck(); if (TraceSwitches.ShowStatusChanges) DebugLine(client, "Received StatusChanged from client"); client.ConnectionStatus = status.ConnectionStatus; client.ReasonDisconnected = status.ReasonDisconnected; client.ServerOperatingSystem = Bitter.ToString(status.ServerOperatingSystem); client.ServerMachineName = Bitter.ToString(status.ServerMachineName); client.ServerDomainName = Bitter.ToString(status.ServerDomainName); client.ActiveCredentialsName = Bitter.ToString(status.ActiveCredentialsName); status.Dispose(); notifiers.Add(notifier, client); break; } // switch receive } terminate: managers.Dispose(); delete controlprovider; notifiers.Dispose(); clientsConfiguring.Dispose(); foreach (ClientProcess! client in _clients.Values) { TerminateClient(client, false); } _clients.Clear(); return 0; } /// /// /// Contains the set of all ClientProcess instances that are managed by this service. /// Aside from brief thread-local references to ClientProcess instances, this table /// contains the only reference to those instances. /// /// /// /// Each key is a string, and is the value of the MountPath field of the client /// process; the keys are case sensitive. Each value is the corresponding /// ClientProcess instance. /// /// static readonly Hashtable/**/! _clients = new Hashtable(); static ClientProcess FindClientByMountPath(string! mountPath) { ClientProcess client = (ClientProcess)_clients[mountPath]; return client; } static void TerminateClient(ClientProcess! client, bool remove) { DebugLine("Terminating child process, pid=" + client.Process.Id); client.Process.Stop(); client.Process.Join(); client.Process.Dispose(false); switch (client.State) { case ClientState.Starting: assert client.Controller == null; break; case ClientState.Running: case ClientState.Stopping: assert client.Controller != null; SmbClientControllerContract.Imp! controller = client.Controller.Acquire(); delete controller; break; default: DebugLine("INVALID STATE for client process! state=" + client.State.ToString()); break; } // This is conditional, because we don't want to modify the _clients collection // while we're enumerating it. if (remove) _clients.Remove(client.MountPath); } static void DebugLine(string! line) { DebugStub.WriteLine("SMBMGR: " + line); } static void DebugLine(string! format, params object[]! args) { DebugLine(String.Format(format, args)); } static void DebugLine(ClientProcess! client, string! line) { DebugStub.WriteLine(client.DebugPrefix + line); } static void DebugLine(ClientProcess! client, string! format, params object[]! args) { DebugLine(client, String.Format(format, args)); } } // This class mirrors the exchange type SmbClientConfig. // It is for use within the process heap, rather than the exchange heap. #if false class ClientConfig { public string! MountPath; public string! UncPath; public string! UserName; public string! Password; } #endif class Util { public static void DumpException(Exception chain) { for (Exception ex = chain; ex != null; ex = ex.InnerException) { DebugStub.WriteLine(String.Format("{0}: {1}", ex.GetType().FullName, ex.Message)); } } public static string! ToStringDelete([Claims]char[]! in ExHeap exstring) { string! localstring = Bitter.ToString2(exstring); delete exstring; return localstring; } } }