Skip to content

Commit c103e26

Browse files
Restore error printing for non-timeout errors and add documentation
- Changed GetLocalServerInstances() to return ([]string, error) - Only return error if NOT os.ErrDeadlineExceeded (timeout is expected) - ListLocalServers() prints errors to stderr (matches ODBC sqlcmd behavior) - Expanded README documentation for :serverlist command - Added batch script examples for error handling and automation
1 parent f241917 commit c103e26

3 files changed

Lines changed: 59 additions & 10 deletions

File tree

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,40 @@ switches are most important to you to have implemented next in the new sqlcmd.
155155
- The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces.
156156
- Sqlcmd can now print results using a vertical format. Use the new `--vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable.
157157
- `:help` displays a list of available sqlcmd commands.
158-
- `:serverlist` lists local SQL Server instances discovered via the SQL Server Browser service.
158+
- `:serverlist` lists local SQL Server instances discovered via the SQL Server Browser service (UDP port 1434). The command queries the SQL Browser service and displays the server name and instance name for each discovered instance. If no instances are found or the Browser service is not running, no output is produced. Non-timeout errors are printed to stderr.
159+
160+
```
161+
1> :serverlist
162+
MYSERVER\SQL2019
163+
MYSERVER\SQL2022
164+
```
165+
166+
#### Using :serverlist in batch scripts
167+
168+
When automating server discovery, you can capture the output and check for errors:
169+
170+
```batch
171+
@echo off
172+
REM Discover local SQL Server instances and connect to the first one
173+
sqlcmd -Q ":serverlist" 2>nul > servers.txt
174+
if %errorlevel% neq 0 (
175+
echo Error discovering servers
176+
exit /b 1
177+
)
178+
for /f "tokens=1" %%s in (servers.txt) do (
179+
echo Connecting to %%s...
180+
sqlcmd -S %%s -Q "SELECT @@SERVERNAME"
181+
goto :done
182+
)
183+
echo No SQL Server instances found
184+
:done
185+
```
186+
187+
To capture stderr separately (for error logging):
188+
```batch
189+
sqlcmd -Q ":serverlist" 2>errors.log > servers.txt
190+
if exist errors.log if not "%%~z errors.log"=="0" type errors.log
191+
```
159192

160193
```
161194
1> select session_id, client_interface_name, program_name from sys.dm_exec_sessions where session_id=@@spid

pkg/sqlcmd/serverlist.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package sqlcmd
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"io"
1011
"net"
12+
"os"
1113
"sort"
1214
"strings"
1315
"time"
@@ -18,15 +20,19 @@ import (
1820
// ListLocalServers queries the SQL Browser service for available SQL Server instances
1921
// and writes the results to the provided writer.
2022
func ListLocalServers(w io.Writer) {
21-
instances := GetLocalServerInstances()
23+
instances, err := GetLocalServerInstances()
24+
if err != nil {
25+
fmt.Fprintln(os.Stderr, err)
26+
}
2227
for _, s := range instances {
2328
fmt.Fprintf(w, " %s\n", s)
2429
}
2530
}
2631

2732
// GetLocalServerInstances queries the SQL Browser service and returns a list of
2833
// available SQL Server instances on the local machine.
29-
func GetLocalServerInstances() []string {
34+
// Returns an error for non-timeout network errors.
35+
func GetLocalServerInstances() ([]string, error) {
3036
bmsg := []byte{byte(msdsn.BrowserAllInstances)}
3137
resp := make([]byte, 16*1024-1)
3238
dialer := &net.Dialer{}
@@ -35,20 +41,26 @@ func GetLocalServerInstances() []string {
3541
conn, err := dialer.DialContext(ctx, "udp", ":1434")
3642
// silently ignore failures to connect, same as ODBC
3743
if err != nil {
38-
return nil
44+
return nil, nil
3945
}
4046
defer conn.Close()
4147
dl, _ := ctx.Deadline()
4248
_ = conn.SetDeadline(dl)
4349
_, err = conn.Write(bmsg)
4450
if err != nil {
45-
// Silently ignore errors, same as ODBC
46-
return nil
51+
// Only return error if it's not a timeout
52+
if !errors.Is(err, os.ErrDeadlineExceeded) {
53+
return nil, err
54+
}
55+
return nil, nil
4756
}
4857
read, err := conn.Read(resp)
4958
if err != nil {
50-
// Silently ignore errors, same as ODBC
51-
return nil
59+
// Only return error if it's not a timeout
60+
if !errors.Is(err, os.ErrDeadlineExceeded) {
61+
return nil, err
62+
}
63+
return nil, nil
5264
}
5365

5466
data := parseInstances(resp[:read])
@@ -73,7 +85,7 @@ func GetLocalServerInstances() []string {
7385
instances = append(instances, fmt.Sprintf(`%s\%s`, serverName, s))
7486
}
7587
}
76-
return instances
88+
return instances, nil
7789
}
7890

7991
func parseInstances(msg []byte) msdsn.BrowserData {

pkg/sqlcmd/serverlist_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ func TestListLocalServers(t *testing.T) {
2222

2323
func TestGetLocalServerInstances(t *testing.T) {
2424
// Test that GetLocalServerInstances returns a slice (may be empty if no servers)
25-
instances := GetLocalServerInstances()
25+
instances, err := GetLocalServerInstances()
2626
// instances may be nil or empty if no SQL Browser is running, that's OK
27+
// err may be non-nil for non-timeout network errors
28+
if err != nil {
29+
t.Logf("GetLocalServerInstances returned error (expected in some environments): %v", err)
30+
}
2731
t.Logf("Found %d instances", len(instances))
2832
for _, inst := range instances {
2933
assert.NotEmpty(t, inst, "Instance name should not be empty")

0 commit comments

Comments
 (0)