// ---------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ---------------------------------------------------------------------------- using System; using System.Collections; using System.Collections.Specialized; using System.Text; using System.Web; using Microsoft.SingSharp; using Microsoft.SingSharp.Runtime; using Microsoft.Singularity.Diagnostics.Contracts; using Microsoft.Singularity.Channels; using Microsoft.Singularity.Directory; using Microsoft.Singularity.WebApps; using Microsoft.Singularity.WebApps.Contracts; using Microsoft.Singularity.PingPong.Contracts; using Microsoft.Singularity.Io; using Microsoft.Singularity.Configuration; using Microsoft.Singularity.Applications; using Microsoft.Singularity.ServiceManager; [assembly: Microsoft.SingSharp.Reflection.Transform(typeof(WebAppResourceTransform))] namespace Microsoft.Singularity.WebApps { [Category("WebApp")] internal sealed class Parameters { [Endpoint] public readonly TRef webAppRef; reflective private Parameters(); } class ServiceManagerWebApp : IWebApp, IDisposable { const string dbgprefix = "ServiceManagerWebApp: "; readonly TRef! rootdsRef; readonly TRef! svmanagerRef; readonly Hashtable/**/! urlHandlers = new Hashtable(); readonly Hashtable/**/! fileHandlers = new Hashtable(); readonly Encoding! encoding; readonly byte[]! newlineBytes; [Microsoft.Contracts.NotDelayed] ServiceManagerWebApp() { DirectoryServiceContract.Imp! rootds = DirectoryService.NewClientEndpoint(); // First, connect to SCM. ServiceManagerContract.Imp! svmanager = ConnectServiceManager(rootds); this.rootdsRef = new TRef(rootds); this.svmanagerRef = new TRef(svmanager); Encoding encoding = Encoding.UTF8; this.encoding = encoding; this.newlineBytes = encoding.GetBytes("\r\n"); base(); this.urlHandlers["/"] = new UrlHandler(this.RootPageHandler); this.fileHandlers["/style.css"] = new FileEntry("/init/sv_style.css", "text/css"); } void Run(Parameters! config) { Dbg("Calling Driver.ServiceChannel"); WebAppContract.Exp! webappChannel = config.webAppRef.Acquire(); webappChannel.SendWebAppReady(); Driver.ServiceChannel(this, webappChannel); } internal static int AppMain(Parameters! config) { Dbg("Starting"); using (ServiceManagerWebApp app = new ServiceManagerWebApp()) { app.Run(config); } return 0; } public void Dispose() { } static ServiceManagerContract.Imp! ConnectServiceManager(DirectoryServiceContract.Imp! rootds) { ServiceManagerContract.Imp! svmanager_imp; ServiceManagerContract.Exp! svmanager_exp; ServiceManagerContract.NewChannel(out svmanager_imp, out svmanager_exp); ErrorCode error; if (!SdsUtils.Bind("/service/services", rootds, svmanager_exp, out error)) { delete svmanager_imp; throw new Exception("Failed to connect to Service Manager."); } svmanager_imp.RecvSuccess(); return svmanager_imp; } public void ProcessRequest(IHttpRequest! request) { try { string! path = request.GetUriPath(); string verb = request.GetVerb(); if (verb == null) throw new InvalidRequestException("Invalid verb"); UrlHandler handler = (UrlHandler)urlHandlers[path]; if (handler != null) { Dbg("Received ProcessRequest - " + path); handler(request); return; } FileEntry file_entry = (FileEntry)fileHandlers[path]; if (file_entry != null) { Dbg("Received ProcessRequest - " + path + " --> file " + file_entry.LocalPath); if (verb != "GET") { throw new InvalidRequestException("Invalid verb"); } SendFile(request, file_entry.LocalPath, file_entry.ContentType); return; } Dbg("No handler for path '{0}'.", path); request.SendStatus(404, "Not Found"); } catch (InvalidRequestException ex) { Dbg("Invalid request: " + ex.Message); request.SendStatus(500, ex.Message); } catch (Exception ex) { Dbg("Exception: " + ex.GetType().FullName + ": " + ex.Message); DebugStub.Break(); } finally { request.Done(); } } ServiceManagerContract.Imp! AcquireServiceManager() { return this.svmanagerRef.Acquire(); } void ReleaseServiceManager([Claims]ServiceManagerContract.Imp! svmanager) { this.svmanagerRef.Release(svmanager); } class RequestFormData { public readonly StringDictionary Values = new StringDictionary(); public string! this[string! name] { get { string value = this.Values[name]; if (value == null) return ""; else return value; } } public bool IsSet(string! name) { string value = this.Values[name]; return value != null && value.Length != 0; } } const string ContentTypeHeaderName = "Content-Type"; const string UrlEncodedContentType = "application/x-www-form-urlencoded"; RequestFormData! GetFormData(IHttpRequest! request) { Dbg("Parsing form data..."); RequestFormData data = new RequestFormData(); string contentType = request.GetHeader(ContentTypeHeaderName); if (contentType == null) { Dbg("No content type. Assuming form data is not present."); return data; } if (String.Compare(contentType, UrlEncodedContentType, true) != 0) { Dbg("Content type is not '{0}'; it's '{1}', which is not recognized.", UrlEncodedContentType, contentType); return data; } Dbg("Content type is correct."); byte[]! body_data = request.GetBodyData(); string! urlEncodedText = Encoding.UTF8.GetString(body_data); Dbg("Decoding: " + urlEncodedText); string[]! fields = urlEncodedText.Split('&'); foreach (string field in fields) { if (field == null) continue; int equals_index = field.IndexOf('='); if (equals_index == -1) { Dbg("field is invalid: " + field); continue; } string! name_encoded = field.Substring(0, equals_index); string! value_encoded = field.Substring(equals_index + 1); string! name_decoded = (!)HttpUtility.UrlDecode(name_encoded); string! value_decoded = (!)HttpUtility.UrlDecode(value_encoded); data.Values[name_decoded] = value_decoded; Dbg("found field '{0}' value '{1}'", name_decoded, value_decoded); } return data; } bool SelectService(ServiceManagerContract.Imp! svmanager, string! serviceName) { svmanager.SendSelectService(Bitter.FromString2(serviceName)); switch receive { case svmanager.Ok(): return true; case svmanager.RequestFailed(error): Dbg("Failed to select service '{0}': {1}", serviceName, ServiceEnums.ToString(error)); return false; case svmanager.ChannelClosed(): Dbg("Service Manager closed channel!"); return false; } } void StartService(string! serviceName) { Dbg("Starting service '{0}'...", serviceName); ServiceManagerContract.Imp! svmanager = AcquireServiceManager(); if (!SelectService(svmanager, serviceName)) return; svmanager.SendStartServiceNoWait(); switch receive { case svmanager.ServiceStarting(): break; case svmanager.RequestFailed(error): break; } svmanager.SendUnselectService(); ReleaseServiceManager(svmanager); } void StopService(string! serviceName) { Dbg("Stopping service '{0}'...", serviceName); ServiceManagerContract.Imp! svmanager = AcquireServiceManager(); if (!SelectService(svmanager, serviceName)) return; svmanager.SendStopServiceNoWait(); switch receive { case svmanager.ServiceStopping(): break; case svmanager.RequestFailed(error): break; } svmanager.SendUnselectService(); ReleaseServiceManager(svmanager); } void EnableService(string! serviceName, bool enabled) { Dbg("Setting service enabled to {0} for service '{1}'...", enabled, serviceName); ServiceManagerContract.Imp! svmanager = AcquireServiceManager(); if (!SelectService(svmanager, serviceName)) return; svmanager.SendEnableService(enabled); switch receive { case svmanager.Ok(): Dbg("Service Manager accepted request to enable/disable service."); break; case svmanager.RequestFailed(error): Dbg("Service Manager FAILED request to enable/disable service: " + ServiceEnums.ToString(error)); break; } svmanager.SendUnselectService(); ReleaseServiceManager(svmanager); } void RootPageHandler(IHttpRequest! request) { Dbg("RootPageHandler running"); // // If the user clicked one of the state-control buttons, then check to see which one. // Perform the requested action, then redirect to this page. This prevents the // browser from interpreting "refresh" as "post the data again". // string! verb = request.GetVerb(); if (verb == "POST") { // User clicked one of our buttons. Dbg("Verb is POST, user probably clicked a control button"); RequestFormData! data = GetFormData(request); string! serviceName = data["ServiceName"]; Dbg("Service name from form: " + serviceName); if (serviceName == "") { Dbg("Service name is empty!"); } if (data.IsSet("StartService")) { StartService(serviceName); } else if (data.IsSet("StopService")) { StopService(serviceName); } else if (data.IsSet("DisableService")) { EnableService(serviceName, false); } else if (data.IsSet("EnableService")) { EnableService(serviceName, false); } else { Dbg("Request did not contain a recognized control action."); } Dbg("Redirecting client"); request.SendStatus(303, "See Other"); request.SendHeader(HttpHeader.RedirectLocation, "/"); return; } request.SendStatus(200, "OK"); request.SendHeader(HttpHeader.Refresh, "5"); // refresh every N seconds ResponseWrite(request, @" Singularity Service Manager "); ResponseWrite(request, "

