Private/Mongo.ps1
|
# Helpers for the local MongoDB replica set commands. # # An mlaunch replica set is described by a .mlaunch_startup JSON file in the # folder passed to `mlaunch init --dir`. It records the set name, the ports and, # under startup_info, the exact mongod command line for each node, including # --dbpath. We read that instead of guessing folder layouts. # Folders searched when no -Path is given and SECONDLINE_MONGO_DIR is not set. function Get-SLMongoSearchRoots { if ($env:SECONDLINE_MONGO_ROOT) { return @($env:SECONDLINE_MONGO_ROOT -split [IO.Path]::PathSeparator | Where-Object { $_ }) } return @( (Join-Path $HOME 'mongo-dev'), (Join-Path $HOME 'mlaunch'), (Join-Path $HOME 'data') ) } # Reads one .mlaunch_startup and returns a description of the set it defines, # including whether every node's data folder actually exists. function Read-SLMongoStartupFile { param([Parameter(Mandatory)] [string] $File) $dir = Split-Path $File -Parent $set = [pscustomobject]@{ PSTypeName = 'SecondLine.MongoReplicaSet' Name = '' Dir = $dir StartupFile = $File Hostname = 'localhost' Ports = @() Nodes = @() Valid = $false Problem = '' MtoolsVersion = '' } try { $json = Get-Content -LiteralPath $File -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { $set.Problem = "not readable as JSON: $($_.Exception.Message)" return $set } $a = $json.parsed_args if ($null -eq $a) { $set.Problem = 'no parsed_args section'; return $set } if ($a.name) { $set.Name = "$($a.name)" } if ($a.hostname) { $set.Hostname = "$($a.hostname)" } if ($json.mtools_version) { $set.MtoolsVersion = "$($json.mtools_version)" } # A copied startup file (e.g. in a backup folder) still records the folder it # was created for. If that isn't where it sits, it's a copy, not a set. if ($a.dir) { $recorded = (ConvertTo-SLLocalPath "$($a.dir)").TrimEnd([IO.Path]::DirectorySeparatorChar) $actual = (ConvertTo-SLLocalPath $dir).TrimEnd([IO.Path]::DirectorySeparatorChar) if (-not [string]::Equals($recorded, $actual, [StringComparison]::OrdinalIgnoreCase)) { $set.Problem = "copy of the startup file for $recorded (it records that folder, not this one)" return $set } } if (-not $a.replicaset) { $set.Problem = 'not a replica set (mlaunch init was run without --replicaset)'; return $set } if ($a.sharded) { $set.Problem = 'sharded clusters are not supported yet'; return $set } $nodes = @() if ($json.startup_info) { foreach ($prop in $json.startup_info.PSObject.Properties) { $port = 0 if (-not [int]::TryParse($prop.Name, [ref] $port)) { continue } $cmd = "$($prop.Value)" $db = ''; $log = '' if ($cmd -match '--dbpath\s+"([^"]+)"') { $db = $Matches[1] } elseif ($cmd -match '--dbpath\s+(\S+)') { $db = $Matches[1] } if ($cmd -match '--logpath\s+"([^"]+)"') { $log = $Matches[1] } elseif ($cmd -match '--logpath\s+(\S+)') { $log = $Matches[1] } $db = ConvertTo-SLLocalPath $db $log = ConvertTo-SLLocalPath $log $nodes += [pscustomobject]@{ Port = $port DbPath = $db LogPath = $log Exists = [bool]($db -and (Test-Path -LiteralPath $db -PathType Container)) } } } if ($nodes.Count -eq 0 -and $a.port -and $a.nodes) { # Older files without startup_info: ports only, data folders unknown. for ($i = 0; $i -lt [int] $a.nodes; $i++) { $nodes += [pscustomobject]@{ Port = [int] $a.port + $i; DbPath = ''; LogPath = ''; Exists = $false } } } $set.Nodes = @($nodes | Sort-Object Port) $set.Ports = @($set.Nodes | ForEach-Object { $_.Port }) if ($set.Nodes.Count -eq 0) { $set.Problem = 'no nodes recorded'; return $set } $missing = @($set.Nodes | Where-Object { -not $_.Exists }) if ($missing.Count -gt 0) { $what = ($missing | ForEach-Object { if ($_.DbPath) { $_.DbPath } else { "port $($_.Port): data folder not recorded" } }) -join '; ' $set.Problem = "node data folders missing: $what" return $set } $set.Valid = $true return $set } # mlaunch on Windows records paths with mixed separators ("...\replset/rs1\db"). function ConvertTo-SLLocalPath { param([string] $Path) if (-not $Path) { return '' } $sep = [IO.Path]::DirectorySeparatorChar $p = $Path -replace '\\\\', '\' return ($p -replace '[\\/]', $sep) } # Picks the one replica set to act on, or throws a message that says exactly why not. function Resolve-SLMongoReplicaSet { param([string] $Path) if (-not $Path -and $env:SECONDLINE_MONGO_DIR) { $Path = $env:SECONDLINE_MONGO_DIR } if ($Path) { $file = $Path if (Test-Path -LiteralPath $Path -PathType Container) { $file = Join-Path $Path '.mlaunch_startup' } if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "No .mlaunch_startup found at $Path. Point -Path (or SECONDLINE_MONGO_DIR) at the folder you passed to 'mlaunch init --dir'." } $set = Read-SLMongoStartupFile -File (Resolve-Path -LiteralPath $file).ProviderPath if (-not $set.Valid) { throw "Replica set at $($set.Dir) can't be used: $($set.Problem)" } return $set } $all = @(Find-SLMongoReplicaSet) $valid = @($all | Where-Object Valid) if ($valid.Count -eq 1) { return $valid[0] } $roots = (Get-SLMongoSearchRoots) -join ', ' if ($all.Count -eq 0) { throw "No .mlaunch_startup found under: $roots. Pass -Path, or set SECONDLINE_MONGO_DIR to the folder you passed to 'mlaunch init --dir'." } $list = ($all | ForEach-Object { $state = if ($_.Valid) { 'valid' } else { "invalid: $($_.Problem)" } " - $($_.Dir) ($state)" }) -join "`n" if ($valid.Count -eq 0) { throw "Found .mlaunch_startup files, but none describe a usable replica set:`n$list" } throw "Found more than one usable replica set. Pass -Path (or set SECONDLINE_MONGO_DIR) to pick one:`n$list" } # True if something accepts TCP connections on 127.0.0.1:Port. function Test-SLPortOpen { param([Parameter(Mandatory)] [int] $Port, [int] $TimeoutMs = 500) $client = New-Object System.Net.Sockets.TcpClient try { $task = $client.ConnectAsync('127.0.0.1', $Port) if (-not $task.Wait($TimeoutMs)) { return $false } return $client.Connected } catch { return $false } finally { $client.Dispose() } } function Get-SLOpenPorts { param([int[]] $Ports) return @($Ports | Where-Object { Test-SLPortOpen -Port $_ }) } # Waits until every port is open (or, with -Closed, every port is closed). function Wait-SLPorts { param([int[]] $Ports, [int] $TimeoutSeconds, [switch] $Closed) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) do { $open = @(Get-SLOpenPorts $Ports) if ($Closed -and $open.Count -eq 0) { return $true } if (-not $Closed -and $open.Count -eq $Ports.Count) { return $true } Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline) return $false } # Runs one mongosh --eval against a single node, bounded by a timeout. # mongosh can print its answer and then never exit; Invoke-SLTool keeps the # output and stops the process. function Invoke-SLMongosh { param( [Parameter(Mandatory)] [int] $Port, [Parameter(Mandatory)] [string] $Eval, [int] $TimeoutSeconds = 8 ) $cmd = Get-Command mongosh -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $cmd) { return $null } $uri = "mongodb://127.0.0.1:$Port/?directConnection=true&serverSelectionTimeoutMS=2000&connectTimeoutMS=2000" if ($Eval.Contains('"')) { throw 'Invoke-SLMongosh: use single quotes inside -Eval.' } $r = Invoke-SLTool -Path $cmd.Path -Arguments "`"$uri`" --quiet --norc --eval `"$Eval`"" -TimeoutSeconds $TimeoutSeconds $script:SLMongoshTimedOut = [bool] $r.TimedOut if ($r.Error) { return $null } return $r.Output } # Returns the PRIMARY's host:port, or '' if none (yet). function Get-SLMongoPrimary { param([int[]] $Ports) foreach ($p in $Ports) { if (-not (Test-SLPortOpen -Port $p)) { continue } $out = Invoke-SLMongosh -Port $p -Eval "db.hello().primary || ''" $script:SLMongoshAnswered = [bool]$out if ($out) { $line = @($out -split "`r?`n" | Where-Object { $_ -match '^\S+:\d+$' }) | Select-Object -First 1 if ($line) { return $line.Trim() } } return '' # one reachable member is enough to ask } return '' } # Returns @(@{ Name; State }) for each member, or @() if it can't be read. function Get-SLMongoMembers { param([int[]] $Ports) # mongosh just timed out without an answer: don't wait on it a second time. if ($script:SLMongoshTimedOut -and -not $script:SLMongoshAnswered) { return @() } foreach ($p in $Ports) { if (-not (Test-SLPortOpen -Port $p)) { continue } $out = Invoke-SLMongosh -Port $p -Eval "JSON.stringify(rs.status().members.map(m => ({ Name: m.name, State: m.stateStr })))" -TimeoutSeconds 10 if (-not $out) { return @() } $line = @($out -split "`r?`n" | Where-Object { $_.TrimStart().StartsWith('[') }) | Select-Object -First 1 if (-not $line) { return @() } try { return @($line | ConvertFrom-Json) } catch { return @() } } return @() } # mongod processes belonging to THIS set: matched by --dbpath, so nothing else # listening on these ports is ever touched. function Get-SLMongodProcess { param($Set) $dbPaths = @($Set.Nodes | ForEach-Object { $_.DbPath } | Where-Object { $_ }) if ($dbPaths.Count -eq 0) { return @() } $procs = @() if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { try { $procs = @(Get-CimInstance Win32_Process -Filter "Name = 'mongod.exe'" -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Id = [int] $_.ProcessId; CommandLine = "$($_.CommandLine)" } }) } catch { $procs = @() } } if ($procs.Count -eq 0) { $procs = @(Get-Process -Name mongod -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Id = $_.Id; CommandLine = "$($_.CommandLine)" } }) } $mine = @() foreach ($proc in $procs) { $line = ConvertTo-SLLocalPath $proc.CommandLine foreach ($db in $dbPaths) { if ($line.IndexOf($db, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $mine += $proc; break } } } return $mine } function Get-SLMlaunchPath { $cmd = Get-Command mlaunch -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($cmd) { return $cmd.Path } return $null } |