In order to capture the screen you need to run a program in the session of an user. That is because without the user there is no way to have a desktop associated.

To solve this you can run a desktop application to take the image, this application can be invoked in the session of the active user, this can be done from a service.

The code below allows you to invoke an desktop application in such way that it run on the local user's desktop.

If you need to execute as a particular user, check the code in article Allow service to interact with desktop? Ouch.. You can also consider using the function LogonUser.

The code:

public void Execute()
{
    IntPtr sessionTokenHandle = IntPtr.Zero;
    try
    {
        sessionTokenHandle = SessionFinder.GetLocalInteractiveSession();
        if (sessionTokenHandle != IntPtr.Zero)
        {
            ProcessLauncher.StartProcessAsUser("Executable Path", "Command Line", "Working Directory", sessionTokenHandle);
        }
    }
    catch
    {
        //What are we gonna do?
    }
    finally
    {
        if (sessionTokenHandle != IntPtr.Zero)
        {
            NativeMethods.CloseHandle(sessionTokenHandle);
        }
    }
}

internal static class SessionFinder
{
    private const int INT_ConsoleSession = -1;

    internal static IntPtr GetLocalInteractiveSession()
    {
        IntPtr tokenHandle = IntPtr.Zero;
        int sessionID = NativeMethods.WTSGetActiveConsoleSessionId();
        if (sessionID != INT_ConsoleSession)
        {
            if (!NativeMethods.WTSQueryUserToken(sessionID, out tokenHandle))
            {
                throw new System.ComponentModel.Win32Exception();
            }
        }
        return tokenHandle;
    }
}

internal static class ProcessLauncher
{
    internal static void StartProcessAsUser(string executablePath, string commandline, string workingDirectory, IntPtr sessionTokenHandle)
    {
        var processInformation = new NativeMethods.PROCESS_INFORMATION();
        try
        {
            var startupInformation = new NativeMethods.STARTUPINFO();
            startupInformation.length = Marshal.SizeOf(startupInformation);
            startupInformation.desktop = string.Empty;
            bool result = NativeMethods.CreateProcessAsUser
            (
                sessionTokenHandle,
                executablePath,
                commandline,
                IntPtr.Zero,
                IntPtr.Zero,
                false,
                0,
                IntPtr.Zero,
                workingDirectory,
                ref startupInformation,
                ref processInformation
            );
            if (!result)
            {
                int error = Marshal.GetLastWin32Error();
                string message = string.Format("CreateProcessAsUser Error: {0}", error);
                throw new ApplicationException(message);
            }
        }
        finally
        {
            if (processInformation.processHandle != IntPtr.Zero)
            {
                NativeMethods.CloseHandle(processInformation.processHandle);
            }
            if (processInformation.threadHandle != IntPtr.Zero)
            {
                NativeMethods.CloseHandle(processInformation.threadHandle);
            }
            if (sessionTokenHandle != IntPtr.Zero)
            {
                NativeMethods.CloseHandle(sessionTokenHandle);
            }
        }
    }
}

