@@ -47,6 +47,24 @@ fn create_container_labels(
4747 labels
4848}
4949
50+ /// Calculate an approximate creation time based on container's created timestamp
51+ /// This converts a Unix timestamp to an Instant for session tracking
52+ fn calculate_session_created_at ( created_ts : Option < i64 > ) -> std:: time:: Instant {
53+ if let Some ( created_ts) = created_ts {
54+ let now = std:: time:: SystemTime :: now ( )
55+ . duration_since ( std:: time:: UNIX_EPOCH )
56+ . unwrap ( )
57+ . as_secs ( ) as i64 ;
58+ let age_secs = now - created_ts;
59+
60+ std:: time:: Instant :: now ( )
61+ . checked_sub ( std:: time:: Duration :: from_secs ( age_secs. max ( 0 ) as u64 ) )
62+ . unwrap_or_else ( || std:: time:: Instant :: now ( ) )
63+ } else {
64+ std:: time:: Instant :: now ( )
65+ }
66+ }
67+
5068/// Restore sessions from existing Docker containers on server startup
5169/// This allows pre-existing containers to be reconnected after server restart
5270pub async fn restore_sessions_from_containers ( state : & AppState ) {
@@ -72,47 +90,70 @@ pub async fn restore_sessions_from_containers(state: &AppState) {
7290 let project_owner_id = labels. get ( "project_owner_id" ) . and_then ( |s| s. parse :: < i64 > ( ) . ok ( ) ) ;
7391 let project_slug = labels. get ( "project_slug" ) . map ( |s| s. clone ( ) ) ;
7492
75- if let ( Some ( session_id) , Some ( names) ) = ( session_id, container. names ) {
76- let container_name = names. first ( )
77- . map ( |n| n. trim_start_matches ( '/' ) . to_string ( ) )
78- . unwrap_or_default ( ) ;
93+ let container_name = container. names
94+ . as_ref ( )
95+ . and_then ( |names| names. first ( ) )
96+ . map ( |n| n. trim_start_matches ( '/' ) . to_string ( ) )
97+ . unwrap_or_default ( ) ;
98+
99+ if container_name. is_empty ( ) {
100+ continue ;
101+ }
102+
103+ // Handle new containers (with session_id label)
104+ if let Some ( session_id) = session_id {
105+ let mut map = state. lock_sessions ( ) ;
79106
80- if !container_name. is_empty ( ) {
81- // Restore session to in-memory map
107+ if !map. contains_key ( & session_id) {
108+ let created_at = calculate_session_created_at ( container. created ) ;
109+
110+ map. insert ( session_id. clone ( ) , SessionContext {
111+ container_name : container_name. clone ( ) ,
112+ shell,
113+ pending_image_tag : None ,
114+ owner_id,
115+ project_owner_id,
116+ is_publishing : false ,
117+ project_slug,
118+ created_at,
119+ is_ws_connected : false ,
120+ } ) ;
121+ restored += 1 ;
122+ tracing:: info!( "Restored session {} with container {}" , session_id, container_name) ;
123+ }
124+ } else {
125+ // Handle legacy containers (without session_id label)
126+ // Extract UUID from container name for use as session_id
127+ let legacy_session_id = if container_name. starts_with ( "trycli-studio-viewer-" ) {
128+ container_name. strip_prefix ( "trycli-studio-viewer-" ) . map ( |s| s. to_string ( ) )
129+ } else if container_name. starts_with ( "trycli-studio-session-" ) {
130+ container_name. strip_prefix ( "trycli-studio-session-" ) . map ( |s| s. to_string ( ) )
131+ } else {
132+ None
133+ } ;
134+
135+ if let Some ( legacy_session_id) = legacy_session_id {
82136 let mut map = state. lock_sessions ( ) ;
83137
84- // Only restore if not already present (shouldn't happen, but be defensive)
85- if !map. contains_key ( & session_id) {
86- // Calculate elapsed time from container creation
87- let created_at = if let Some ( created_ts) = container. created {
88- // Container age in seconds
89- let now = std:: time:: SystemTime :: now ( )
90- . duration_since ( std:: time:: UNIX_EPOCH )
91- . unwrap ( )
92- . as_secs ( ) as i64 ;
93- let age_secs = now - created_ts;
94-
95- // Set created_at to approximate original time
96- std:: time:: Instant :: now ( )
97- . checked_sub ( std:: time:: Duration :: from_secs ( age_secs. max ( 0 ) as u64 ) )
98- . unwrap_or_else ( || std:: time:: Instant :: now ( ) )
99- } else {
100- std:: time:: Instant :: now ( )
101- } ;
138+ if !map. contains_key ( & legacy_session_id) {
139+ let created_at = calculate_session_created_at ( container. created ) ;
140+
141+ // For legacy containers, we don't know the exact metadata
142+ // Set reasonable defaults based on container type
102143
103- map. insert ( session_id . clone ( ) , SessionContext {
144+ map. insert ( legacy_session_id . clone ( ) , SessionContext {
104145 container_name : container_name. clone ( ) ,
105146 shell,
106147 pending_image_tag : None ,
107- owner_id,
108- project_owner_id,
148+ owner_id : None , // Unknown for legacy containers
149+ project_owner_id : None , // Unknown for legacy containers
109150 is_publishing : false ,
110- project_slug,
151+ project_slug : None , // Unknown for legacy containers
111152 created_at,
112- is_ws_connected : false , // Will be set to true on reconnection
153+ is_ws_connected : false ,
113154 } ) ;
114155 restored += 1 ;
115- tracing:: info!( "Restored session {} with container {}" , session_id , container_name) ;
156+ tracing:: info!( "Restored legacy session {} from container {}" , legacy_session_id , container_name) ;
116157 }
117158 }
118159 }
@@ -171,7 +212,8 @@ pub async fn ws_handler(
171212/// Attempt to restore a specific session from Docker containers
172213/// This is called when a client tries to connect to a session that isn't in memory
173214async fn restore_specific_session ( state : & AppState , session_id : & str ) {
174- let filters = HashMap :: from ( [
215+ // First, try to find a container with the session_id label (new containers)
216+ let filters_with_label = HashMap :: from ( [
175217 ( "label" . to_string ( ) , vec ! [
176218 "managed_by=TryCli Studio" . to_string( ) ,
177219 format!( "session_id={}" , session_id)
@@ -180,7 +222,7 @@ async fn restore_specific_session(state: &AppState, session_id: &str) {
180222
181223 let opts = ListContainersOptions {
182224 all : false , // Only running containers
183- filters,
225+ filters : filters_with_label ,
184226 ..Default :: default ( )
185227 } ;
186228
@@ -198,20 +240,7 @@ async fn restore_specific_session(state: &AppState, session_id: &str) {
198240 . unwrap_or_default ( ) ;
199241
200242 if !container_name. is_empty ( ) {
201- // Calculate elapsed time from container creation
202- let created_at = if let Some ( created_ts) = container. created {
203- let now = std:: time:: SystemTime :: now ( )
204- . duration_since ( std:: time:: UNIX_EPOCH )
205- . unwrap ( )
206- . as_secs ( ) as i64 ;
207- let age_secs = now - created_ts;
208-
209- std:: time:: Instant :: now ( )
210- . checked_sub ( std:: time:: Duration :: from_secs ( age_secs. max ( 0 ) as u64 ) )
211- . unwrap_or_else ( || std:: time:: Instant :: now ( ) )
212- } else {
213- std:: time:: Instant :: now ( )
214- } ;
243+ let created_at = calculate_session_created_at ( container. created ) ;
215244
216245 let mut map = state. lock_sessions ( ) ;
217246 map. insert ( session_id. to_string ( ) , SessionContext {
@@ -232,6 +261,59 @@ async fn restore_specific_session(state: &AppState, session_id: &str) {
232261 }
233262 }
234263 }
264+
265+ // If not found, try to find a legacy container by matching container name
266+ // Legacy containers have UUID in name: trycli-studio-viewer-{uuid} or trycli-studio-session-{uuid}
267+ let legacy_container_names = vec ! [
268+ format!( "trycli-studio-viewer-{}" , session_id) ,
269+ format!( "trycli-studio-session-{}" , session_id) ,
270+ ] ;
271+
272+ for legacy_name in legacy_container_names {
273+ // Try to find container by exact name match
274+ let filters_by_name = HashMap :: from ( [
275+ ( "label" . to_string ( ) , vec ! [ "managed_by=TryCli Studio" . to_string( ) ] ) ,
276+ ( "name" . to_string ( ) , vec ! [ legacy_name. clone( ) ] )
277+ ] ) ;
278+
279+ let opts = ListContainersOptions {
280+ all : false ,
281+ filters : filters_by_name,
282+ ..Default :: default ( )
283+ } ;
284+
285+ if let Ok ( containers) = state. docker . list_containers ( Some ( opts) ) . await {
286+ if let Some ( container) = containers. first ( ) {
287+ let labels = container. labels . as_ref ( ) ;
288+ let shell = labels
289+ . and_then ( |l| l. get ( "shell" ) )
290+ . map ( |s| s. clone ( ) )
291+ . unwrap_or_else ( || "/bin/bash" . to_string ( ) ) ;
292+
293+ let container_name = container. names . as_ref ( )
294+ . and_then ( |names| names. first ( ) )
295+ . map ( |n| n. trim_start_matches ( '/' ) . to_string ( ) )
296+ . unwrap_or ( legacy_name. clone ( ) ) ;
297+
298+ let created_at = calculate_session_created_at ( container. created ) ;
299+
300+ let mut map = state. lock_sessions ( ) ;
301+ map. insert ( session_id. to_string ( ) , SessionContext {
302+ container_name : container_name. clone ( ) ,
303+ shell,
304+ pending_image_tag : None ,
305+ owner_id : None , // Unknown for legacy containers
306+ project_owner_id : None ,
307+ is_publishing : false ,
308+ project_slug : None ,
309+ created_at,
310+ is_ws_connected : false ,
311+ } ) ;
312+ tracing:: info!( "Restored legacy session {} from container {}" , session_id, container_name) ;
313+ return ;
314+ }
315+ }
316+ }
235317}
236318
237319async fn handle_socket ( mut socket : WebSocket , state : AppState , session_id : String , user_id : Option < i64 > ) {
0 commit comments