Skip to content

Commit 736007e

Browse files
Import-DbaSpConfigure - Stop eating the caller loop and skipping its own cleanup (#10641)
1 parent bcfb5ab commit 736007e

2 files changed

Lines changed: 89 additions & 7 deletions

File tree

public/Import-DbaSpConfigure.ps1

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ function Import-DbaSpConfigure {
135135
}
136136

137137
if (-not (Test-SqlSa -SqlInstance $sourceserver -SqlCredential $SourceSqlCredential)) {
138-
Stop-Function -Message "Not a sysadmin on $sourceserver. Quitting." -Category PermissionDenied -Target $sourceserver -Continue
138+
# No -Continue on these guards: the begin block has no enclosing loop, so the continue
139+
# would escape the command, eat an iteration of whatever loop the caller runs in, and
140+
# skip the connection cleanup in the end block.
141+
Stop-Function -Message "Not a sysadmin on $sourceserver. Quitting." -Category PermissionDenied -Target $sourceserver
142+
return
139143
}
140144

141145
try {
@@ -151,7 +155,8 @@ function Import-DbaSpConfigure {
151155
}
152156

153157
if (-not (Test-SqlSa -SqlInstance $destserver -SqlCredential $DestinationSqlCredential)) {
154-
Stop-Function -Message "Not a sysadmin on $destserver. Quitting." -Category PermissionDenied -Target $destserver -Continue
158+
Stop-Function -Message "Not a sysadmin on $destserver. Quitting." -Category PermissionDenied -Target $destserver
159+
return
155160
}
156161

157162
$source = $sourceserver.DomainInstanceName
@@ -170,11 +175,13 @@ function Import-DbaSpConfigure {
170175
}
171176

172177
if (!(Test-SqlSa -SqlInstance $server -SqlCredential $SqlCredential)) {
173-
Stop-Function -Message "Not a sysadmin on $server. Quitting." -Category PermissionDenied -Target $server -Continue
178+
Stop-Function -Message "Not a sysadmin on $server. Quitting." -Category PermissionDenied -Target $server
179+
return
174180
}
175181

176182
if (-not (Test-Path $Path)) {
177-
Stop-Function -Message "File $Path Not Found" -Category InvalidArgument -Target $Path -Continue
183+
Stop-Function -Message "File $Path Not Found" -Category InvalidArgument -Target $Path
184+
return
178185
}
179186
}
180187

@@ -294,9 +301,10 @@ function Import-DbaSpConfigure {
294301
}
295302
}
296303
end {
297-
if (Test-FunctionInterrupt) { return }
298-
299-
# Only close the connections that were opened here. See #10554.
304+
# Only close the connections that were opened here, and close them before the interrupt
305+
# return below: a begin-block guard sets the interrupt after a connection was already
306+
# opened (a missing -Path, a failed sysadmin check), and returning first would leak it
307+
# on every guard path. See #10554.
300308
if ($isNewServerConnection) {
301309
$server.ConnectionContext.Disconnect()
302310
}
@@ -307,6 +315,9 @@ function Import-DbaSpConfigure {
307315
$destserver.ConnectionContext.Disconnect()
308316
}
309317

318+
# Only the finished message stays suppressed when the command was interrupted.
319+
if (Test-FunctionInterrupt) { return }
320+
310321
If ($Pscmdlet.ShouldProcess("console", "Showing finished message")) {
311322
Write-Message -Level Output -Message "SQL Server configuration options migration finished."
312323
}

tests/Import-DbaSpConfigure.Tests.ps1

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ Describe $CommandName -Tag IntegrationTests {
5050
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
5151
}
5252

53+
Context "A missing file does not eat an iteration of the caller's loop" {
54+
It "Warns and completes every iteration" {
55+
# The begin block guards used to run Stop-Function -Continue without an enclosing loop -
56+
# the continue escaped the command and consumed an iteration of this very loop, so the
57+
# counter fell short (#10638).
58+
$loopCount = 0
59+
foreach ($i in 1..3) {
60+
$null = Import-DbaSpConfigure -SqlInstance $TestConfig.InstanceSingle -Path "$exportPath\does-not-exist.sql" -WarningAction SilentlyContinue
61+
$loopCount++
62+
}
63+
$loopCount | Should -Be 3
64+
$WarnVar | Should -BeLike "*Not Found*"
65+
}
66+
}
67+
5368
Context "The connection of the caller is left alone when importing from a file (#10554)" {
5469
BeforeAll {
5570
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
@@ -246,4 +261,60 @@ SELECT name, value, value_in_use FROM sys.configurations WHERE name IN ('cost th
246261
$WarnVar[-1] | Should -Match "Some configuration options will be updated once SQL Server is restarted"
247262
}
248263
}
264+
265+
Context "A guard interrupt still closes the connection the command opened (#10554)" {
266+
BeforeAll {
267+
# This pins the invariant that a guard interrupt leaves no session of the command
268+
# behind. Probed while writing it: SMO's auto-disconnect returns the physical
269+
# connection after every batch for every connection the command opens itself - the
270+
# skipped end-block disconnect was therefore not observable on any reachable input
271+
# shape, and this test also passes on the unfixed code. It stands guard for the day a
272+
# connection is held open eagerly. The application name marks the session so the count
273+
# below finds exactly this one; Pooling=False makes a survivor impossible to miss.
274+
$guardAppName = "dbatoolsci_spconfigure_guard_$(Get-Random)"
275+
$guardConnectionString = "Data Source=$($TestConfig.InstanceSingle);Integrated Security=True;Trust Server Certificate=True;Pooling=False;Application Name=$guardAppName"
276+
277+
# The missing file is the guard under test: the begin block has already opened the
278+
# connection when it stops, so the end block cleanup must run despite the interrupt.
279+
$splatGuardImport = @{
280+
SqlInstance = $guardConnectionString
281+
Path = "$exportPath\does-not-exist.sql"
282+
WarningAction = "SilentlyContinue"
283+
}
284+
$null = Import-DbaSpConfigure @splatGuardImport
285+
# Invoke-DbaQuery below writes to $WarnVar as well, so it has to be kept here.
286+
$guardWarnings = $WarnVar
287+
288+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
289+
290+
$guardSessionQuery = @"
291+
SELECT COUNT(*) AS SessionCount FROM sys.dm_exec_sessions WHERE program_name = '$guardAppName'
292+
"@
293+
$guardSessionCount = (Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Query $guardSessionQuery).SessionCount
294+
295+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
296+
}
297+
298+
AfterAll {
299+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
300+
301+
# On a defective command the non-pooled session survives - close the cached connection
302+
# and kill any remaining marked session so nothing leaks into later tests.
303+
$guardEntry = Get-DbaConnectedInstance | Where-Object ConnectionString -match $guardAppName
304+
if ($guardEntry) {
305+
$null = $guardEntry.ConnectionObject | Disconnect-DbaInstance
306+
}
307+
$null = Get-DbaProcess -SqlInstance $TestConfig.InstanceSingle -Program $guardAppName -WarningAction SilentlyContinue | Stop-DbaProcess -WarningAction SilentlyContinue
308+
309+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
310+
}
311+
312+
It "warns about the missing file" {
313+
$guardWarnings | Should -BeLike "*Not Found*"
314+
}
315+
316+
It "closes the non-pooled connection it opened although the guard interrupted the command" {
317+
$guardSessionCount | Should -Be 0
318+
}
319+
}
249320
}

0 commit comments

Comments
 (0)