views:

212

answers:

7

I have an MS Access program in use in multiple locations. It connects to MS SQL Server tables, but the server name is different in each location. I am looking for the fastest way to test for the existence of a server. The code I am currently using looks like this:

ShellWait "sc \\" & ServerName & " qdescription MSSQLSERVER > " & Qt(fn)
FNum = FreeFile()
Open fn For Input As #FNum
Line Input #FNum, Result
Close #FNum
Kill fn

If InStr(Result, "SUCCESS") Then ...

ShellWait: executes a shell command and waits for it to finish
Qt: wraps a string in double quotes
fn: temporary filename variable

I run the above code against a list of server names (of which only one is normally available). The code takes about one second if the server is available and takes about 8 seconds for each server that is unavailable. I'd like to get both of these lower, if possible, but especially the fail case as this one happens most often.

+1  A: 

The way I've done this (in the distant past) was to use linked tables and have a User form that allows a server to be one-off selected at runtime. Alternatively, you would place the server name in a config file and dynamically create the connection string using it.

Mitch Wheat
+1 for simplicity
mwolfe02
+1  A: 
telnet servername 1433
despart
That of course will only work if the SQL Server instance is on its default port.
kevinw
+1  A: 

You could try to create an ADO connection and set the timeout to some low value, e.g. (untested):

Dim cn As ADODB.Connection 
Set cn = New ADODB.Connection
cn.ConnectionTimeout = 4     ' Wait at most 4 seconds for connection
cn.ConnectionString = "Provider=SQLOLEDB.1;Data Source=" & ServerName & ";Integrated Security=SSPI"

On Error Resume Next
cn.Open
If Err.Number > 0 Then
    ...
Else
    cn.Close
    ...
End If
On Error Goto 0   ' replace 0 with previously used error handler
Heinzi
I'll admit I did not try this option, since I am currently using DAO as my reference of choice and did not want to have to deal with any complications that might arise from adding this reference (especially due to the high amount of overlap between ADO and DAO). This certainly looks reasonable, though.
mwolfe02
I just attempt a connection via ADO and if that fails I notify the user.
Lunatik
You don't need a reference to ADO to use ADO in Access. You can either user late binding with Application.CreateObject to initialize an ADO top-level connection object or use CurrentProject.Connection without needing to initialize with late binding.
David-W-Fenton
+1  A: 

The solution I eventually settled on was to use nslookup.exe as a precursor to my sc.exe command. If the SQL Server server does not exist, nslookup tells me so immediately. Making this change cut down the time it took to fail on a SQL Server lookup from about 8 seconds to well under 1 second. The success case is actually slightly longer, but not noticeable. For those who may be interested, here is my final solution (hopefully the purpose of my personal functions [ShellWait, Qt, PassThru, LogError] will be obvious):

UPDATE: I've updated the function to incorporate dmaruca's clsRunApp (my new favorite class module) and the issue raised by Philippe concerning working in disconnected mode. The result is much better than I originally posted and I'd like to thank both of them for their contributions. Here's the function as it stands now:

Function SQLServerDBExists(ComputerName As String, DbName As String) As Boolean
Const LocalHost = "127.0.0.1"
Dim Result As String, RunApp As New clsRunApp

    On Error GoTo Err_SQLServerDBExists

    If ComputerName <> LocalHost And _
       ComputerName <> "." And _
       ComputerName <> Environ("COMPUTERNAME") Then
        'Check for existence of the server using Name Server Lookup'
        Result = RunApp.RunAppWait_CaptureOutput("nslookup " & ComputerName)
        If InStr(Result, "Non-existent domain") Or _
           InStr(Result, "Default servers are not available") Then
            SQLServerDBExists = False
            GoTo Exit_SQLServerDBExists
        End If
    End If

    Result = RunApp.RunAppWait_CaptureOutput("sc \\" & ComputerName & " qdescription MSSQLSERVER")
    If InStr(Result, "SUCCESS") Then
        With PassThru("SELECT Name FROM sysdatabases " & _
                      "WHERE Name='" & DbName & "'", "master", ComputerName)
            SQLServerDBExists = (Not .EOF)
        End With
    End If

Exit_SQLServerDBExists:
    Exit Function
Err_SQLServerDBExists:
    LogError Err.Number, Err.Description, "SQLServerDBExists", "AttachToSQL"
    Resume Exit_SQLServerDBExists
End Function

Note: I realize Environ("COMPUTERNAME") is not a 100% reliable way of determining the computer's name, so feel free to replace that with your own code if you want. I think the lazy approach is sufficient for its purpose here.