Services

\r\n"); ServiceManagerContract.Imp! svmanager = AcquireServiceManager(); try { ServiceInfo[]! in ExHeap first_infos = new[ExHeap] ServiceInfo[40]; svmanager.SendEnumerateServices(first_infos); string![]! columns = { "Service Name", // 0 // "Display Name", // // "Executable", // "Activation Mode", // 1 "State", // 2 "Process ID", // 3 "Actions" // 4 }; string[] cells = new string[columns.Length]; ResponseWriteLine(request, "\r\n"); for (;;) { ServiceInfo[]! in ExHeap infos; bool more; int count; switch receive { case svmanager.NextServiceInfo(returned_infos, c): infos = returned_infos; count = c; more = true; break; case svmanager.EnumerationTerminated(returned_infos, c): infos = returned_infos; more = false; count = c; break; case timeout(TimeSpan.FromSeconds(30)): ResponseWriteLine(request, "

Error: Timeout occurred while waiting to receive data from Service Manager.

"); goto done; } if (count > 0) { for (int i = 0; i < count; i++) { expose(infos[i]) { string! serviceName = Bitter.ToString2(infos[i].Config.ServiceName); //string! displayName = Bitter.ToString2(infos[i].Config.DisplayName); //string! executableName = Bitter.ToString2(infos[i].Config.ExecutableName); string! activationMode = ServiceEnums.ToString(infos[i].Config.ActivationMode); int c = 0; cells[c++] = serviceName; // cells[c++] = displayName; // cells[c++] = executableName; cells[c++] = activationMode; cells[c++] = ServiceEnums.ToString(infos[i].Status.State); cells[c++] = infos[i].Status.ProcessId.ToString(); StringBuilder! actions = new StringBuilder(); bool start_stop_applies = infos[i].Config.ActivationMode == ServiceActivationMode.Manual || infos[i].Config.ActivationMode == ServiceActivationMode.Demand; ServiceState svstate = infos[i].Status.State; bool disabled = infos[i].Config.IsAdministrativelyDisabled; bool can_start = start_stop_applies && svstate == ServiceState.Stopped && !disabled; bool can_stop = start_stop_applies && (svstate == ServiceState.Running || svstate == ServiceState.Starting) && !disabled; actions.Append("\r\n"); actions.AppendFormat("\r\n", serviceName); actions.Append(MakeServiceControlButton("StartService", "Start", can_start)); actions.Append(MakeServiceControlButton("StopService", "Stop", can_stop)); actions.Append(MakeServiceControlButton("DisableService", "Disable", !disabled)); actions.Append(MakeServiceControlButton("EnableService", "Enable", disabled)); actions.Append("\r\n"); cells[c++] = actions.ToString(); } ResponseWrite(request, ""); } } if (more) { svmanager.SendEnumerateServices(infos); } else { delete infos; break; } } done: ResponseWriteLine(request, "
"); for (int i = 0; i < columns.Length; i++) { string! column = columns[i]; ResponseWrite(request, column); if (i + 1 < columns.Length) ResponseWrite(request, ""); } ResponseWriteLine(request, "
"); for (int j = 0; j < cells.Length; j++) { string cell = cells[j]; if (cell == null) cell = ""; ResponseWrite(request, cell); if (j + 1 < cells.Length) ResponseWrite(request, ""); } ResponseWriteLine(request, "
"); } finally { ReleaseServiceManager(svmanager); } } static string! MakeServiceControlButton(string! action, string! label, bool enabled) { return String.Format("\r\n", action, label, enabled ? "" : " disabled"); } DirectoryServiceContract.Imp! AcquireDirectory() { return rootdsRef.Acquire(); } void ReleaseDirectory([Claims]DirectoryServiceContract.Imp! rootds) { rootdsRef.Release(rootds); } void SendFile(IHttpRequest! request, string! path, string! contentType) { FileContract.Imp! file; FileContract.Exp! file_exp; FileContract.NewChannel(out file, out file_exp); DirectoryServiceContract.Imp! rootds = AcquireDirectory(); ErrorCode error; if (!SdsUtils.Bind(path, rootds, file_exp, out error)) { delete file; ReleaseDirectory(rootds); string! errorText = SdsUtils.ErrorCodeToString(error); request.SendStatus(400, errorText); request.SendHeader(HttpHeader.ContentType, ContentType.TextHtmlUtf8); StringBuilder! html = new StringBuilder(); html.Append("Error - "); html.Append(errorText); html.Append("

