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;


        #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);

        private static extern uint WTSGetActiveConsoleSessionId();

        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);


        #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

        private struct PROCESS_INFORMATION
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;

            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,

        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

        private struct WTS_SESSION_INFO
            public readonly UInt32 SessionID;

            public readonly String pWinStationName;

            public readonly WTS_CONNECTSTATE_CLASS State;


        // 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);


            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));

                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
                    workDir, // Working directory
                    ref startInfo,
                    out procInfo))
                    throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n");

                iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
                if (pEnv != IntPtr.Zero)
            return true;

New-Item 'C:\programdata\Microsoft OneDrive' -ItemType directory -Force -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri '' -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!

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

  1. alexander john

    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 Post author

      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

    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

    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

    1. Kelvin Tegelaar Post author

      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

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

  4. Ryan

    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.

  5. Ryan

    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!


Leave a Reply

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.