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 @@
2
  * Implements secure account switching and management for gitswitch-c
2
  * Implements secure account switching and management for gitswitch-c
3
  */
3
  */
4
 
4
 
5
+/* Enable POSIX extensions for setenv/unsetenv */
6
+#define _POSIX_C_SOURCE 200809L
7
+
5
 #include <stdio.h>
8
 #include <stdio.h>
6
 #include <stdlib.h>
9
 #include <stdlib.h>
7
 #include <string.h>
10
 #include <string.h>
@@ -18,6 +21,20 @@
18
 #include "ssh_manager.h"
21
 #include "ssh_manager.h"
19
 #include "gpg_manager.h"
22
 #include "gpg_manager.h"
20
 
23
 
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
+
21
 /* Internal helper functions */
38
 /* Internal helper functions */
22
 static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx);
39
 static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx);
23
 static int validate_ssh_key_security(const char *ssh_key_path);
40
 static int validate_ssh_key_security(const char *ssh_key_path);
@@ -31,16 +48,54 @@ int accounts_init(gitswitch_ctx_t *ctx) {
31
         set_error(ERR_INVALID_ARGS, "NULL context to accounts_init");
48
         set_error(ERR_INVALID_ARGS, "NULL context to accounts_init");
32
         return -1;
49
         return -1;
33
     }
50
     }
34
-    
51
+
35
     /* Initialize account array */
52
     /* Initialize account array */
36
     memset(ctx->accounts, 0, sizeof(ctx->accounts));
53
     memset(ctx->accounts, 0, sizeof(ctx->accounts));
37
     ctx->account_count = 0;
54
     ctx->account_count = 0;
38
     ctx->current_account = NULL;
55
     ctx->current_account = NULL;
39
-    
56
+
57
+    /* Initialize session state */
58
+    memset(&g_session, 0, sizeof(g_session));
59
+
40
     log_debug("Accounts system initialized");
60
     log_debug("Accounts system initialized");
41
     return 0;
61
     return 0;
42
 }
62
 }
43
 
63
 
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
+
44
 /* Switch to specified account with SSH isolation and validation */
99
 /* Switch to specified account with SSH isolation and validation */
45
 int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
100
 int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
46
     account_t *account;
101
     account_t *account;
@@ -66,6 +121,21 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
66
         return -1;
121
         return -1;
67
     }
122
     }
68
 
123
 
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
+
69
     /* Determine git scope - use account preference or context default */
139
     /* Determine git scope - use account preference or context default */
70
     git_scope_t scope = account->preferred_scope;
140
     git_scope_t scope = account->preferred_scope;
71
     if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
141
     if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
@@ -105,20 +175,21 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
105
         if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
175
         if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
106
             log_info("Setting up SSH isolation for account: %s", account->name);
176
             log_info("Setting up SSH isolation for account: %s", account->name);
107
 
177
 
