tenseleyflow/gitswitch / c55d684

Browse files

feat: persistent SSH agents with session state tracking

- Add session state tracking to prevent orphaned SSH agents
- SSH agents now persist after gitswitch exits for use by git commands
- Kill orphaned agents from previous runs when switching accounts
- Create current.sock symlink for easy shell integration
- Show shell integration tip after successful switch

To use persistent SSH in your shell, add to your rc file:
export SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/gitswitch-ssh/current.sock
Authored by espadonne
SHA
c55d6840fda46f3bc11230b7268f27f3c03381c3
Parents
aa90aa8
Tree
382c80b

4 changed files

StatusFile+-
M src/accounts.c 94 13
M src/accounts.h 9 0
M src/main.c 6 1
M src/ssh_manager.c 53 2
src/accounts.cmodified
@@ -2,6 +2,9 @@
22
  * Implements secure account switching and management for gitswitch-c
33
  */
44
 
5
+/* Enable POSIX extensions for setenv/unsetenv */
6
+#define _POSIX_C_SOURCE 200809L
7
+
58
 #include <stdio.h>
69
 #include <stdlib.h>
710
 #include <string.h>
@@ -18,6 +21,20 @@
1821
 #include "ssh_manager.h"
1922
 #include "gpg_manager.h"
2023
 
24
+/* Active session state - tracks SSH/GPG resources for proper cleanup */
25
+typedef struct {
26
+    ssh_config_t ssh_config;
27
+    gpg_config_t gpg_config;
28
+    bool ssh_active;
29
+    bool gpg_active;
30
+    char original_gnupghome[MAX_PATH_LEN];
31
+    bool had_original_gnupghome;
32
+    bool gnupghome_saved;
33
+} active_session_t;
34
+
35
+/* Static session state - only one active session at a time */
36
+static active_session_t g_session = {0};
37
+
2138
 /* Internal helper functions */
2239
 static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx);
2340
 static int validate_ssh_key_security(const char *ssh_key_path);
@@ -31,16 +48,54 @@ int accounts_init(gitswitch_ctx_t *ctx) {
3148
         set_error(ERR_INVALID_ARGS, "NULL context to accounts_init");
3249
         return -1;
3350
     }
34
-    
51
+
3552
     /* Initialize account array */
3653
     memset(ctx->accounts, 0, sizeof(ctx->accounts));
3754
     ctx->account_count = 0;
3855
     ctx->current_account = NULL;
39
-    
56
+
57
+    /* Initialize session state */
58
+    memset(&g_session, 0, sizeof(g_session));
59
+
4060
     log_debug("Accounts system initialized");
4161
     return 0;
4262
 }
4363
 
64
+/* Clean up active session resources */
65
+void accounts_session_cleanup(void) {
66
+    log_debug("Cleaning up active session resources");
67
+
68
+    /* Clean up SSH agent if we started one */
69
+    if (g_session.ssh_active) {
70
+        log_info("Stopping SSH agent (pid=%d)", g_session.ssh_config.agent_pid);
71
+        ssh_manager_cleanup(&g_session.ssh_config);
72
+        g_session.ssh_active = false;
73
+    }
74
+
75
+    /* Clean up GPG environment if we modified it */
76
+    if (g_session.gpg_active) {
77
+        log_info("Cleaning up GPG environment");
78
+        gpg_manager_cleanup(&g_session.gpg_config);
79
+        g_session.gpg_active = false;
80
+    }
81
+
82
+    /* Restore original GNUPGHOME environment variable */
83
+    if (g_session.gnupghome_saved) {
84
+        if (g_session.had_original_gnupghome) {
85
+            log_debug("Restoring original GNUPGHOME: %s", g_session.original_gnupghome);
86
+            setenv("GNUPGHOME", g_session.original_gnupghome, 1);
87
+        } else {
88
+            log_debug("Unsetting GNUPGHOME (was not set originally)");
89
+            unsetenv("GNUPGHOME");
90
+        }
91
+        g_session.gnupghome_saved = false;
92
+    }
93
+
94
+    /* Clear session state */
95
+    memset(&g_session, 0, sizeof(g_session));
96
+    log_debug("Session cleanup complete");
97
+}
98
+
4499
 /* Switch to specified account with SSH isolation and validation */
45100
 int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
46101
     account_t *account;
@@ -66,6 +121,21 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
66121
         return -1;
67122
     }
68123
 
124
+    /* Clean up any previous session before starting new one */
125
+    accounts_session_cleanup();
126
+
127
+    /* Save original GNUPGHOME if not already saved */
128
+    if (!g_session.gnupghome_saved) {
129
+        const char *orig = getenv("GNUPGHOME");
130
+        if (orig) {
131
+            safe_strncpy(g_session.original_gnupghome, orig, sizeof(g_session.original_gnupghome));
132
+            g_session.had_original_gnupghome = true;
133
+        } else {
134
+            g_session.had_original_gnupghome = false;
135
+        }
136
+        g_session.gnupghome_saved = true;
137
+    }
138
+
69139
     /* Determine git scope - use account preference or context default */
