Elevated trust in Silverlight 4

Background

In Silverlight 4, Out Of Browser with elevated permission is significantly improved, now the OOB application has more privilege in accessing system resources such as the ability of accessing Isolated Storage, manipulating COM objects, access local registry entries, or even invoke Microsoft Speech API to phonate.

Essentially, to achieve this, the main improvements are:

  1. Microsoft gives Silverlight 4 OOB applications ability to request elevated trust.

    From Trusted Applications
    You can configure out-of-browser applications to require elevated trust. After installation, these trusted applications can bypass some of the restrictions of the security sandbox. For example, trusted applications can access user files and use full-screen mode without keyboard restrictions.

  2. A new concept coming from .NET 4.0 called “late binding”, the C# key word: dynamic could be use to declare a undetermined type at build time, during runtime, Microsoft.CSharp.RuntimeBinder will do dynamically building.

Introduction

My post is going to concentrated discuss about elevated trust, so read the articles below if you have any issues about creating OOB and request elevated permission.

How to: Configure an Application for Out-of-Browser Support
How to Configure your Silverlight App to run in Elevated Trust Mode

I developed a simple Silverlight OOB demo, it will access local system resources including:

  • Let user choose some file(s) and then copy them to isolated storage.
  • Access isolated storage enumerate all files.
  • Create a txt file under drive C: by invoking “Scripting.FileSystemObejct”, as well as read its content back.
  • Write registry entry under HKEY_CURRENT_USER, read registry entry under HKEY_LOCAL_MACHINE, by using “WScript.Shell”.
    Note: Silverlight OOB application will NOT have write permission to HKLM, it only have read permission.
  • Run another executable files located on the system by using “WScript.Shell”.
  • Phonate a sentence user input into the textbox.

Screenshot

After installing on the system, its UI is shown below (I know it is really poor… SorrySmile):
MainPage

Implementation

The elevated permission ONLY enabled in Out Of Brower scenario, so in our Silverlight application we need check whether currently it is running out of browser:

if(Application.Current.IsRunningOutOfBrowser)
    // Access local file, registry, COM, etc.

In additional, to invoke COM objects, we need check whether AutomationFactory is available:

if (AutomationFactory.IsAvailable)

OK here we go to see the code behind to implement elevated permission.

1. Clicks on Button – “Copy File to Isolated Storage acces”, a File open dialog will popup, screenshot below:
OpenFileDialog
Code behind to open file dialog:

            OpenFileDialog dlg = new OpenFileDialog { Filter = "All files (*.*)|*.*", Multiselect = true };
            var dlgResult = dlg.ShowDialog();

Read selected file(s) and copy them to isolated storage:

                IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication();
                foreach (FileInfo file in dlg.Files)
                {
                    using (Stream fileStream = file.OpenRead())
                    {
                        using (IsolatedStorageFileStream isoStream =
                            new IsolatedStorageFileStream(file.Name, FileMode.Create, iso))
                        {
                            // Read and write the data block by block until finish
                            while (true)
                            {
                                byte[] buffer = new byte[100001];
                                int count = fileStream.Read(buffer, 0, buffer.Length);
                                if (count > 0)
                                {
                                    isoStream.Write(buffer, 0, count);
                                }
                                else
                                {
                                    break;
                                }
                            }
                        }
                    }
                }

Code behind for “Load file from isolated storage”:


var isoFiles = from files in IsolatedStorageFile.GetUserStoreForApplication().GetFileNames()
                           select files;

2. Create a text file at “C:\WayneTestSL4Fso\WayneTest.txt”, please note: if you use System.IO.File to do such operation you won’t success, I guess it is because elevated trust is still not directly implemented in a lot of managed assemblies. Here in my demo I used Scripting.FileSystemObejct:

        private String folderPath = "C:\\WayneTestSL4FSO";
        private String filePath = "C:\\WayneTestSL4Fso\\WayneTest.txt";

        using (dynamic fso = AutomationFactory.CreateObject("Scripting.FileSystemObject"))
        {
            if (!fso.FolderExists(folderPath)) fso.CreateFolder(folderPath);
            dynamic txtFile = fso.CreateTextFile(filePath);
            txtFile.WriteLine("Some text...");
            txtFile.close();
        }