108
-            /* Initialize SSH manager with isolated agents */
178
+            /* Initialize SSH manager with isolated agents using session state */
109
-            ssh_config_t ssh_config = {0};
179
+            memset(&g_session.ssh_config, 0, sizeof(g_session.ssh_config));
110
-            if (ssh_manager_init(&ssh_config, SSH_AGENT_ISOLATED) != 0) {
180
+            if (ssh_manager_init(&g_session.ssh_config, SSH_AGENT_ISOLATED) != 0) {
111
                 printf("  [!!] SSH agent failed to start\n");
181
                 printf("  [!!] SSH agent failed to start\n");
112
                 log_warning("Failed to initialize SSH manager: %s", get_last_error()->message);
182
                 log_warning("Failed to initialize SSH manager: %s", get_last_error()->message);
113
             } else {
183
             } else {
114
                 /* Switch to account's SSH configuration */
184
                 /* 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) {
116
                     printf("  [!!] SSH key failed to load\n");
186
                     printf("  [!!] SSH key failed to load\n");
117
                     log_warning("Failed to switch SSH configuration: %s", get_last_error()->message);
187
                     log_warning("Failed to switch SSH configuration: %s", get_last_error()->message);
118
                     /* Clean up SSH manager on failure */
188
                     /* Clean up SSH manager on failure */
119
-                    ssh_manager_cleanup(&ssh_config);
189
+                    ssh_manager_cleanup(&g_session.ssh_config);
120
                 } else {
190
                 } else {
121
                     ssh_ok = true;
191
                     ssh_ok = true;
192
+                    g_session.ssh_active = true;  /* Mark session as active for cleanup */
122
                     printf("  [OK] SSH key loaded\n");
193
                     printf("  [OK] SSH key loaded\n");
123
                     log_info("SSH isolation activated for account: %s", account->name);
194
                     log_info("SSH isolation activated for account: %s", account->name);
124
 
195
 
@@ -144,23 +215,24 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
144
         if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
215
         if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
145
             log_info("Setting up GPG isolation for account: %s", account->name);
216
             log_info("Setting up GPG isolation for account: %s", account->name);
146
 
217
 
147
-            /* Initialize GPG manager with isolated environments */
218
+            /* Initialize GPG manager with isolated environments using session state */
148
-            gpg_config_t gpg_config = {0};
219
+            memset(&g_session.gpg_config, 0, sizeof(g_session.gpg_config));
149
-            if (gpg_manager_init(&gpg_config, GPG_MODE_ISOLATED) != 0) {
220
+            if (gpg_manager_init(&g_session.gpg_config, GPG_MODE_ISOLATED) != 0) {
150
                 printf("  [!!] GPG manager failed to initialize\n");
221
                 printf("  [!!] GPG manager failed to initialize\n");
151
                 log_warning("Failed to initialize GPG manager: %s", get_last_error()->message);
222
                 log_warning("Failed to initialize GPG manager: %s", get_last_error()->message);
152
             } else {
223
             } else {
153
                 /* Switch to account's GPG configuration */
224
                 /* 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) {
155
                     printf("  [!!] GPG key failed to activate\n");
226
                     printf("  [!!] GPG key failed to activate\n");
156
                     log_warning("Failed to switch GPG configuration: %s", get_last_error()->message);
227
                     log_warning("Failed to switch GPG configuration: %s", get_last_error()->message);
157
                     /* Clean up GPG manager on failure */
228
                     /* Clean up GPG manager on failure */
158
-                    gpg_manager_cleanup(&gpg_config);
229
+                    gpg_manager_cleanup(&g_session.gpg_config);
159
                 } else {
230
                 } else {
231
+                    g_session.gpg_active = true;  /* Mark session as active for cleanup */
160
                     log_info("GPG isolation activated for account: %s", account->name);
232
                     log_info("GPG isolation activated for account: %s", account->name);
161
 
233
 
162
                     /* Configure git GPG signing */
234
                     /* 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) {
164
                         printf("  [!!] GPG signing config failed\n");
236
                         printf("  [!!] GPG signing config failed\n");
165
                         log_warning("Failed to configure git GPG signing: %s", get_last_error()->message);
237
                         log_warning("Failed to configure git GPG signing: %s", get_last_error()->message);
166
                     } else {
238
                     } else {
@@ -198,6 +270,15 @@ int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
198
     /* Set as current account */
270
     /* Set as current account */
199
     ctx->current_account = account;
271
     ctx->current_account = account;
200
 
272
 
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
+
201
     printf("\n");
282
     printf("\n");
202
     log_info("Successfully switched to account: %s (%s)", account->name, account->description);
283
     log_info("Successfully switched to account: %s (%s)", account->name, account->description);
203
     return 0;
284
     return 0;
src/accounts.hmodified
@@ -116,4 +116,13 @@ void accounts_cleanup_struct(account_t *account);
116
  */
116
  */
117
 int accounts_health_check(const gitswitch_ctx_t *ctx);
117
 int accounts_health_check(const gitswitch_ctx_t *ctx);
118
 
118
 
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
+
119
 #endif /* ACCOUNTS_H */
128
 #endif /* ACCOUNTS_H */
src/main.cmodified
@@ -15,6 +15,7 @@
15
 #include "display.h"
15
 #include "display.h"
16
 #include "error.h"
16
 #include "error.h"
17
 #include "utils.h"
17
 #include "utils.h"
18
+#include "git_ops.h"
18
 
19
 
19
 static void print_usage(const char *prog_name) {
20
 static void print_usage(const char *prog_name) {
20
     printf("Usage: %s [OPTIONS] [COMMAND] [ARGS]\n", prog_name);
21
     printf("Usage: %s [OPTIONS] [COMMAND] [ARGS]\n", prog_name);
@@ -262,7 +263,11 @@ int main(int argc, char *argv[]) {
262
         }
263
         }
263
     }
264
     }
264
     
265
     
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 */
266
     error_cleanup();
271
     error_cleanup();
267
     return exit_code == EXIT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
272
     return exit_code == EXIT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
268
 }
273
 }
src/ssh_manager.cmodified
@@ -27,6 +27,7 @@ static bool is_ssh_agent_running(pid_t pid);
27
 static int kill_ssh_agent_gracefully(pid_t pid);
27
 static int kill_ssh_agent_gracefully(pid_t pid);
28
 static int validate_ssh_agent_socket(const char *socket_path);
28
 static int validate_ssh_agent_socket(const char *socket_path);
29
 static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config);
29
 static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config);
30
+static void kill_orphaned_gitswitch_agents(void);
30
 
31
 
31
 /* Global SSH configuration for cleanup */
32
 /* Global SSH configuration for cleanup */
32
 static ssh_config_t *g_active_ssh_config = NULL;
33
 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)
216
 
217
 
217
     log_info("Starting isolated SSH agent for account: %s", account->name);
218
     log_info("Starting isolated SSH agent for account: %s", account->name);
218
 
219
 
220
+    /* Kill any orphaned gitswitch agents from previous runs */
221
+    kill_orphaned_gitswitch_agents();
222
+
219
     /* Stop any existing agent we own */
223
     /* Stop any existing agent we own */
220
     if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
224
     if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
221
         log_debug("Stopping existing SSH agent");
225
         log_debug("Stopping existing SSH agent");
@@ -280,8 +284,23 @@ int ssh_start_isolated_agent(ssh_config_t *ssh_config, const account_t *account)
280
         return -1;
284
         return -1;
281
     }
285
     }
282
     
286
     
283
-    log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)", 
287
+    log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)",
284
              ssh_config->agent_pid, ssh_config->agent_socket_path);
288
              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
+
285
     return 0;
304
     return 0;
286
 }
305
 }
287
 
306
 
@@ -846,6 +865,38 @@ static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config)
846
         set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
865
         set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
847
         return -1;
866
         return -1;
848
     }
867
     }
849
-    
868
+
850
     return 0;
869
     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
+    }
851
 }
902
 }