@@ -194,6 +194,140 @@ export class DockerService {
194194 }
195195 }
196196
197+ /**
198+ * Force reinstall a service by stopping, removing, and recreating its container.
199+ * This method will also clear any associated volumes/data.
200+ * Handles edge cases gracefully (e.g., container not running, container not found).
201+ */
202+ async forceReinstall ( serviceName : string ) : Promise < { success : boolean ; message : string } > {
203+ try {
204+ const service = await Service . query ( ) . where ( 'service_name' , serviceName ) . first ( )
205+ if ( ! service ) {
206+ return {
207+ success : false ,
208+ message : `Service ${ serviceName } not found` ,
209+ }
210+ }
211+
212+ // Check if installation is already in progress
213+ if ( this . activeInstallations . has ( serviceName ) ) {
214+ return {
215+ success : false ,
216+ message : `Service ${ serviceName } installation is already in progress` ,
217+ }
218+ }
219+
220+ // Mark as installing to prevent concurrent operations
221+ this . activeInstallations . add ( serviceName )
222+ service . installation_status = 'installing'
223+ await service . save ( )
224+
225+ this . _broadcast (
226+ serviceName ,
227+ 'reinstall-starting' ,
228+ `Starting force reinstall for ${ serviceName } ...`
229+ )
230+
231+ // Step 1: Try to stop and remove the container if it exists
232+ try {
233+ const containers = await this . docker . listContainers ( { all : true } )
234+ const container = containers . find ( ( c ) => c . Names . includes ( `/${ serviceName } ` ) )
235+
236+ if ( container ) {
237+ const dockerContainer = this . docker . getContainer ( container . Id )
238+
239+ // Only try to stop if it's running
240+ if ( container . State === 'running' ) {
241+ this . _broadcast ( serviceName , 'stopping' , `Stopping container...` )
242+ await dockerContainer . stop ( { t : 10 } ) . catch ( ( error ) => {
243+ // If already stopped, continue
244+ if ( ! error . message . includes ( 'already stopped' ) ) {
245+ logger . warn ( `Error stopping container: ${ error . message } ` )
246+ }
247+ } )
248+ }
249+
250+ // Step 2: Remove the container
251+ this . _broadcast ( serviceName , 'removing' , `Removing container...` )
252+ await dockerContainer . remove ( { force : true } ) . catch ( ( error ) => {
253+ logger . warn ( `Error removing container: ${ error . message } ` )
254+ } )
255+ } else {
256+ this . _broadcast (
257+ serviceName ,
258+ 'no-container' ,
259+ `No existing container found, proceeding with installation...`
260+ )
261+ }
262+ } catch ( error ) {
263+ logger . warn ( `Error during container cleanup: ${ error . message } ` )
264+ this . _broadcast (
265+ serviceName ,
266+ 'cleanup-warning' ,
267+ `Warning during cleanup: ${ error . message } `
268+ )
269+ }
270+
271+ // Step 3: Clear volumes/data if needed
272+ try {
273+ this . _broadcast ( serviceName , 'clearing-volumes' , `Checking for volumes to clear...` )
274+ const volumes = await this . docker . listVolumes ( )
275+ const serviceVolumes =
276+ volumes . Volumes ?. filter (
277+ ( v ) => v . Name . includes ( serviceName ) || v . Labels ?. service === serviceName
278+ ) || [ ]
279+
280+ for ( const vol of serviceVolumes ) {
281+ try {
282+ const volume = this . docker . getVolume ( vol . Name )
283+ await volume . remove ( { force : true } )
284+ this . _broadcast ( serviceName , 'volume-removed' , `Removed volume: ${ vol . Name } ` )
285+ } catch ( error ) {
286+ logger . warn ( `Failed to remove volume ${ vol . Name } : ${ error . message } ` )
287+ }
288+ }
289+
290+ if ( serviceVolumes . length === 0 ) {
291+ this . _broadcast ( serviceName , 'no-volumes' , `No volumes found to clear` )
292+ }
293+ } catch ( error ) {
294+ logger . warn ( `Error during volume cleanup: ${ error . message } ` )
295+ this . _broadcast (
296+ serviceName ,
297+ 'volume-cleanup-warning' ,
298+ `Warning during volume cleanup: ${ error . message } `
299+ )
300+ }
301+
302+ // Step 4: Mark service as uninstalled
303+ service . installed = false
304+ service . installation_status = 'installing'
305+ await service . save ( )
306+
307+ // Step 5: Recreate the container
308+ this . _broadcast ( serviceName , 'recreating' , `Recreating container...` )
309+ const containerConfig = this . _parseContainerConfig ( service . container_config )
310+
311+ // Execute installation asynchronously and handle cleanup
312+ this . _createContainer ( service , containerConfig ) . catch ( async ( error ) => {
313+ logger . error ( `Reinstallation failed for ${ serviceName } : ${ error . message } ` )
314+ await this . _cleanupFailedInstallation ( serviceName )
315+ } )
316+
317+ return {
318+ success : true ,
319+ message : `Service ${ serviceName } force reinstall initiated successfully. You can receive updates via server-sent events.` ,
320+ }
321+ } catch ( error ) {
322+ logger . error ( `Force reinstall failed for ${ serviceName } : ${ error . message } ` )
323+ await this . _cleanupFailedInstallation ( serviceName )
324+ return {
325+ success : false ,
326+ message : `Failed to force reinstall service ${ serviceName } : ${ error . message } ` ,
327+ }
328+ }
329+ }
330+
197331 /**
198332 * Handles the long-running process of creating a Docker container for a service.
199333 * NOTE: This method should not be called directly. Instead, use `createContainerPreflight` to check prerequisites first
0 commit comments