mwolfe02
Why are you so excited about clsRunApp (http://www.vbforums.com/showthread.php?p=3720615)? Seems like an overly complex solution to a not very difficult problem. And use of it is in complete contradiction of your decision to use Environ() instead of an API call to get the machine name 100% reliably. Code for that can be found by searching http://mvps.org/access/ .
David-W-Fenton
@Fenton: Can you point me to a less complex solution to the problem of capturing output from a command line program?
mwolfe02
I can only point you to ones that you seem to have rejected as somehow unacceptable.
David-W-Fenton
If you are talking about mvps.org/access, I searched there and the closest thing I found was a ShellWait function (which I've used for some time now). I appreciated the clsRunApp module because my solution to the problem in the past was to call ShellWait and redirect output to a temporary text file...wait for the temporary text file to exist and finish its write process...load one or more lines from the temp file into a variable...kill the temp file. That's been fairly reliable for me, but always felt kludgy. I would still appreciate you pointing me to ones that you believe I've rejected.
mwolfe02
A: 

The solution proposed here works great in specific conditions, where the computer is networked and a dns server is available. If your application is supposed to work in both connected (where you can connect to one of the 'main' servers) and disconnected mode (where you connect to your local copy of the database), this solution will not do it.

Our generic, efficient but (I must admit) not-so-smart solution is until now to have a client-side connection table (in fact xml files - one per connection - in the app folder) and let the user choose the connection at startup. depending on the user, the place(s) he works in, and his ability to work off-line, we can choose which xml-connection strings to install on his computer.

Our idea is (when we'll have time) to use the ip address of the computer to identify/calculate the 'best' database server available: if computer is not networked, connection will be set to 'localhost'. Otherwise, connection will be built 'on the fly', on a network where database servers are given a predefinite ip suffix. Thus, when a computer's ip is aaa.bbb.ccc.ddd, the machine will know it has to connect to aaa.bbb.120.132, where 120.132 is the predefined database server suffix.

Philippe Grondier
+2  A: 

I apologize if I am out of line since I am new here, but I wanted to make a suggestion. You can actually capture the output of shelled commands without using an intermediate text file using windows api. I created a small class file that wraps this up cleanly (link).

Using this you can refactor the code to this and not have to worry about the added headache of reading files.

Function SQLServerDBExists(ServerName As String, DbName As String) As Boolean
Dim Result As String
Dim cls as new clsRunApp

On Error GoTo Err_SQLServerDBExists

'Check for existence of the server
Result = cls.RunAppWait_CaptureOutput("nslookup " & ServerName)
If InStr(Result, "Non-existent domain") Then
    SQLServerDBExists = False
    GoTo Exit_SQLServerDBExists
End If

 Result = cls.RunAppWait_CaptureOutput("sc \\" & ServerName & " qdescription MSSQLSERVER")
If InStr(Result, "SUCCESS") Then
    With PassThru("SELECT Name FROM sysdatabases " & _
                  "WHERE Name='" & DbName & "'", "master", ServerName)
        SQLServerDBExists = (Not .EOF)
    End With
End If

Exit_SQLServerDBExists:
    Exit Function
Err_SQLServerDBExists:
    LogError Err.Number, Err.Description, "SQLServerDBExists", "AttachToSQL", , , Erl
    Resume Exit_SQLServerDBExists
End Function
dmaruca
Not out of line at all. That's a great class module you've got there. I ran into one issue, though: nslookup returned different output using the RunAppWait_CaptureOutput method than it does from the command prompt itself or using my temporary files hack. When I run 'nslookup wcdbs02' (where wcdbs02 is a known good server) at the command prompt I get the full server name and IP address. When I run it using RunAppWait I get the following message: '*** UnKnown can't find wcdbs02: No response from server'. So unless I'm missing something, it looks like nslookup is incompatible with RunAppWait.
mwolfe02
You're right. I've never tried to us nslookup with it, but I get the same type of response. I suspect it has to do with how the process is created. I will look at it more when I get some time later.
dmaruca
dmaruca
mwolfe: The problem was solved in this thread: http://stackoverflow.com/questions/2188885/createprocess-and-strange-nslookup-error Just edit the createprocess line in RunAppWait_CaptureOutput to add that ByVal and it will work.
dmaruca
Works like a charm. Wish I could vote this up more than once.
mwolfe02
A: 

Hello I had a dilemma working in Access97 which this solution will work very well for. I just had a question about the PassThru function that appears in the code which I do not see defined anywhere. Can you elaborate please? Thank you!

jsargent
PassThru returns a DAO recordset. Its three arguments are a SQL string, database name, and server name, respectively. The function creates a new querydef (or you could just re-use a saved querydef that you set aside for this purpose), assigns the SQL string to the .SQL property of the querydef, sets the .ReturnsRecords property of the querydef to True, and sets the .Connect property of the recordset to "ODBC; Driver={SQL Server}; Server=" Database=" Trusted_Connection=Yes;"
mwolfe02