Error

"); html.AppendFormat("

The file '{0}' could not be opened.

", path); html.Append("

Error: "); html.Append(errorText); html.Append("

"); html.Append(""); return; } file.RecvSuccess(); ReleaseDirectory(rootds); request.SendStatus(200, "OK"); request.SendHeader("Content-Type", contentType); const int buffer_length = 4096; byte[]! in ExHeap exbuf = new[ExHeap] byte[buffer_length]; byte[]! localbuf = new byte[buffer_length]; long file_offset = 0; file.SendRead(exbuf, 0, file_offset, exbuf.Length); bool done = false; while (!done) { switch receive { case file.AckRead(returned_exbuf, bytes_read, read_error): if (error != 0) { Dbg("A request to read on file '{0}' failed, error = {1}", path, read_error); delete returned_exbuf; done = true; break; } if (bytes_read < 0 || bytes_read > buffer_length) { Dbg("Filesystem returned a ridiculous number of bytes transferred."); delete returned_exbuf; done = true; break; } if (returned_exbuf.Length != buffer_length) { Dbg("Filesystem returned a buffer with different length?!"); delete returned_exbuf; done = true; break; } if (bytes_read == 0) { Dbg("Received EOF from filesystem"); delete returned_exbuf; done = true; break; } // We know this will not overflow; we just checked it against buffer_length. int bytes_read32 = (int)bytes_read; Dbg("Received {0} bytes from filesystem", bytes_read); // Grrrrrr. IHttpRequest needs to be improved. It does not take // a length within a buffer; it just blats out an entire byte[] buffer. if (bytes_read < buffer_length) { byte[]! partial_buffer = new byte[bytes_read32]; Bitter.ToByteArray(returned_exbuf, 0, bytes_read32, partial_buffer, 0); request.SendBodyData(partial_buffer); done = true; } else { Bitter.ToByteArray(returned_exbuf, 0, bytes_read32, localbuf, 0); request.SendBodyData(localbuf); } file_offset += bytes_read; file.SendRead(returned_exbuf, 0, file_offset, exbuf.Length); break; case file.ChannelClosed(): Dbg("Filesystem closed channel!"); done = true; break; } } Dbg("Done sending file."); delete file; } void SendOkHtmlHeader(IHttpRequest! request) { request.SendStatus(200, "OK"); request.SendHeader("Content-Type", "text/html;charset=utf-8"); } void ResponseWriteLine(IHttpRequest! request, string! format, params object[]! args) { ResponseWriteLine(request, String.Format(format, args)); } void ResponseWriteLine(IHttpRequest! request, string! line) { if (line.Length != 0) { byte[]! bytes = encoding.GetBytes(line); request.SendBodyData(bytes); } request.SendBodyData(newlineBytes); } void ResponseWrite(IHttpRequest! request, string! text) { if (text.Length != 0) { byte[]! bytes = encoding.GetBytes(text); request.SendBodyData(bytes); } } internal static void Dbg(string! line) { DebugStub.WriteLine(dbgprefix + line); } internal static void Dbg(string! format, params object[]! args) { Dbg(String.Format(format, args)); } } delegate void UrlHandler(IHttpRequest! request); class FileEntry { public FileEntry(string! localPath, string! contentType) { this.LocalPath = localPath; this.ContentType = contentType; } public string! LocalPath; public string! ContentType; } class InvalidRequestException : Exception { public InvalidRequestException(string! msg) : base(msg) { } public InvalidRequestException() : base("The HTTP request is invalid; details are not available.") { } } static class ContentType { public const string TextHtml = "text/html"; public const string TextHtmlUtf8 = "text/html;charset=utf-8"; } static class HttpHeader { public const string ContentType = "Content-Type"; public const string RedirectLocation = "Location"; public const string Refresh = "Refresh"; } #if false static class HttpUtility { public static string! UrlDecode(string! text) { int pos = 0; while (true) { if (pos == text.Length) { // no escapes found, just return same string return text; } if (pos == '+' || pos == '%') { // found an escape; must break this loop and process for real break; } // keep looking pos++; } StringBuilder! result = new StringBuilder(); result.Append(text, 0, pos); while (pos < text.Length) { char c = text[pos]; pos++; if (c == '+') { result.Append(' '); } else if (c == '%') { if (pos + 2 <= text.Length) { int high = GetHexValue(text[pos]); int low = GetHexValue(text[pos + 1]); if (high != -1 && low != -1) { pos += 2; char hexc = (char)(high << 4 | low); result.Append(hexc); } else { // bogus escape sequence! result.Append('%'); } } else { // too short for proper escape sequence! result.Append('%'); continue; } } else { result.Append(c); } } } int GetHexValue(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } } #endif }