11package io .github .quafadas .sjsls
22
3- import cats .effect .IO
3+ import scala .concurrent .duration .*
4+
5+ import com .comcast .ip4s .Port
6+
47import fs2 .io .process
8+
9+ import cats .effect .IO
510import cats .effect .kernel .Resource
6- import com .comcast .ip4s .Port
7- import scala .concurrent .duration ._
811
9- private [sjsls] def checkPortInUse (port : Port ): IO [Boolean ] = {
12+ private [sjsls] def checkPortInUse (port : Port ): IO [Boolean ] =
1013 val osName = System .getProperty(" os.name" ).toLowerCase
1114 val portInt = port.value
1215
13- if (osName.contains(" win" )) {
14- val ps = s " Get-NetTCPConnection -LocalPort $portInt -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count "
16+ if osName.contains(" win" ) then
17+ val ps =
18+ s " Get-NetTCPConnection -LocalPort $portInt -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count "
1519
16- process.ProcessBuilder (" powershell" , " -NoProfile" , " -ExecutionPolicy" , " Bypass" , " -Command" , ps)
20+ process
21+ .ProcessBuilder (" powershell" , " -NoProfile" , " -ExecutionPolicy" , " Bypass" , " -Command" , ps)
1722 .spawn[IO ]
18- .use { proc =>
19- proc.stdout.through(fs2.text.utf8.decode).compile.string.map(_.trim.toIntOption.getOrElse(0 ) > 0 )
23+ .use {
24+ proc =>
25+ proc.stdout.through(fs2.text.utf8.decode).compile.string.map(_.trim.toIntOption.getOrElse(0 ) > 0 )
2026 }
21- } else {
27+ else
2228 val sh = s " lsof -ti tcp: $portInt 2>/dev/null | wc -l "
2329
24- process.ProcessBuilder (" sh" , " -c" , sh)
30+ process
31+ .ProcessBuilder (" sh" , " -c" , sh)
2532 .spawn[IO ]
26- .use { proc =>
27- proc.stdout.through(fs2.text.utf8.decode).compile.string.map(_.trim.toIntOption.getOrElse(0 ) > 0 )
33+ .use {
34+ proc =>
35+ proc.stdout.through(fs2.text.utf8.decode).compile.string.map(_.trim.toIntOption.getOrElse(0 ) > 0 )
2836 }
29- }
30- }
37+ end if
38+ end checkPortInUse
3139
32- private [sjsls] def dezombify (port : Port ): Resource [IO , Unit ] = {
40+ private [sjsls] def dezombify (port : Port ): Resource [IO , Unit ] =
3341 val portInt = port.value
3442
35- val checkAndKill = for {
43+ val checkAndKill = for
3644 _ <- scribe.cats[IO ].debug(s " Checking if port $portInt is in use before attempting cleanup " )
3745 portInUse <- checkPortInUse(port)
38- _ <- if (portInUse) {
39- scribe.cats[IO ].warn(s " Found zombie server on port $portInt - attempting to kill it " )
40- } else {
41- scribe.cats[IO ].debug(s " Port $portInt appears to be free, no zombie cleanup needed " )
42- }
43- _ <- if (portInUse) killProcessesOnPort(port) else IO .unit
44- _ <- if (portInUse) {
45- for {
46- _ <- IO .sleep(scala.concurrent.duration.Duration .fromNanos(500_000_000 )) // 500ms
47- stillInUse <- checkPortInUse(port)
48- _ <- if (stillInUse) {
49- scribe.cats[IO ].error(s " Port $portInt still appears to be in use after cleanup attempt " )
50- } else {
51- scribe.cats[IO ].debug(s " Successfully cleaned up port $portInt" )
52- }
53- } yield ()
54- } else IO .unit
55- } yield ()
46+ _ <-
47+ if portInUse then scribe.cats[IO ].warn(s " Found zombie server on port $portInt - attempting to kill it " )
48+ else scribe.cats[IO ].debug(s " Port $portInt appears to be free, no zombie cleanup needed " )
49+ _ <- if portInUse then killProcessesOnPort(port) else IO .unit
50+ _ <-
51+ if portInUse then
52+ for
53+ _ <- IO .sleep(scala.concurrent.duration.Duration .fromNanos(500_000_000 )) // 500ms
54+ stillInUse <- checkPortInUse(port)
55+ _ <-
56+ if stillInUse then scribe.cats[IO ].error(s " Port $portInt still appears to be in use after cleanup attempt " )
57+ else scribe.cats[IO ].debug(s " Successfully cleaned up port $portInt" )
58+ yield ()
59+ else IO .unit
60+ yield ()
5661
5762 checkAndKill.toResource
63+ end dezombify
5864
59- }
60-
61- private def killProcessesOnPort (port : Port ): IO [Unit ] = {
65+ private def killProcessesOnPort (port : Port ): IO [Unit ] =
6266 val osName = System .getProperty(" os.name" ).toLowerCase
6367 val portInt = port.value
6468
65- if ( osName.contains(" win" )) {
69+ if osName.contains(" win" ) then
6670 // Windows: try PowerShell Get-NetTCPConnection, fallback to netstat/taskkill
6771 val ps = s """
6872 |if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) {
@@ -87,15 +91,16 @@ private def killProcessesOnPort(port: Port): IO[Unit] = {
8791 |}
8892 | """ .stripMargin
8993
90- for {
94+ for
9195 _ <- scribe.cats[IO ].debug(s " Running Windows cleanup command for port $portInt" )
92- exitCode <- process.ProcessBuilder (" powershell" , " -NoProfile" , " -ExecutionPolicy" , " Bypass" , " -Command" , ps)
96+ exitCode <- process
97+ .ProcessBuilder (" powershell" , " -NoProfile" , " -ExecutionPolicy" , " Bypass" , " -Command" , ps)
9398 .spawn[IO ]
9499 .use(_.exitValue)
95100 _ <- scribe.cats[IO ].debug(s " Windows cleanup command completed with exit code $exitCode" )
96- } yield ()
97-
98- } else {
101+ yield ()
102+ end for
103+ else
99104 // macOS/Linux: use lsof if available, fallback to fuser
100105 val sh = s """
101106 |if command -v lsof >/dev/null 2>&1; then
@@ -116,17 +121,20 @@ private def killProcessesOnPort(port: Port): IO[Unit] = {
116121 |fi
117122 | """ .stripMargin
118123
119- for {
124+ for
120125 _ <- scribe.cats[IO ].debug(s " Running Unix cleanup command for port $portInt" )
121126 // Use a timeout to prevent hanging
122- result <- process.ProcessBuilder (" sh" , " -c" , sh)
127+ result <- process
128+ .ProcessBuilder (" sh" , " -c" , sh)
123129 .spawn[IO ]
124130 .use(_.exitValue)
125131 .timeout(5 .seconds)
126- .handleErrorWith { err =>
127- scribe.cats[IO ].warn(s " Process cleanup timed out or failed: ${err.getMessage}" ) *> IO .pure(- 1 )
132+ .handleErrorWith {
133+ err =>
134+ scribe.cats[IO ].warn(s " Process cleanup timed out or failed: ${err.getMessage}" ) *> IO .pure(- 1 )
128135 }
129136 _ <- scribe.cats[IO ].debug(s " Unix cleanup command completed with exit code $result" )
130- } yield ()
131- }
132- }
137+ yield ()
138+ end for
139+ end if
140+ end killProcessesOnPort
0 commit comments