Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using SQL LocalDB in a Windows Service

I have a very small test application in which I'm trying to install a Windows Service and create a LocalDB database during the install process, then connect to that LocalDB database when the Windows Service runs.

I am running into huge problems connecting to a LocalDB instance from my Windows Service.

My installation process is exactly like this:

  1. Execute an installer .msi file which runs the msiexec process as the NT AUTHORITY\SYSTEM account.

  2. Run a custom action to execute SqlLocalDB.exe with the following commands:

    • sqllocaldb.exe create MYINSTANCE
    • sqllocaldb.exe share MYINSTANCE MYINSTANCESHARE
    • sqllocaldb.exe start MYINSTANCE
  3. Run a custom C# action using ADO.NET (System.Data.SqlConnection) to perform the following actions:

    • Connect to the following connection string, Data Source=(localdb)\MYINSTANCE; Integrated Security=true
    • CREATE DATABASE TestDB
    • USE TestDB
    • CREATE TABLE ...
  4. Start the Windows Service before the installer finishes.

  5. The Windows Service is installed to the LocalSystem account and so also runs as the NT AUTHORITY\SYSTEM user account.

  6. The service attempts to connect using the same connection string used above.

I am consistently getting the following error when trying to open the connection to the above connection string from within the Windows Service:

System.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 50 - Local Database Runtime error occurred. The specified LocalDB instance does not exist.