P.S. While I first time used “dynamic” keyword within a using statement, I was a little bit surprised, I can simply try to dispose a dynamic object without checking whether it has implemented IDisposible, hence I tried run using (dynamic x = 8 ), then I got thisSmile:
IncorrectUsing

OK, let’s back to the code implementation for reading the text file I just created,

                var fileContent = String.Empty;

                using (dynamic fso = AutomationFactory.CreateObject("Scripting.FileSystemObject"))
                {
                    dynamic file = fso.OpenTextFile(filePath);
                    fileContent = file.ReadAll();

                    file.Close();
                }

3. Registry write/read, please note: we can only have registry write permission to HKCU NOT HKLM, we have read permission to HKLM entries.

                using (dynamic wScript = AutomationFactory.CreateObject("WScript.Shell"))
                {
                    // Only has write permissin to HKCU
                    wScript.RegWrite(@"HKCU\Software\WayneTestRegValue",
                            "SomeStrValue", "REG_SZ");
                }
                using (dynamic wScript = AutomationFactory.CreateObject("WScript.Shell"))
                {
                    string dotNetRoot =
                        wScript.RegRead(@"HKLM\SOFTWARE\Microsoft\.NETFramework\InstallRoot");
                }

4. Run another local application

                using (dynamic wScript = AutomationFactory.CreateObject("WScript.Shell"))
                {
                    //Refer WScript.Run at: http://msdn.microsoft.com/en-us/library/d5fk67ky(v=VS.85).aspx
                    wScript.Run("iexplore http://wayneye.com", 1, true);
                }

Note 1: WScript.Shell.Run method can accepts not only executable files, but also accepts *.bat, Windows Script Host files (*.vbs, *.js) or PowerShell script files, etc.
Note 2: Intention to elevate more permission by running another exe or script file definitely won’t success, for example, if I try to invoke AccessKHLM.js below from my OOB application I will get a 80070005 error code that indicates access denied:

var WshShell = WScript.CreateObject("WScript.Shell");

WshShell.RegWrite("HKLM\\Software\\WayneTestValue\\", 1, "REG_BINARY");
WshShell.Close();

If you double click the Demo.js you will success since you are a Windows Administrator, while “Silverlight-based applications run in partial trust, which means they run within a security sandbox“, for more information please refer Trusted Application.

5. Phonate a sentence

    using (dynamic speechApi = AutomationFactory.CreateObject("Sapi.SpVoice"))
    {
        speechApi.Speak(this.txtPhonateSource.Text);
    }

6. Code to implement close button “X” appear on the upper-top corner.

    using (var wScript = AutomationFactory.CreateObject("WScript.Shell"))
    {
        wScript.Run(@"cmd /k taskkill /IM sllauncher.exe & exit", 0);
    }

This is a little bit tricky, I searched a while on google and found a great article Programmatically exit Silverlight 4 Out-of-browser application.  Essentially the code invokes WScript.Shell and runs cmd and terminate sllauncher.exe, so that our OOB process got killedSmile with tongue out.

Conclusion

With elevated trust for Silverlight OOB applications, we can do much more than ever, it give more confidence to develope Enterprise business applications using Silverlight technology, yesterday I saw Scott Guthrie posted a blog talking about Silverlight, he mentioned Microsoft will absolutely continue work hard on Silverlight for Enterprise Businees Applications (both online and OOB).

Source Code Download

Silverlight4ManipulateSystem.zip

References

How to: Configure an Application for Out-of-Browser Support
http://http://msdn.microsoft.com/en-us/library/dd833073(v=VS.95).aspx

How to Configure your Silverlight App to run in Elevated Trust Mode
http://blogs.silverlight.net/blogs/msnow/archive/2010/04/20/tip-of-the-day-112-how-to-configure-your-silverlight-app-to-run-in-elevated-trust-mode.aspx