internal static class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    internal static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern bool CreateProcessAsUser(IntPtr tokenHandle, string applicationName, string commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandle, int creationFlags, IntPtr envrionment, string currentDirectory, ref STARTUPINFO startupInfo, ref PROCESS_INFORMATION processInformation);

    [DllImport("Kernel32.dll", EntryPoint = "WTSGetActiveConsoleSessionId")]
    internal static extern int WTSGetActiveConsoleSessionId();

    [DllImport("WtsApi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool WTSQueryUserToken(int SessionId, out IntPtr phToken);

    [StructLayout(LayoutKind.Sequential)]
    internal struct PROCESS_INFORMATION
    {
        public IntPtr processHandle;
        public IntPtr threadHandle;
        public int processID;
        public int threadID;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct STARTUPINFO
    {
        public int length;
        public string reserved;
        public string desktop;
        public string title;
        public int x;
        public int y;
        public int width;
        public int height;
        public int consoleColumns;
        public int consoleRows;
        public int consoleFillAttribute;
        public int flags;
        public short showWindow;
        public short reserverd2;
        public IntPtr reserved3;
        public IntPtr stdInputHandle;
        public IntPtr stdOutputHandle;
        public IntPtr stdErrorHandle;
    }
}

This code is a modification of the one found at the article Allow service to interact with desktop? Ouch. (MUST READ)


Addendum:

The code above allows to execute a program in the desktop of the user logged locally on the machine. This method specific for the current local user, but it is possible to do it for any user. Check the code at the article Allow service to interact with desktop? Ouch. for an example.

The core of this method is the function CreateProcessAsUser, you can find more about at MSDN.

Replace "Executable Path" with the path of the executable to run. Replace "Command Line" with the string passed as execution arguments, and replace "Working Directory" with the working directory you want. For example you can extract the folder of the executable path:

    internal static string GetFolder(string path)
    {
        var folder = System.IO.Directory.GetParent(path).FullName;
        if (!folder.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()))
        {
            folder += System.IO.Path.DirectorySeparatorChar;
        }
        return folder;
    }

If you have a service, you can use this code in the service to invoke a desktop application. That desktop application may also be the the service executable... for that you can use Assembly.GetExecutingAssembly().Location as the executable path. Then you can use System.Environment.UserInteractive to detect if the executable is not running as a service and pass as execution arguments information about the task needed to do. In the context of this answer that is to capture the screen (for example with CopyFromScreen), it could be something else.


What I did to solve this is call tscon.exe and tell it to redirect the session back to the console just before the screenshot is taken. It goes like this (note, this exact code is untested):

public static void TakeScreenshot(string path) {
    try {
        InternalTakeScreenshot(path);
    } catch(Win32Exception) {
        var winDir = System.Environment.GetEnvironmentVariable("WINDIR");
        Process.Start(
            Path.Combine(winDir, "system32", "tscon.exe"),
            String.Format("{0} /dest:console", GetTerminalServicesSessionId()))
        .WaitForExit();

        InternalTakeScreenshot(path);
    }
}

static void InternalTakeScreenshot(string path) {
    var point = new System.Drawing.Point(0,0);
    var bounds = System.Windows.Forms.Screen.GetBounds(point);

    var size = new System.Drawing.Size(bounds.Width, bounds.Height);
    var screenshot = new System.Drawing.Bitmap(bounds.Width, bounds.Height);
    var g = System.Drawing.Graphics.FromImage(screenshot)
    g.CopyFromScreen(0,0,0,0,size);

    screenshot.Save(path, System.Drawing.Imaging.ImageFormat.Jpeg); 
}

[DllImport("kernel32.dll")]
static extern bool ProcessIdToSessionId(uint dwProcessId, out uint pSessionId);

static uint GetTerminalServicesSessionId()
{
    var proc = Process.GetCurrentProcess();
    var pid = proc.Id;

    var sessionId = 0U;
    if(ProcessIdToSessionId((uint)pid, out sessionId))
        return sessionId;
    return 1U; // fallback, the console session is session 1
}

This is not a supported feature it is true that it works in XP and windows server 2003 however this is seen as security flaw.

To prevent this, don't use the 'x' to close the remote connection, but use %windir%\system32\tscon.exe 0 /dest:console instead. (That will insure that the screen isn't locked). - Nicolas Voron

It true that if you disconnect from the server in this way the "screen" wont be locked to ensure it stay unlocked you need to make sure you turn off the screen saver since as soon as that start up it will auto lock you screen.

There is quite a few examples only of people doing the same thing even here at stack overflow the post below suggest that you create a windows application that run under an actual user account that sends screen shots over IPC to the running service.

The correct way to get a custom GUI that works with a service is to separate them into two processes and do some kind of IPC (inter process communication). So the service will start-up when the machine comes up and a GUI application will be started in the user session. In that case the GUI can create a screenshot, send it to the service and the service can do with it, whatever you like. - Screenshot of process under Windows Service

I have collated a few strategies I have found online that may give you some ideas.

Third party software

There is a lot of programs out there that make screen shots of web sites like http://www.websitescreenshots.com/ they have a user interface and command line tool. But if you are using some testing framework this might not work since it will make a new request to fetch all the assets and draw the page.

WebBrowser control

I am not sure what browser you are using to test you company web site however if you are not bothered about which browser It is you could use a WebBrowser control and use the DrawToBitmap method.

Virtualisation

I have seen a system where the developers were using virtual environments with the browser of their choice with all the settings done to make sure the machine didn't lock and if it did it would restart.

Selenium

It is also possible using selenium with the selenium-webdriver and headless a ruby gem developed by leonid-shevtsov if your test are in selenium this approach might be the best. Selenium itself support screen capture on the webdrivers they have available.

Of course all of this depends on what you are using for your testing framework if you can share some details on your setup we will be able to give you a better answer.


The problem seems to be that when you close the remote connection, the screen goes in a locked state which prevent the system to perform graphics operation like your g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);

To prevent this, don't use the 'x' to close the remote connection, but use %windir%\system32\tscon.exe 0 /dest:console instead. (That will insure that the screen isn't locked).

Read this post for further informations (in VBA, but c#-understandable ;-) )

EDIT If you want to do it directly in c#, try something like this :

Process p = new Process();

p.StartInfo.FileName = "tscon";
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;

p.StartInfo.Arguments = "0 /dest:console";
p.Start();