Monitoring with PowerShell: Monitoring OneDrive status for current logged on user!

Since the release of Onedrive and Onedrive for business, a lot of system administrators have been trying to figure out how to monitor the onedrive status. Rodney Viana at Microsoft made a pretty awesome module to be able to get the current OneDrive Sync status, you can find that module here.

The issue with this module is that it has to run under the current logged on user, You don’t always have the ability to do that, especially when using RMM systems that always use the NT AUTHORITY\SYSTEM account. Now PowerShell has the ability to load .NET components as code and execute them, this gave me the idea to use impersonation of the current user in my PowerShell script to monitor OneDrive.

After messing around trying to build my own, a friend of mine pointed me to Roger Zanders post here. Combining these two scripts was pretty simple and resolved the entire onedrive monitoring issue for me.

So without any more ado; I introduce the CyberDrain OneDrive Status monitoring script for RMM systems. The script downloads the latest version of the OneDriveLib.dll, runs Get-ODStatus cmdlet under the current user, and returns the state of the OneDrive sync in $ODErrors.

The script

$Source = @"
using System;  
using System.Runtime.InteropServices;

namespace murrayju.ProcessExtensions  
{
    public static class ProcessExtensions
    {
        #region Win32 Constants

        private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
        private const int CREATE_NO_WINDOW = 0x08000000;

        private const int CREATE_NEW_CONSOLE = 0x00000010;

        private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
        private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

        #endregion

        #region DllImports

        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            String lpApplicationName,
            String lpCommandLine,
            IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes,
            bool bInheritHandle,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            String lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        private static extern bool DuplicateTokenEx(
            IntPtr ExistingTokenHandle,
            uint dwDesiredAccess,
            IntPtr lpThreadAttributes,
            int TokenType,
            int ImpersonationLevel,
            ref IntPtr DuplicateTokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

        [DllImport("userenv.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hSnapshot);

        [DllImport("kernel32.dll")]
        private static extern uint WTSGetActiveConsoleSessionId();

        [DllImport("Wtsapi32.dll")]
        private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern int WTSEnumerateSessions(
            IntPtr hServer,
            int Reserved,
            int Version,
            ref IntPtr ppSessionInfo,
            ref int pCount);

        #endregion

        #region Win32 Structs

        private enum SW
        {
            SW_HIDE = 0,
            SW_SHOWNORMAL = 1,
            SW_NORMAL = 1,
            SW_SHOWMINIMIZED = 2,
            SW_SHOWMAXIMIZED = 3,
            SW_MAXIMIZE = 3,
            SW_SHOWNOACTIVATE = 4,
            SW_SHOW = 5,
            SW_MINIMIZE = 6,
            SW_SHOWMINNOACTIVE = 7,
            SW_SHOWNA = 8,
            SW_RESTORE = 9,
            SW_SHOWDEFAULT = 10,
            SW_MAX = 10
        }

        private enum WTS_CONNECTSTATE_CLASS
        {
            WTSActive,
            WTSConnected,
            WTSConnectQuery,
            WTSShadow,
            WTSDisconnected,
            WTSIdle,
            WTSListen,
            WTSReset,
            WTSDown,
            WTSInit
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;
        }

        private enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct STARTUPINFO
        {
            public int cb;
            public String lpReserved;
            public String lpDesktop;
            public String lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        private enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WTS_SESSION_INFO
        {
            public readonly UInt32 SessionID;

            [MarshalAs(UnmanagedType.LPStr)]
            public readonly String pWinStationName;

            public readonly WTS_CONNECTSTATE_CLASS State;
        }

        #endregion

        // Gets the user token from the currently active session
        private static bool GetSessionUserToken(ref IntPtr phUserToken)
        {
            var bResult = false;
            var hImpersonationToken = IntPtr.Zero;
            var activeSessionId = INVALID_SESSION_ID;
            var pSessionInfo = IntPtr.Zero;
            var sessionCount = 0;

            // Get a handle to the user access token for the current active session.
            if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
            {
                var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
                var current = pSessionInfo;

                for (var i = 0; i < sessionCount; i++)
                {
                    var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
                    current += arrayElementSize;

                    if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive)
                    {
                        activeSessionId = si.SessionID;
                    }
                }
            }

            // If enumerating did not work, fall back to the old method
            if (activeSessionId == INVALID_SESSION_ID)
            {
                activeSessionId = WTSGetActiveConsoleSessionId();
            }

            if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0)
            {
                // Convert the impersonation token to a primary token
                bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                    ref phUserToken);

                CloseHandle(hImpersonationToken);
            }

            return bResult;
        }

        public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true)
        {
            var hUserToken = IntPtr.Zero;
            var startInfo = new STARTUPINFO();
            var procInfo = new PROCESS_INFORMATION();
            var pEnv = IntPtr.Zero;
            int iResultOfCreateProcessAsUser;

            startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));

