rds_ssm_connect 1.7.14 → 1.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/connect.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { exec } from 'node:child_process'
3
+ import { exec, spawn } from 'node:child_process'
4
4
  import { EventEmitter } from 'node:events'
5
5
  import fs from 'node:fs/promises'
6
6
  import net from 'node:net'
@@ -14,7 +14,7 @@ import {
14
14
  } from './configLoader.js'
15
15
 
16
16
  // Package info for version checking
17
- const packageJson = { name: 'rds_ssm_connect', version: '1.7.14' }
17
+ const packageJson = { name: 'rds_ssm_connect', version: '1.7.15' }
18
18
 
19
19
  const execAsync = promisify(exec)
20
20
 
@@ -83,20 +83,30 @@ async function checkForUpdates() {
83
83
  // Store active child processes for cleanup
84
84
  let activeChildProcesses = []
85
85
 
86
+ // Kill entire process group (shell + aws-vault + session-manager-plugin).
87
+ // Requires the child to have been spawned with `detached: true` so that
88
+ // setsid() makes it a process group leader.
89
+ function killProcessTree(child) {
90
+ if (!child || !child.pid) return
91
+ try {
92
+ process.kill(-child.pid, 'SIGTERM') // negative PID = kill entire process group
93
+ } catch (_err) {
94
+ // ESRCH: process group already exited — safe to ignore
95
+ }
96
+ }
97
+
86
98
  // Handle graceful shutdown
87
99
  function setupProcessCleanup() {
88
- const cleanup = () => {
100
+ const killAll = () => {
89
101
  activeChildProcesses.forEach((child) => {
90
- if (child && !child.killed) {
91
- child.kill('SIGTERM')
92
- }
102
+ killProcessTree(child)
93
103
  })
94
- process.exit(0)
104
+ activeChildProcesses = []
95
105
  }
96
106
 
97
- process.on('SIGINT', cleanup)
98
- process.on('SIGTERM', cleanup)
99
- process.on('exit', cleanup)
107
+ process.on('SIGINT', () => { killAll(); process.exit(0) })
108
+ process.on('SIGTERM', () => { killAll(); process.exit(0) })
109
+ process.on('exit', killAll)
100
110
  }
101
111
 
102
112
  async function readAwsConfig() {
@@ -251,6 +261,7 @@ async function handleTargetNotConnectedError(
251
261
  region,
252
262
  retryCount,
253
263
  maxRetries,
264
+ onChild,
254
265
  ) {
255
266
  // Terminate the disconnected instance
256
267
  await terminateBastionInstance(ENV, instanceId, region)
@@ -270,6 +281,7 @@ async function handleTargetNotConnectedError(
270
281
  region,
271
282
  retryCount + 1,
272
283
  maxRetries,
284
+ onChild,
273
285
  )
274
286
  }
275
287
 
@@ -282,7 +294,11 @@ function executePortForwardingCommand(
282
294
  region,
283
295
  ) {
284
296
  const portForwardingCommand = `aws-vault exec ${ENV} -- aws ssm start-session --region ${region} --target ${instanceId} --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=${rdsEndpoint},portNumber='${remotePort}',localPortNumber='${portNumber}'" --cli-connect-timeout 0`
285
- const child = exec(portForwardingCommand)
297
+ const child = spawn(portForwardingCommand, {
298
+ shell: true,
299
+ detached: true,
300
+ stdio: ['ignore', 'pipe', 'pipe'],
301
+ })
286
302
 
287
303
  // Register child process for cleanup
288
304
  activeChildProcesses.push(child)
@@ -299,6 +315,7 @@ async function startPortForwardingWithConfig(
299
315
  region,
300
316
  retryCount = 0,
301
317
  maxRetries = RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
318
+ onChild,
302
319
  ) {
303
320
  return new Promise((resolve, reject) => {
304
321
  const child = executePortForwardingCommand(
@@ -309,6 +326,11 @@ async function startPortForwardingWithConfig(
309
326
  remotePort,
310
327
  region,
311
328
  )
329
+ // Notify caller of the new child (used for per-connection tracking).
330
+ // If the connection was already disconnected, the callback kills this
331
+ // child immediately so it doesn't linger as an orphan.
332
+ if (onChild) onChild(child)
333
+
312
334
  const sessionState = monitorPortForwardingSession(child)
313
335
 
314
336
  child.on('close', async (code) => {
@@ -331,6 +353,7 @@ async function startPortForwardingWithConfig(
331
353
  region,
332
354
  retryCount,
333
355
  maxRetries,
356
+ onChild,
334
357
  )
335
358
  resolve()
336
359
  } else if (code !== 0) {
@@ -543,6 +566,18 @@ async function connect(projectKey, profile, options = {}) {
543
566
 
544
567
  let manualDisconnect = false
545
568
  let stopKeepalive = null
569
+ let currentChild = null // per-connection child tracking
570
+
571
+ // Called whenever a new child process is spawned for this connection.
572
+ // If disconnect() was already called, kills the child immediately so
573
+ // it doesn't linger as an orphan during retry chains.
574
+ const onChild = (child) => {
575
+ currentChild = child
576
+ if (manualDisconnect) {
577
+ killProcessTree(child)
578
+ activeChildProcesses = activeChildProcesses.filter((p) => p !== child)
579
+ }
580
+ }
546
581
 
547
582
  // Emit status updates
548
583
  const emit = (event, data) => {
@@ -600,6 +635,7 @@ async function connect(projectKey, profile, options = {}) {
600
635
  region,
601
636
  0,
602
637
  RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
638
+ onChild,
603
639
  )
604
640
 
605
641
  // Session ended — clean up keepalive
@@ -672,13 +708,12 @@ async function connect(projectKey, profile, options = {}) {
672
708
  disconnect: () => {
673
709
  manualDisconnect = true
674
710
  stopKeepalive?.()
675
- // Kill all active child processes
676
- activeChildProcesses.forEach((child) => {
677
- if (child && !child.killed) {
678
- child.kill('SIGTERM')
679
- }
680
- })
681
- activeChildProcesses = []
711
+ // Kill only THIS connection's child process group
712
+ if (currentChild) {
713
+ killProcessTree(currentChild)
714
+ activeChildProcesses = activeChildProcesses.filter((p) => p !== currentChild)
715
+ currentChild = null
716
+ }
682
717
  },
683
718
  waitForClose: () => portForwardingPromise,
684
719
  }
package/gui-adapter.js CHANGED
@@ -253,19 +253,19 @@ async function handleCommand(command) {
253
253
  }
254
254
  }
255
255
 
256
- // Handle process signals for cleanup
257
- function setupCleanup() {
258
- const cleanup = () => {
259
- for (const [_connId, connection] of activeConnections) {
260
- connection.disconnect()
261
- }
262
- activeConnections.clear()
263
- process.exit(0)
256
+ // Disconnect all active connections (idempotent — safe to call multiple times)
257
+ function disconnectAll() {
258
+ for (const [_connId, connection] of activeConnections) {
259
+ connection.disconnect()
264
260
  }
261
+ activeConnections.clear()
262
+ }
265
263
 
266
- process.on('SIGINT', cleanup)
267
- process.on('SIGTERM', cleanup)
268
- process.on('exit', cleanup)
264
+ // Handle process signals for cleanup
265
+ function setupCleanup() {
266
+ process.on('SIGINT', () => { disconnectAll(); process.exit(0) })
267
+ process.on('SIGTERM', () => { disconnectAll(); process.exit(0) })
268
+ process.on('exit', disconnectAll)
269
269
  }
270
270
 
271
271
  // Main entry point
@@ -294,10 +294,7 @@ async function main() {
294
294
  })
295
295
 
296
296
  rl.on('close', () => {
297
- for (const [_connId, connection] of activeConnections) {
298
- connection.disconnect()
299
- }
300
- activeConnections.clear()
297
+ disconnectAll()
301
298
  process.exit(0)
302
299
  })
303
300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.7.14",
3
+ "version": "1.7.15",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.svelte CHANGED
@@ -182,7 +182,12 @@ async function confirmClose() {
182
182
  } catch (_err) {
183
183
  // Best-effort disconnect before closing
184
184
  }
185
- await invoke('quit_app')
185
+ try {
186
+ await invoke('quit_app')
187
+ } catch (_err) {
188
+ // If quit_app fails, force close via window API
189
+ appWindow?.close()
190
+ }
186
191
  }
187
192
 
188
193
  function cancelClose() {
@@ -428,8 +433,10 @@ async function handleInstallUpdate() {
428
433
 
429
434
  try {
430
435
  await invoke('install_update')
431
- statusMessage = 'Update installed! Restart to apply.'
432
- // The app will restart automatically after install
436
+ // App should auto-restart and never reach here, but just in case:
437
+ isUpdating = false
438
+ showUpdateBanner = false
439
+ statusMessage = 'Update installed successfully.'
433
440
  } catch (err) {
434
441
  errorMessage = `Update failed: ${err}`
435
442
  isUpdating = false
@@ -3057,7 +3057,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
3057
3057
 
3058
3058
  [[package]]
3059
3059
  name = "rds-ssm-connect"
3060
- version = "1.7.14"
3060
+ version = "1.7.15"
3061
3061
  dependencies = [
3062
3062
  "reqwest 0.12.28",
3063
3063
  "semver",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rds-ssm-connect"
3
- version = "1.7.14"
3
+ version = "1.7.15"
4
4
  description = "Secure RDS connections through AWS SSM"
5
5
  authors = ["Iaroslav Pyrogov"]
6
6
  edition = "2024"
@@ -818,6 +818,8 @@ async fn install_update(app_handle: AppHandle) -> Result<(), String> {
818
818
  .await
819
819
  .map_err(|e| format!("Failed to download/install update: {}", e))?;
820
820
 
821
+ app_handle.restart();
822
+
821
823
  Ok(())
822
824
  }
823
825
 
@@ -836,7 +838,18 @@ async fn open_url(app_handle: AppHandle, url: String) -> Result<(), String> {
836
838
  }
837
839
 
838
840
  #[tauri::command]
839
- async fn quit_app(app_handle: AppHandle) -> Result<(), String> {
841
+ async fn quit_app(
842
+ app_handle: AppHandle,
843
+ state: tauri::State<'_, Arc<TokioMutex<SidecarState>>>,
844
+ ) -> Result<(), String> {
845
+ // Kill the sidecar process (triggers its cleanup handlers which
846
+ // disconnect all active connections and kill process groups)
847
+ {
848
+ let mut guard = state.lock().await;
849
+ if let Some(child) = guard.child.take() {
850
+ let _ = child.kill();
851
+ }
852
+ }
840
853
  app_handle.exit(0);
841
854
  Ok(())
842
855
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "RDS SSM Connect",
4
- "version": "1.7.14",
4
+ "version": "1.7.15",
5
5
  "identifier": "com.rds-ssm-connect.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:vite",