Silverlight Tip of the Day #19: Using Isolated Storage
http://blogs.silverlight.net/blogs/msnow/archive/2008/07/16/tip-of-the-day-19-using-isolated-storage.aspx

File Explorer using Silverlight 4 COM Interoperability
http://www.codeproject.com/KB/silverlight/FileExplorerInSilverlight.aspx

WshShell Object
http://msdn.microsoft.com/en-us/library/aew9yb99(v=VS.85).aspx

A complete Impersonation Demo in C#.NET

Under some scenarios we need impersonate another Windows account and do some work under that user’s session, for example:

  • An enterprise ASP.NET web application provides server administrators’ ability to access the server under some specific privilege set; Server admin input their NT account information (domain\account + password) on the page, we need get WinNT Access Token and then impersonate this server user, so that we acquire its specific privilege and do the things ONLY THIS ACCOUNT CAN DO.
  • We developed a Windows Service which needs internet access periodically, but a specific user sets an Sock5 proxy to access internet, then your Windows Service needs to know the Socks proxy information so that it could access internet, you must impersonate this user and read the settings.
  • Impersonation definition

    Definition copied from: http://msdn.microsoft.com/en-us/library/aa376391(VS.85).aspx

    Impersonation is the ability of a thread to execute using different security information than the process that owns the thread. Typically, a thread in a server application impersonates a client. This allows the server thread to act on behalf of that client to access objects on the server or validate access to the client’s own objects.

    I read many articles and blogs and wrote an ImpersonateHelper class to do impersonation work, during the investigating I noticed that very few articles/blogs refer a complete impersonation process, so I decided to write one that refer as more details as I can, and actually my code was a code snippet combination came from 10+ sourcesSmile.

    Functionality

    I create a local user: TempUser which belongs to “Administrators” (make sure log on TempUser at least once), I logged on as my own account and I am going to impersonate TempUser and do two things:

    1. Create a folder “C:\TempFolder”, modify its default privilege, ONLY TempUser has full control of it, I will create a text file under this folder after impersonating to prove impersonation is successfully.
      Tempfolder1Notes: After setting TempUser as the only owner, my current account cannot access this folder except privilege promotion (I disabled UAC, if UAC is enabled, a prompt window will pop up and as for Admin confirm).
      Tempfolder2 

      In additional, I tried to access this folder programmatically under my account, UnauthorizedAccessException will be thrown!

      Unauthorized

    2. I will access TempUser’s HKEY_CURRENT_USER and do registry key creation, reading and deleting.

    Code Snippet

    We need invoke three very famous Win32 API: LogonUser, DuplicateToken and RevertToSelf.

            [DllImport("advapi32.dll")]
            public static extern int LogonUser(String lpszUserName,
                String lpszDomain,
                String lpszPassword,
                int dwLogonType,
                int dwLogonProvider,
                ref IntPtr phToken);
    
            [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);
    
            ///
            /// A process should call the RevertToSelf function after finishing any impersonation begun by using the DdeImpersonateClient, ImpersonateDdeClientWindow, ImpersonateLoggedOnUser, ImpersonateNamedPipeClient, ImpersonateSelf, ImpersonateAnonymousToken or SetThreadToken function.
            /// If RevertToSelf fails, your application continues to run in the context of the client, which is not appropriate. You should shut down the process if RevertToSelf fails.
            /// RevertToSelf Function: http://msdn.microsoft.com/en-us/library/aa379317(VS.85).aspx
            ///
            /// A boolean value indicates the function succeeded or not.
            [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern bool RevertToSelf();
    
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern bool CloseHandle(IntPtr handle);
    

    An important notes: in order to access HKCU, we need invoke another Win32 API: LoadUserProfile, to acquire the handle of HKCU under TempUser, code below, as I highlighted line 45 and 49, after invoking LoadUserProfile, hProfile will be set handle to HKCU:

        [StructLayout(LayoutKind.Sequential)]
        public struct ProfileInfo
        {
            ///
            /// Specifies the size of the structure, in bytes.
            ///
            public int dwSize;
    
            ///
            /// This member can be one of the following flags: PI_NOUI or PI_APPLYPOLICY
            ///
            public int dwFlags;
    
            ///
            /// Pointer to the name of the user.
            /// This member is used as the base name of the directory in which to store a new profile.
            ///
            public string lpUserName;
    
            ///
            /// Pointer to the roaming user profile path.
            /// If the user does not have a roaming profile, this member can be NULL.
            ///
            public string lpProfilePath;
    
            ///
            /// Pointer to the default user profile path. This member can be NULL.
            ///
            public string lpDefaultPath;
    
            ///
            /// Pointer to the name of the validating domain controller, in NetBIOS format.
            /// If this member is NULL, the Windows NT 4.0-style policy will not be applied.
            ///
            public string lpServerName;
    
            ///
            /// Pointer to the path of the Windows NT 4.0-style policy file. This member can be NULL.
            ///
            public string lpPolicyPath;
    
            ///
            /// Handle to the HKEY_CURRENT_USER registry key.
            ///
            public IntPtr hProfile;
        }
    
            [DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);
    
            [DllImport("Userenv.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool UnloadUserProfile(IntPtr hToken, IntPtr lpProfileInfo);
    

    Code to execute impersonation:

    WindowsIdentity m_ImpersonatedUser;
                IntPtr token = IntPtr.Zero;
                IntPtr tokenDuplicate = IntPtr.Zero;
                const int SecurityImpersonation = 2;
                const int TokenType = 1;
    
                try
                {
                    if (RevertToSelf())
                    {
                        Console.WriteLine("Before impersonation: " +
                                          WindowsIdentity.GetCurrent().Name);
    
                        String userName = "TempUser";
                        IntPtr password = GetPassword();
    
                        if (LogonUser(userName, Environment.MachineName, "!@#$QWERasdf", LOGON32_LOGON_INTERACTIVE,
                                      LOGON32_PROVIDER_DEFAULT, ref token) != 0)
                        {
                            if (DuplicateToken(token, SecurityImpersonation, ref tokenDuplicate) != 0)
                            {
                                m_ImpersonatedUser = new WindowsIdentity(tokenDuplicate);
                                using (m_ImpersonationContext = m_ImpersonatedUser.Impersonate())
                                {
                                    if (m_ImpersonationContext != null)
                                    {
                                        Console.WriteLine("After Impersonation succeeded: " + Environment.NewLine +
                                                          "User Name: " +
                                                          WindowsIdentity.GetCurrent(TokenAccessLevels.MaximumAllowed).Name +
                                                          Environment.NewLine +
                                                          "SID: " +
                                                          WindowsIdentity.GetCurrent(TokenAccessLevels.MaximumAllowed).User.
                                                              Value);
    
                                        #region LoadUserProfile
                                        // Load user profile
                                        ProfileInfo profileInfo = new ProfileInfo();
                                        profileInfo.dwSize = Marshal.SizeOf(profileInfo);
                                        profileInfo.lpUserName = userName;
                                        profileInfo.dwFlags = 1;
                                        Boolean loadSuccess = LoadUserProfile(tokenDuplicate, ref profileInfo);
    
                                        if (!loadSuccess)
                                        {
                                            Console.WriteLine("LoadUserProfile() failed with error code: " +
                                                              Marshal.GetLastWin32Error());
                                            throw new Win32Exception(Marshal.GetLastWin32Error());
                                        }
    
                                        if (profileInfo.hProfile == IntPtr.Zero)
                                        {
                                            Console.WriteLine(
                                                "LoadUserProfile() failed - HKCU handle was not loaded. Error code: " +
                                                Marshal.GetLastWin32Error());
                                            throw new Win32Exception(Marshal.GetLastWin32Error());
                                        }
                                        #endregion
    
                                        CloseHandle(token);
                                        CloseHandle(tokenDuplicate);
    
                                        // Do tasks after impersonating successfully
                                        AccessFileSystem();
    
                                        // Access HKCU after loading user's profile
                                        AccessHkcuRegistry(profileInfo.hProfile);
    
                                        // Unload user profile
                                        // MSDN remarks http://msdn.microsoft.com/en-us/library/bb762282(VS.85).aspx
                                        // Before calling UnloadUserProfile you should ensure that all handles to keys that you have opened in the
                                        // user's registry hive are closed. If you do not close all open registry handles, the user's profile fails
                                        // to unload. For more information, see Registry Key Security and Access Rights and Registry Hives.
                                        UnloadUserProfile(tokenDuplicate, profileInfo.hProfile);
    
                                        // Undo impersonation
                                        m_ImpersonationContext.Undo();
                                    }
                                }
                            }
                            else
                            {
                                Console.WriteLine("DuplicateToken() failed with error code: " + Marshal.GetLastWin32Error());
                                throw new Win32Exception(Marshal.GetLastWin32Error());
                            }
                        }
                    }
                }
                catch (Win32Exception we)
                {
                    throw we;
                }
                catch
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
                finally
                {
                    if (token != IntPtr.Zero) CloseHandle(token);
                    if (tokenDuplicate != IntPtr.Zero) CloseHandle(tokenDuplicate);
    
                    Console.WriteLine("After finished impersonation: " + WindowsIdentity.GetCurrent().Name);
                }
    

    AccessFileSystem method

            private static void AccessFileSystem()
            {
                // Access file system %appdata% will be "C:\Users\TempUser\appdata\Roaming"
                String appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                File.AppendAllText("C:\\TempFolder\\Temp.txt", "some text...");
            }
    

    AccessHkcuRegistry method

            private static void AccessHkcuRegistry(IntPtr hkcuHandle)
            {
                // Access registry HKCU
                using (SafeRegistryHandle safeHandle = new SafeRegistryHandle(hkcuHandle, true))
                {
                    using (RegistryKey tempUserHKCU = RegistryKey.FromHandle(safeHandle))
                    {
                        // Unum all sub keys under tempuser's HKCU
                        String[] keys = tempUserHKCU.GetSubKeyNames();
    
                        // Create a new sub key under tempuser's HKCU
                        using (RegistryKey tempKeyByWayne = tempUserHKCU.CreateSubKey("TempKeyByWayne"))
                        {
                            // Ensure priviledge
                            //RegistrySecurity registrySecurity = new RegistrySecurity();
                            //RegistryAccessRule accessRule = new RegistryAccessRule(Environment.MachineName + "\\" + userName,
                            //                                                       RegistryRights.TakeOwnership,
                            //                                                       InheritanceFlags.ContainerInherit,
                            //                                                       PropagationFlags.None,
                            //                                                       AccessControlType.Allow);
                            //registrySecurity.SetAccessRule(accessRule);
                            //tempKeyByWayne.SetAccessControl(registrySecurity);
    
                            // Create a new String value under created TempKeyByWayne subkey
                            tempKeyByWayne.SetValue("StrType", "TempContent", RegistryValueKind.String);
    
                            // Read the value
                            using (RegistryKey regKey = tempUserHKCU.OpenSubKey("TempKeyByWayne"))
                            {
                                String valueContent = regKey.GetValue("StrType") as String;
                                Console.WriteLine(valueContent);
                            }
    
                            // Delete created TempKeyByWayne subkey
                            tempUserHKCU.DeleteSubKey("TempKeyByWayne");
                            tempKeyByWayne.Close();
                        }
                    }
                }
            }
    

    Impersonation result and verification

    Temp.txt was created.
    TempFolder

    “TempKeyByWayne” was created under HKCU.
    Registry

    Complete source code download

    References

    How to implement impersonation in an ASP.NET application
    http://support.microsoft.com/kb/306158

    LogonUser Win32 API
    http://msdn.microsoft.com/en-us/library/aa378184(VS.85).aspx

    How to spawn a process that runs under the context of the impersonated user in Microsoft ASP.NET pages
    http://support.microsoft.com/kb/889251

    ASP.NET Impersonation
    http://msdn.microsoft.com/en-us/library/xh507fc5(v=VS.100).aspx

    Safely Impersonating Another User
    http://blogs.msdn.com/b/shawnfa/archive/2005/03/22/400749.aspx

    Original post permalink: http://wayneye.com/Blog/DotNet-Impersonation-Demo

    Personal Schedule Management Tool

    Time is always ticking no matter you care it or you don’t care it, everyone knows it including myself, to remind myself don’t waste too much time on gaming, reading news or something else, I developed a little tool which I call it “Personal Schedule Management Tool” to achieve this simple goal.

    Mechanism

    I create an XML file to store a serial of [time stamp/task to do] pairs, the tool will start with Windows and load the XML file, a working timer behind it will periodically check whether there is a task needs to be done at defined time stamp, once the condition is matched, the tool will

    1. Shows Windows Balloon Tips with the task as content, screenshot below.
      Balloon
    2. Invokes managed Microsoft Speech API: SpeechSynthesizer.Speak() to remind me even if I am not in front of my dev-box at that time.

    Here is my XML to store schedule items:

    <?xml version="1.0" encoding="utf-8" ?>
    <WayneScheduleItems>
      <ScheduleItem TriggerHour="18" TriggerMinute="20" Content="Wayne, time to have your dinner." />
      <ScheduleItem TriggerHour="19" TriggerMinute="0" Content="Wayne! It is time to learn technologies and programming skills, i.e. CODING TIME!" />
      <ScheduleItem TriggerHour="20" TriggerMinute="30" Content="OK, your eye and brain need rest, time to do some body execise - running, sit-ups, push-up, etc.  Enjoy:)" />
      <ScheduleItem TriggerHour="21" TriggerMinute="0" Content="Well, sweat off your face and have a bath:)." />
      <ScheduleItem TriggerHour="21" TriggerMinute="30" Content="All right, well come back, you know you should read some books!" />
      <ScheduleItem TriggerHour="23" TriggerMinute="0" Content="Wayne, thanks for the hard work! Time to sleep, have a good night!" />
    </WayneScheduleItems>
    

    UI Implementation

    The tool is a WinForm developed in C#, it has no Windows and starts in system tray, to achieve this I set MainWindow’s FormBorderStyle = None, ShowIcon = False and ShowInTaskBar = False, see below.

    WindowProperty

    One more thing is drag two controls onto the form: System.Windows.Forms.NotifyIcon, System.Windows.Forms.ContextMenuStrip and  and naming the instances as “systrayIcon” and “systrayMenu”.

    Controls

    After that I need add menu items, code snippet below:

    this.editScheduleItemsToolStripMenuItem.Name = "editScheduleItemsToolStripMenuItem";
    this.editScheduleItemsToolStripMenuItem.Text = "Edit Schedule Items";
    this.editScheduleItemsToolStripMenuItem.Click += new System.EventHandler(this.editScheduleItemsToolStripMenuItem_Click);
    this.systrayMenu.Items.Add(editScheduleItemsToolStripMenuItem);
    

    All right, its appearance is like below:

    SystrayMenu

    Functionality implementation

    Timer:

                // Initializes timer
                scheduleTimer = new System.Timers.Timer()
                                {
                                    Interval = 1000d * 60 * 10, // 10 minutes
                                    Enabled = true
                                };
                this.scheduleTimer.Start();
                this.scheduleTimer.Elapsed += new System.Timers.ElapsedEventHandler(scheduleTimer_Elapsed);
            }
    
            void scheduleTimer_Elapsed(object sender, ElapsedEventArgs e)
            {
                DateTime currentTime = e.SignalTime;
    
                try
                {
                    foreach (ScheduleItem scheduleItem in scheduleItems)
                    {
                        if (currentTime.Hour == scheduleItem.TriggerHour
                        && currentTime.Minute == scheduleItem.TriggerMinute)
                        {
                            LogManager.WriteAsync(String.Format("{0}, notification occurred: {1}.{2}",
                                currentTime.ToLocalTime(), scheduleItem.Content, Environment.NewLine));
    
                            // Trigger bubble/voice notification
                            this.systrayIcon.ShowBalloonTip(8000, Constants.BalloonTitle, scheduleItem.Content, ToolTipIcon.Info);
                            SoundNotifier.Phonate(scheduleItem.Content);
    
                            break; // Avoid redundant check
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogManager.WriteAsync(ex.Message);
                }
    
                LogManager.WriteAsync(String.Format("Schedule check at: {0}{1}", currentTime.ToLocalTime(), Environment.NewLine));
            }
    

    Load schedule items from XML using LINQ:

        public static class ScheduleItemsReader
        {
            private static readonly String ScheduleItemsXmlLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScheduleItems.xml");
    
            public static IEnumerable<ScheduleItem> Load()
            {
                IList<ScheduleItem> scheduleItems = new List<ScheduleItem>();
    
                IEnumerable<XElement> scheduleItemsCollection = from scheduleItem in XElement.Load(ScheduleItemsXmlLocation).Descendants("ScheduleItem")
                                                                select scheduleItem;
    
                foreach (XElement ele in scheduleItemsCollection)
                {
                    ScheduleItem scheduleItem = new ScheduleItem();
                    scheduleItem.TriggerHour = ushort.Parse(ele.Attribute("TriggerHour").Value);
                    scheduleItem.TriggerMinute = ushort.Parse(ele.Attribute("TriggerMinute").Value);
                    scheduleItem.Content = ele.Attribute("Content").Value;
    
                    scheduleItems.Add(scheduleItem);
                }
    
                return scheduleItems;
            }
        }
    

    Invoke Speech API:

        public static class SoundNotifier
        {
            public static void Phonate(String sentence)
            {
                using (SpeechSynthesizer speaker = new SpeechSynthesizer())  // Dispose the its instance as soon as it spoke fnished
                {
                    ReadOnlyCollection<InstalledVoice> installedVoices = speaker.GetInstalledVoices();
    
                    CultureInfo cultureInfo = CultureInfo.GetCultureInfo("en-US");
    
                    PromptBuilder pb = new PromptBuilder();
                    pb.StartVoice(VoiceGender.Female, VoiceAge.Adult);
                    pb.StartStyle(new PromptStyle(PromptVolume.Loud));
    
                    pb.StartSentence();
                    pb.AppendText(sentence, PromptEmphasis.Strong);
                    pb.EndSentence();
    
                    pb.EndStyle();
                    pb.EndVoice();
    
                    speaker.Speak(pb);
                }
            }
        }
    

    LogManager:

            public static void WriteAsync(String content)
            {
                FileStream fileStream = null;
    
                LogFilePath = String.Format(LogFilePath, DateTime.Now.ToString("MM-dd-yyyy"));
                if (File.Exists(LogFilePath))
                    fileStream = File.OpenWrite(LogFilePath);
                else
                    fileStream = File.Create(LogFilePath);
                FileInfo logFile = new FileInfo(LogFilePath);
    
                StringBuilder logBuilder = new StringBuilder();
                logBuilder.AppendFormat(
                    "===================================================================================================={0}{1}{0}{0}",
                    Environment.NewLine,
                    content);
    
                byte[] data = Encoding.UTF8.GetBytes(logBuilder.ToString());
                fileStream.Position = logFile.Length; // Move to the end of the existing text file
    
                fileStream.BeginWrite(data, 0, data.Length, new AsyncCallback(r =>
                {
                    AsyncState asyncState = r.AsyncState as AsyncState;
    
                    asyncState.FStream.EndWrite(r);
                    asyncState.FStream.Close();
                }), new AsyncState(fileStream, data, m_ManualResetEvent));
    
                m_ManualResetEvent.WaitOne(100, false);
            }
    

    Source download:

    Schedule Management.zip