            try
            {
                if (!GetSessionUserToken(ref hUserToken))
                {
                    throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed.");
                }

                uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW);
                startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE);
                startInfo.lpDesktop = "winsta0\\default";

                if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false))
                {
                    throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");
                }

                if (!CreateProcessAsUser(hUserToken,
                    appPath, // Application Name
                    cmdLine, // Command Line
                    IntPtr.Zero,
                    IntPtr.Zero,
                    false,
                    dwCreationFlags,
                    pEnv,
                    workDir, // Working directory
                    ref startInfo,
                    out procInfo))
                {
                    throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n");
                }

                iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
            }
            finally
            {
                CloseHandle(hUserToken);
                if (pEnv != IntPtr.Zero)
                {
                    DestroyEnvironmentBlock(pEnv);
                }
                CloseHandle(procInfo.hThread);
                CloseHandle(procInfo.hProcess);
            }
            return true;
        }
    }
}


"@
New-Item 'C:\programdata\Microsoft OneDrive' -ItemType directory -Force -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/rodneyviana/ODSyncService/master/Binaries/PowerShell/OneDriveLib.dll' -OutFile 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'

Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp 
$scriptblock = {
    Unblock-File 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'
    import-module 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'
    $ODStatus = Get-ODStatus | convertto-json | out-file 'C:\programdata\Microsoft OneDrive\OneDriveLogging.txt'
}


[murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser("C:\Windows\System32\WindowsPowershell\v1.0\Powershell.exe", "-command $($scriptblock)","C:\Windows\System32\WindowsPowershell\v1.0",$false)
start-sleep 5
$ErrorList = @("NotInstalled", "ReadOnly", "Error", "OndemandOrUnknown")
$ODStatus = (get-content "C:\programdata\Microsoft OneDrive\OneDriveLogging.txt" | convertfrom-json).value
foreach ($ODStat in $ODStatus) {
    if ($ODStat.StatusString -in $ErrorList) { $ODerrors = "$($ODStat.LocalPath) is in state $($ODStat.StatusString)" }
}
if (!$ODerrors) {
    $ODerrors = "Healthy"
}

 

And that’s it! I’m quite proud of this one as I have seen a lot of people struggle with it so I hope it helps. As always, Happy PowerShelling!

46 Comments

  1. alexander john March 29, 2020 at 12:03 pm

    Thank you so much for the script!

    I’m trying to get it to work but I get following error:

    Ausnahme beim Aufrufen von “StartProcessAsCurrentUser” mit 4 Argument(en): “StartProcessAsCurrentUser: GetSessionUserToken failed.”
    In Zeile:282 Zeichen:148
    + … dowsPowershell\v1.0\Powershell.exe”, “-command $($scriptblock)”,”C:\W …
    + ~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : Exception

    I commented out line 271 und 272 (I have the dll and folder already).

    1. Kelvin Tegelaar March 30, 2020 at 9:00 am

      This script is supposed to run as SYSTEM, the error where you cannot get a user token means you are not running it as system. to do that, you can use PSEXEC or your RMM, as most RMMs run everything as system.

  2. Alex Appleton May 9, 2020 at 4:37 pm

    This is great, thank you for this! I modified the if condition on the foreach loop a bit so that it wasn’t reporting on old profiles not in use. Just want to receive current ones here:

    if ($ODStat.StatusString -in $ErrorList `
    -and (Get-CimInstance win32_userprofile | ? {($_.SID -eq $ODStat.SID).LastUseTime}) `
    -and (Get-CimInstance win32_userprofile | ? {($_.SID -eq $ODStat.SID).LastUseTime}) -lt ((get-date).AddDays(-7)))

  3. Martyn May 30, 2020 at 3:53 pm

    Firstly thank-you so much for sharing this!
    Unfortunately, and probably because I’m doing something wrong, when I run it I receive the output as below. Not sure what this indicates. Any ideas?

    Mode LastWriteTime Length Name
    —- ————- —— —-
    d—– 30/05/2020 14:45 Microsoft OneDrive
    True

    1. Kelvin Tegelaar May 30, 2020 at 4:20 pm

      Thats expected behaviour, the actual state is stored in the $ODerrors variable; most RMM systems are able to pick up variables. if yours does not you can add “write-host $ODerrors” at the end of the script.

      1. Martyn June 2, 2020 at 8:40 am

        Thanks. We’re using Solarwinds RMM but I’ll modify the script to output the variable. Awesome work by the way!

  4. Ryan June 11, 2020 at 7:26 am

    Thanks a bunch for the script Kelvin, works a treat.

    Just for anyone else using CWA(or any other RMM that doesn’t read PowerShell vars) separating everything after “Start sleep” to another script step works great for just getting the message.
    Otherwise all of the previous console text will be returned.

    1. Andrew November 2, 2020 at 3:57 am

      I found that adding > $null to the New-Item line stopped the output of the directory creation. So the whole line became:
      New-Item ‘C:\programdata\Microsoft OneDrive’ -ItemType directory -Force -ErrorAction SilentlyContinue > $null

  5. Ryan June 28, 2020 at 7:31 am

    Thank you for the work – this is going to save a lot of time.

    Likely an easy question, but is there a way to remove OneDrive personal from triggering an error result? We do not have Personal accounts logged in and it is showing the “OnDemandOrUnknown” as an error state. I can remove that error state from the component list, but that is the same state shown if the program is closed and would not help.

    Thank you again for your help!

    1. Sasa July 15, 2020 at 5:47 pm

      Ryan, you can just filter Servicetype
      e.g.
      $ODStat.StatusString -in $ErrorList -and $odstat.servicetype -notlike “personal”

  6. Pingback: Monitoring with PowerShell: Monitoring the Onedrive client limitations - CyberDrain

  7. Pingback: Automating with PowerShell: Impersonating users while running as SYSTEM - CyberDrain

  8. Shital Girmal July 23, 2020 at 11:26 am

    It is throw me two error one is OneDriveLogging.txt file is missing and second one Exception calling ‘StartProcessAsCurrentUser’ mit 4 Argument(en): “StartProcessAsCurrentUser: GetSessionUserToken failed.

    1. Kelvin Tegelaar July 23, 2020 at 12:43 pm

      You’re not running the script as system. You’ll have to use your RMM to run this as system, or try using psexec or something similar.

  9. Jimmy August 5, 2020 at 10:32 am

    Hi, I use component at DattoRMM ComponentStore.
    If FileOnDemand is enable always return error: “OnDemandOrUnknown”
    StatusString : OnDemandOrUnknown

    1. Kelvin Tegelaar August 5, 2020 at 10:34 am

      That has nothing to do with files on demand. “OnDemand” is the status that the sync engine is paused and is only running on-demand, unknown is often a state in which a file that cannot be live synced is open, PST, PS1, CSV, etc.

  10. Douglas Hall August 5, 2020 at 8:39 pm

    If only running the GET-ODStatus would stop refreshing the task bar. 9.99999999999999/10

  11. Reta Kneale August 25, 2020 at 7:54 pm

    when i first implemented this was great — however today all of a sudden i am getting errors everywhere…
    Monitor OneDrive Sync Status [WIN] – Alert: Hash of OneDriveLib.DLL does not match. Check hash. (OneDriveLibHash: 3FED7E2E24A64835FA5CC241BC0ECAB3)

    ???? HELP!

    1. Kelvin Tegelaar August 25, 2020 at 8:04 pm

      Hi Reta,

      The error is pretty clear; You’ll have to replace the hash you are using, as the file was updated on github.

  12. Michie DeBerry September 10, 2020 at 8:32 pm

    This doesn’t appear to be working for me. Get-ODStatus in the spawned process doesn’t provide any output; however, when I run it from a regular PowerShell window, it works as expected. Any thoughts?

    1. Michie DeBerry September 10, 2020 at 8:39 pm

      I figured it out. Our RMM is 32 bit, and the DLL is a 64 bit DLL, so we have to reference PowerShell.exe via C:\Windows\SysNative\.

      Thanks for this script!

  13. Boyd September 15, 2020 at 12:08 pm

    Hi Kelvin,

    First let me thank you for the awesome script. It works flawlessly, but every once in a while we get a bunch of errors because of a changed Hash.
    I was just wondering why that check is in the script. I have tried to read it myself and see if there is a reason for it, but my knowledge is lacking and I am unable to find out why.
    Could you help me with the answer?

    1. Kelvin Tegelaar September 15, 2020 at 12:11 pm

      Hi Boyd,

      The datto version does a hash check on the DLL file as we’re downloading it from an untrusted external source(github). for security reasons you want to be sure that the version downloaded is the one we’re expecting and not one a bad actor injected.

      1. Boyd September 15, 2020 at 1:08 pm

        Hi Kelvin,

        That makes perfect sense. Thank you for the quick answer.
        We are loving your scripts in Datto. We have been able to monitor a lot more since you started working with them.
        Thank you.

        Kind regards,
        Boyd

  14. Brandon Allison October 3, 2020 at 1:54 am

    Hello,
    Thank you for providing this script. However, is there any way to test this script by putting OneDrive in an error state? We also use Continuum as our RMM, but I don’t believe it has the ability to create custom monitor scripts?

  15. Eric November 19, 2020 at 10:59 pm

    Hi. First off, thank you so much. Your scripts have been so helpful. My team and I have put them to great use.

    I know others have said it before but I wanted to bring it up to see if you have any additional advice. I have this setup in my RMM and it is running in SYSTEM context. For most of my machines that I’ve tested it with, I get OnDemandOrUnknown as the status. I’ve confirmed that it’s checking the correct library. Any tips you can suggest? It very well (and most likely) is an issue with the computers or my deployment but I’m hoping you can help shed some light on it.

  16. Alberto Garcia December 29, 2020 at 6:06 pm

    Hi, thank you for all your hard work. We’re getting errors on some devices when we open One Drive we can’t seem to find what the problem is. We’re using Datto RMM which creates tickets on AutoTask. We tried removing “Read Only” from the code below as we thought that maybe people having files open that are trying to back up would throw a false positive. Is there a way to know what error is throwing creating the ticket?

    $ErrorList = @(“NotInstalled”, “Error”, “OndemandOrUnknown”)

    “Monitor OneDrive Sync Status [WIN] v2 – Alert: Onedrive Sync not functional. Check diagnostics (OneDriveLibHash: 93BBF37D334D55D825E3978D5D88A70E) for JOTTO-SURFACE4”

    This alert ticket was generated from AEM alert … for the trigger “Monitor OneDrive Sync Status [WIN] v2 – Alert: Onedrive Sync not functional. Check diagnostics (OneDriveLibHash: 93BBF37D334D55D825E3978D5D88A70E)” within the policy “OneDrive Sync Status”. The Surface Pro 4 (Microsoft Corporation) was last accessed by “…” on 2020-12-29 15:13:33.0

    1. Alberto Garcia December 30, 2020 at 6:11 pm

      I’m sorry I was able to find the output in the RMM. They’re failing for OnDemandOrUnknown just like Eric even though everything is syncing properly.

      1. Kelvin Tegelaar December 30, 2020 at 6:21 pm

        “OnDemandOrUnknown” is the status that a machine currently does not sync for several reasons. On demand references that the sync engine is only running ‘on demand'(so paused in the taskbar). This can have a bunch of sources; A file is open that cannot be synced such a PS1,PST,CSV file, the machine is running on battery power, etc etc.

        1. Alberto Garcia January 7, 2021 at 1:06 am

          We have people that sync to sharepoint using One Drive, we’re getting the alert of the file open when the script runs causing tickets to be opened. By the time we’d check the computer the sync would have succeeded. We’re thinking of having it run nightly to eliminate false positives as users. Thanks for your help.

  17. Tyler Berger February 25, 2021 at 3:07 pm

    For those struggling to get Datto to work. Note you are looking for a MD5 hash. I was attempting to obtain it using powershell and failing. Without specifying the algorithm, powershell returns the SHA256 hash by default. The command below did the trick for me.

    get-filehash OneDriveLib.dll -Algorithm MD5

  18. Jay April 30, 2021 at 11:20 pm

    Thanks for posting this.
    Here is what I found after running it on several machines: We use CWA.

    Signed out status: healthy.
    Paused status: onDemondorUnknown.
    Normal status: onDemondorUnknown
    OneDrive Not installed: healthy.

    I added “write-host $ODerrors” at the end of the script.
    How do I know when there is an issue with OneDrive sync? paused, signed out, normal?

    1. Jeff November 16, 2021 at 10:02 pm

      Jay, you ever figure out how to signify a user is signed out and not using OneDrive at all?

    1. Mike September 29, 2021 at 9:37 pm

      Unfortunately, for us, this feature is not available for GCC licensing.

  19. Steve Hunt May 5, 2021 at 11:55 am

    I am getting a good status (up to date) from the user’s individual onedrive for business cloud, but an error from the shared documents library in their sharepoint team site, which is also being sync’d by Onedrive.
    The OneDriveLogging.txt shows both are being checked but the sharepoint library shows the following error:
    “ServiceType”: “Business1”,
    “StatusString”: “\u003cERROR\u003eStatus not found for type [ Team Site – Documents]”

    Has anyone else encountered / solved this?
    fingers crossed…

    1. Jamie May 17, 2021 at 2:59 pm

      I’m seeing the same problem as Steve.
      Any solutions out there?
      fingers crossed!

      1. Kelvin Tegelaar May 18, 2021 at 11:20 am

        The developer of the module is working on it, all I can say right now…sorry! 🙂

  20. Matt August 4, 2021 at 1:08 pm

    Hi, are there any further updates on this?

  21. Mike September 29, 2021 at 10:00 pm

    I created a CI in SCCM with this script but it is always returning “Non-compliant.” The txt file shows “StatusString”: “Up to date”, my compliance rule looks for the value True from $OSDerrors. What am I missing?

  22. Dave KIng October 23, 2021 at 11:44 am

    Thanks for the script

    I have this setup as a monitor in Datto, I am a bit lost as to how the variable $ODerrors generates an alert though? The script is running fine and giving the output as expected in a post mentioned above, and differently if there is a sync issue. But of course, I only want a ticket if there is an issue.

    Any suggestions much appreciated.

  23. David King October 23, 2021 at 12:24 pm

    I am lost – why does my comment get deleted everytime?

    My questions are no different to anyone elses?

    1. Kelvin Tegelaar October 23, 2021 at 12:39 pm

      H Dave, comments don’t get deleted but get queued, mostly because of spammers. I take the time once every couple of weeks to reply to comments when the mood strikes.

      1. David King October 23, 2021 at 12:45 pm

        OK thanks for the reply. My question has now arrived lol.

        The scripts are great, my questions are simple ;0)

        Great work

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.