This is frustrating because both the msi installer custom action and the Windows Service are running under the same Windows user account (I checked, they're both NT AUTHORITY\System). So why the first works and the second does not is beyond me.

I have tried changing the connection strings used in the custom action and the Windows Service to use the share name (localdb)\.\MYINSTANCESHARE and I get the exact same error from the Windows Service.

I have tried changing the user account that the Windows Service logs on as to my Windows user account, which does work as long as I first run a command to add it to the SQL server logins for that instance.

I've also tried running a console application and connecting to the share name connection string and that works as well.

I've also tried connecting to the share name from SQL Server Management Studio and that works as well.

However none of these methods really solve my problem. I need a Windows Service because it starts up as soon as the computer starts up (even if no user logs on) and starts up no matter which user account is logged in.

How does a Windows Service connect to a LocalDB private instance?

I am using SQL Server 2014 Express LocalDB.

like image 403
Trevor Elliott Avatar asked Oct 27 '14 21:10

Trevor Elliott


People also ask

Can I use LocalDB in production?

LocalDB is absolutely supported in production. From the performance point of view it is identical to SQL Server Express, as they share the same database engine.

Is LocalDB SQL Server?

Microsoft SQL Server Express LocalDB is a feature of SQL Server Express targeted to developers. It is available on SQL Server Express with Advanced Services. LocalDB installation copies a minimal set of files necessary to start the SQL Server Database Engine.


2 Answers

Picking up from the comments on the question, here are some areas to look at. Some of these have already been answered in those comments, but I am documenting here for others in case the info might be helpful.

  • Check here for a great source of info on SQL Server Express LocalDB:

    • SQL Server 2014 Express LocalDB
    • SqlClient Support for LocalDB
    • SqlLocalDB Utlity
    • Introducing LocalDB, an improved SQL Express (also look at the Q&A section at the end of the main post, just before the comments, as someone asked if LocalDB can be launched from a service, and the answer is:

      LocalDB can be launched from a service, as long as the profile is loaded for the service account.


  • What version of .Net is being used? Here it is 4.5.1 (good) but earlier versions could not handle the preferred connection string (i.e. @"(localdb)\InstanceName"). The following quote is taken from the link noted above:

    If your application uses a version of .NET before 4.0.2 you must connect directly to the named pipe of the LocalDB.

    And according to the MSDN page for SqlConnection.ConnectionString:

    Beginning in .NET Framework 4.5, you can also connect to a LocalDB database as follows:

    server=(localdb)\\myInstance

  • Paths:

    • Instances: C:\Users{Windows Login}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances

    • Databases:

      • Created via SSMS or direct connection: C:\Users{Windows Login}\Documents or C:\Users{Windows Login}
      • Created via Visual Studio: C:\Users{Windows Login}\AppData\Local\Microsoft\VisualStudio\SSDT


  • Initial Problem
    • Symptoms:
      • Database files (.mdf and .ldf) created in the expected location:
        C:\Windows\System32\config\systemprofile
      • Instance files created in an unexpected location:
        C:\Users\{current user}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
    • Cause (note taken from "SqlLocalDB Utility" MSDN page that is linked above; emphasis mine):

      Operations other than start can only be performed on an instance belonging to currently logged in user.


  • Things to try:

    • Connection string that specifies the database (though maybe a long-shot if the error is regarding not being able to connect to the instance):
    • "Server=(LocalDB)\MYINSTANCE; Integrated Security=true ;AttachDbFileName=C:\Windows\System32\config\systemprofile\TestDB.mdf"
    • "Server=(LocalDB)\.\MYINSTANCESHARE; Integrated Security=true ;AttachDbFileName=C:\Windows\System32\config\systemprofile\TestDB.mdf"

    • Is the service running? Run the following from a Command Prompt:
      TASKLIST /FI "IMAGENAME eq sqlservr.exe"
      It should probably be listed under "Console" for the "Session Name" column

    • Run the following from a Command Prompt:
      sqllocaldb.exe info MYINSTANCE
      And verify that the value for "Owner" is correct. Is the value for "Shared name" what it should be? If not, the documentation states:

      Only an administrator on the computer can create a shared instance of LocalDB

    • As part of the setup, add the NT AUTHORITY\System account as a Login to the system, which is required if this account is not showing as the "Owner" of the instance:
      CREATE LOGIN [NT AUTHORITY\System] FROM WINDOWS; ALTER SERVER ROLE [sysadmin] ADD MEMBER [NT AUTHORITY\System];

    • Check the following file for clues / details:
      C:\Users{Windows Login}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MYINSTANCE\error.log

    • In the end you might need to create an actual account to create and own the Instance and Database, as well as run your service. LocalDB really is meant to be user-mode, and is there any downside to having your service have its own login? And you probably wouldn't need to share the instance at that point.

      And in fact, as noted by Microsoft on the SQL Server YYYY Express LocalDB MSDN page:

      An instance of LocalDB owned by the built-in accounts such as NT AUTHORITY\SYSTEM can have manageability issues due to windows file system redirection; Instead use a normal windows account as the owner.

UPDATE (2015-08-21)

Based on feedback from the O.P. that using a regular User account can be problematic in certain environments, AND keeping in mind the original issue of the LocalDB instance being created in the %LOCALAPPDATA% folder for the user running the installer (and not the %LOCALAPPDATA% folder for NT AUTHORITY\System ), I found a solution that seems to keep with the intent of easy installation (no user to create) and should not require needing extra code to load the SYSTEM profile.

Try using one of the two built-in accounts that is not the LocalSystem account (which does not maintain its own registry info. Use either:

  • NT AUTHORITY\LocalService
  • NT AUTHORITY\NetworkService

Both have their profile folders in: C:\Windows\ServiceProfiles

While I have not been able to test via an installer, I did test a service logging on as NT AUTHORITY\NetworkService by setting my SQL Server Express 2014 instance to log on as this account, and restarted the SQL Server service. I then ran the following:

EXEC xp_cmdshell 'sqllocaldb c MyTestInstance -s';

and it created the instance in: C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances

I then ran the following:

EXEC xp_cmdshell N'SQLCMD -S (localdb)\MyTestInstance -E -Q "CREATE DATABASE [MyTestDB];"';

and it had created the database in: C:\Windows\ServiceProfiles\NetworkService

like image 90
Solomon Rutzky Avatar answered Sep 17 '22 13:09

Solomon Rutzky


I was able to solve similar issue in our WiX installer recently. We have a Windows service, running under SYSTEM account, and an installer, where LocalDB-based storage is one of the options for database configuration. For some time (a couple of years actually) product upgrades and service worked quite fine, with no issues related to LocalDB. We are using default v11.0 instance, which is created in SYSTEM profile in C:\Windows\System32\config tree, and a database specified via AttachDbFileName, created in ALLUSERSPROFILE tree. DB provider is configured to use Windows authentication. We also have a custom action in installer, scheduled as deferred/non-impersonate, which runs DB schema updates.

All this worked fine until recently. After another bunch of DB updates, our new release started to fail after having upgraded over the former - service was unable to start, reporting infamous "A network-related or instance-specific error occurred while establishing a connection to SQL Server" (error 50) fault.

When investigating this issue, it became apparent that the problem is in a way WiX runs custom actions. Although non-impersonated CA-s run under SYSTEM account, the registry profile and environment remain that of current user (I suspect WiX loads these voluntary when attaching to user's session). This leads to incorrect path being expanded from the LOCALAPPDATA variable - the service receives SYSTEM profile one, but the schema update CA works with the user's one.

So here are two possible solutions. The first one is simple, but too intrusive to user's system - with cmd.exe started via psexec, recreate broken instance under the SYSTEM account. This was not an option for us as the user may have other databases created in v11.0 instance, which is public. The second option assumed lots of refactoring, but wouldn't hurt anything. Here is what to do to run DB schema updates properly with LocalDB in WiX CA:

  1. Configure your CA as deferred/non-impersonate (should run under SYSTEM account);
  2. Fix environment to point to SYSTEM profile paths:

    var systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
    Environment.SetEnvironmentVariable("USERPROFILE", String.Format(@"{0}\System32\config\systemprofile", systemRoot));
    Environment.SetEnvironmentVariable("APPDATA", String.Format(@"{0}\System32\config\systemprofile\AppData\Roaming", systemRoot));
    Environment.SetEnvironmentVariable("LOCALAPPDATA", String.Format(@"{0}\System32\config\systemprofile\AppData\Local", systemRoot));
    Environment.SetEnvironmentVariable("HOMEPATH", String.Empty);
    Environment.SetEnvironmentVariable("USERNAME", Environment.UserName);
    
  3. Load SYSTEM account profile. I used LogonUser/LoadUserProfile native API methods, as following:

    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool LogonUser(
        string lpszUserName,
        string lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);
    
    [StructLayout(LayoutKind.Sequential)]
    struct PROFILEINFO
    {
        public int dwSize; 
        public int dwFlags;
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpUserName; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpProfilePath; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpDefaultPath; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpServerName; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpPolicyPath; 
        public IntPtr hProfile; 
    }
    
    [DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool LoadUserProfile(IntPtr hToken, ref PROFILEINFO lpProfileInfo);
    
    var hToken = IntPtr.Zero;
    var hProfile = IntPtr.Zero;
    bool result = LogonUser("SYSTEM", "NT AUTHORITY", String.Empty, 3 /* LOGON32_LOGON_SERVICE */, 0 /* LOGON32_PROVIDER_DEFAULT */, ref token);
    if (result)
    {
        var profileInfo = new PROFILEINFO();
        profileInfo.dwSize = Marshal.SizeOf(profileInfo);
        profileInfo.lpUserName = @"NT AUTHORITY\SYSTEM";
        if (LoadUserProfile(token, ref profileInfo))
            hProfile = profileInfo.hProfile;
    }
    

    Wrap this in an IDisposable class, and use with a using statement to build a context.

  4. The most important - refactor your code to perform necessary DB updates in a child process. This could be a simple exe-wrapper over your installer DLL, or stand-alone utility, if your already have one.

P.S. All these difficulties could be avoided, if only Microsoft let uses choose where to create LocalDB instances, via command line option. Like Postgres' initdb/pg_ctl utilities have, for example.

like image 29
denis.gz Avatar answered Sep 17 '22 13:09

denis.gz