70140
     git_scope_t scope = account->preferred_scope;
71141
     if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
@@ -105,20 +175,21 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
105175
         if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
106176
             log_info("Setting up SSH isolation for account: %s", account->name);
107177
 
108
-            /* Initialize SSH manager with isolated agents */
109
-            ssh_config_t ssh_config = {0};
110
-            if (ssh_manager_init(&ssh_config, SSH_AGENT_ISOLATED) != 0) {
178
+            /* Initialize SSH manager with isolated agents using session state */
179
+            memset(&g_session.ssh_config, 0, sizeof(g_session.ssh_config));
180
+            if (ssh_manager_init(&g_session.ssh_config, SSH_AGENT_ISOLATED) != 0) {
111181
                 printf("  [!!] SSH agent failed to start\n");
112182
                 log_warning("Failed to initialize SSH manager: %s", get_last_error()->message);
113183
             } else {
114184
                 /* Switch to account's SSH configuration */
115
-                if (ssh_switch_account(&ssh_config, account) != 0) {
185
+                if (ssh_switch_account(&g_session.ssh_config, account) != 0) {
116186
                     printf("  [!!] SSH key failed to load\n");
117187
                     log_warning("Failed to switch SSH configuration: %s", get_last_error()->message);
118188
                     /* Clean up SSH manager on failure */
119
-                    ssh_manager_cleanup(&ssh_config);
189
+                    ssh_manager_cleanup(&g_session.ssh_config);
120190
                 } else {
121191
                     ssh_ok = true;
192
+                    g_session.ssh_active = true;  /* Mark session as active for cleanup */
122193
                     printf("  [OK] SSH key loaded\n");
123194
                     log_info("SSH isolation activated for account: %s", account->name);
124195
 
@@ -144,23 +215,24 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
144215
         if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
145216
             log_info("Setting up GPG isolation for account: %s", account->name);
146217
 
147
-            /* Initialize GPG manager with isolated environments */
148
-            gpg_config_t gpg_config = {0};
149
-            if (gpg_manager_init(&gpg_config, GPG_MODE_ISOLATED) != 0) {
218
+            /* Initialize GPG manager with isolated environments using session state */
219
+            memset(&g_session.gpg_config, 0, sizeof(g_session.gpg_config));
220
+            if (gpg_manager_init(&g_session.gpg_config, GPG_MODE_ISOLATED) != 0) {
150221
                 printf("  [!!] GPG manager failed to initialize\n");
151222
                 log_warning("Failed to initialize GPG manager: %s", get_last_error()->message);
152223
             } else {
153224
                 /* Switch to account's GPG configuration */
154
-                if (gpg_switch_account(&gpg_config, account) != 0) {
225
+                if (gpg_switch_account(&g_session.gpg_config, account) != 0) {
155226
                     printf("  [!!] GPG key failed to activate\n");
156227
                     log_warning("Failed to switch GPG configuration: %s", get_last_error()->message);
157228
                     /* Clean up GPG manager on failure */
158
-                    gpg_manager_cleanup(&gpg_config);
229
+                    gpg_manager_cleanup(&g_session.gpg_config);
159230
                 } else {
231
+                    g_session.gpg_active = true;  /* Mark session as active for cleanup */
160232
                     log_info("GPG isolation activated for account: %s", account->name);
161233
 
162234
                     /* Configure git GPG signing */
163
-                    if (gpg_configure_git_signing(&gpg_config, account, scope) != 0) {
235
+                    if (gpg_configure_git_signing(&g_session.gpg_config, account, scope) != 0) {
164236
                         printf("  [!!] GPG signing config failed\n");
165237
                         log_warning("Failed to configure git GPG signing: %s", get_last_error()->message);
166238
                     } else {
@@ -198,6 +270,15 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
198270
     /* Set as current account */
199271
     ctx->current_account = account;
200272
 
273
+    /* Print shell integration tip if SSH was set up */
274
+    if (ssh_ok) {
275
+        const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
276
+        if (runtime_dir) {
277
+            printf("\n  Tip: Add to your shell rc for persistent SSH:\n");
278
+            printf("       export SSH_AUTH_SOCK=%s/gitswitch-ssh/current.sock\n", runtime_dir);
279
+        }
280
+    }
281
+
201282
     printf("\n");
202283
     log_info("Successfully switched to account: %s (%s)", account->name, account->description);
203284
     return 0;
src/accounts.hmodified
@@ -116,4 +116,13 @@ void accounts_cleanup_struct(account_t *account);
116116
  */
117117
 int accounts_health_check(const gitswitch_ctx_t *ctx);
118118
 
119
+/**
120
+ * Clean up active session resources
121
+ * - Stops SSH agent if one was started
122
+ * - Restores original GNUPGHOME environment
123
+ * - Cleans up isolated GPG home if created
124
+ * Call this before program exit or when switching accounts
125
+ */
126
+void accounts_session_cleanup(void);
127
+
119128
 #endif /* ACCOUNTS_H */
src/main.cmodified
@@ -15,6 +15,7 @@
1515
 #include "display.h"
1616
 #include "error.h"
1717
 #include "utils.h"
18
+#include "git_ops.h"
1819
 
1920
 static void print_usage(const char *prog_name) {
2021
     printf("Usage: %s [OPTIONS] [COMMAND] [ARGS]\n", prog_name);
@@ -262,7 +263,11 @@ int main(int argc, char *argv[]) {
262263
         }
263264
     }
264265
     
265
-    /* Cleanup */
266
+    /* Note: We intentionally do NOT clean up SSH agents on exit.
267
+     * The agent should persist so subsequent git commands can use it.
268
+     * Cleanup happens at the start of the next account switch. */
269
+
270
+    /* Cleanup error handling */
266271
     error_cleanup();
267272
     return exit_code == EXIT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
268273
 }
src/ssh_manager.cmodified
@@ -27,6 +27,7 @@ static bool is_ssh_agent_running(pid_t pid);
2727
 static int kill_ssh_agent_gracefully(pid_t pid);
2828
 static int validate_ssh_agent_socket(const char *socket_path);
2929
 static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config);
30
+static void kill_orphaned_gitswitch_agents(void);
3031
 
3132
 /* Global SSH configuration for cleanup */
3233
 static ssh_config_t *g_active_ssh_config = NULL;
@@ -216,6 +217,9 @@ int ssh_start_isolated_agent(ssh_config_t *ssh_config, const account_t *account)
216217
 
217218
     log_info("Starting isolated SSH agent for account: %s", account->name);
218219
 
220
+    /* Kill any orphaned gitswitch agents from previous runs */
221
+    kill_orphaned_gitswitch_agents();
222
+
219223
     /* Stop any existing agent we own */
220224
     if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
221225
         log_debug("Stopping existing SSH agent");
@@ -280,8 +284,23 @@ int ssh_start_isolated_agent(ssh_config_t *ssh_config, const account_t *account)
280284
         return -1;
281285
     }
282286
     
283
-    log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)", 
287
+    log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)",
284288
              ssh_config->agent_pid, ssh_config->agent_socket_path);
289
+
290
+    /* Create/update symlink for easy shell integration */
291
+    char symlink_path[MAX_PATH_LEN];
292
+    if ((size_t)snprintf(symlink_path, sizeof(symlink_path),
293
+                        "%s/current.sock", socket_dir) < sizeof(symlink_path)) {
294
+        /* Remove old symlink if it exists */
295
+        unlink(symlink_path);
296
+        /* Create new symlink to current socket */
297
+        if (symlink(socket_path, symlink_path) == 0) {
298
+            log_debug("Created symlink: %s -> %s", symlink_path, socket_path);
299
+        } else {
300
+            log_warning("Failed to create socket symlink: %s", strerror(errno));
301
+        }
302
+    }
303
+
285304
     return 0;
286305
 }
287306
 
@@ -846,6 +865,38 @@ static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config)
846865
         set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
847866
         return -1;
848867
     }
849
-    
868
+
850869
     return 0;
870
+}
871
+
872
+/* Kill orphaned gitswitch ssh-agents from previous runs */
873
+static void kill_orphaned_gitswitch_agents(void) {
874
+    char socket_dir[256];
875
+    const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
876
+
877
+    /* Determine socket directory - use short buffer since runtime_dir is typically short */
878
+    if (runtime_dir && strlen(runtime_dir) < 200 && path_exists(runtime_dir)) {
879
+        snprintf(socket_dir, sizeof(socket_dir), "%s/gitswitch-ssh", runtime_dir);
880
+    } else {
881
+        snprintf(socket_dir, sizeof(socket_dir), "/tmp/gitswitch-ssh-%d", getuid());
882
+    }
883
+
884
+    /* Use pkill to kill any existing gitswitch ssh-agents */
885
+    char command[512];
886
+    snprintf(command, sizeof(command),
887
+             "pkill -f 'ssh-agent -a %.200s/' 2>/dev/null", socket_dir);
888
+
889
+    int result = system(command);
890
+    if (result == 0) {
891
+        log_debug("Killed orphaned gitswitch ssh-agents");
892
+    }
893
+    /* result != 0 is normal if no agents were running */
894
+
895
+    /* Also clean up any stale socket files */
896
+    char cleanup_cmd[512];
897
+    snprintf(cleanup_cmd, sizeof(cleanup_cmd),
898
+             "rm -f %.200s/ssh-agent.*.sock 2>/dev/null", socket_dir);
899
+    if (system(cleanup_cmd) != 0) {
900
+        /* Ignore cleanup failures - files may not exist */
901
+    }
851902
 }