@@ -13,6 +13,72 @@ CTFd._internal.challenge.postRender = function () {
1313
1414if ( window . $ === undefined ) window . $ = CTFd . lib . $ ;
1515
16+ const BOOT_DEFAULT_LABEL = "Launch the challenge" ;
17+ const BOOT_STARTING_LABEL = "Starting..." ;
18+ const STARTING_KEY_PREFIX = "cm-starting:" ;
19+ const STARTING_TTL_MS = 10 * 60 * 1000 ;
20+
21+ function getStartingKey ( challengeId ) {
22+ return `${ STARTING_KEY_PREFIX } ${ challengeId } ` ;
23+ }
24+
25+ function markInstanceStarting ( challengeId ) {
26+ try {
27+ localStorage . setItem (
28+ getStartingKey ( challengeId ) ,
29+ JSON . stringify ( { ts : Date . now ( ) } ) ,
30+ ) ;
31+ } catch ( e ) {
32+ // storage unavailable, nothing to do
33+ }
34+ }
35+
36+ function clearInstanceStarting ( challengeId ) {
37+ try {
38+ localStorage . removeItem ( getStartingKey ( challengeId ) ) ;
39+ } catch ( e ) {
40+ // storage unavailable, nothing to do
41+ }
42+ }
43+
44+ function isInstanceStarting ( challengeId ) {
45+ try {
46+ const raw = localStorage . getItem ( getStartingKey ( challengeId ) ) ;
47+ if ( ! raw ) return false ;
48+
49+ const parsed = JSON . parse ( raw ) ;
50+ if ( ! parsed . ts ) return false ;
51+
52+ const age = Date . now ( ) - parsed . ts ;
53+ if ( age > STARTING_TTL_MS ) {
54+ clearInstanceStarting ( challengeId ) ;
55+ return false ;
56+ }
57+ return true ;
58+ } catch ( e ) {
59+ return false ;
60+ }
61+ }
62+
63+ function setBootButtonStarting ( ) {
64+ $ ( '#whale-button-boot' ) . text ( BOOT_STARTING_LABEL ) ;
65+ $ ( '#whale-button-boot' ) . prop ( 'disabled' , true ) ;
66+ }
67+
68+ function resetBootButton ( ) {
69+ $ ( '#whale-button-boot' ) . text ( BOOT_DEFAULT_LABEL ) ;
70+ $ ( '#whale-button-boot' ) . prop ( 'disabled' , false ) ;
71+ }
72+
73+ function renderStartingState ( ) {
74+ $ ( '#cm-panel-loading' ) . hide ( ) ;
75+ $ ( '#cm-panel-until' ) . hide ( ) ;
76+ $ ( '#whale-panel-started' ) . hide ( ) ;
77+ $ ( '#whale-panel-stopped' ) . show ( ) ;
78+ $ ( '#whale-challenge-lan-domain' ) . html ( '' ) ;
79+ setBootButtonStarting ( ) ;
80+ }
81+
1682function formatCountDown ( countdown ) {
1783
1884 // Convert
@@ -42,7 +108,12 @@ function formatCountDown(countdown) {
42108function loadInfo ( ) {
43109 var challenge_id = CTFd . _internal . challenge . data . id ;
44110 var url = "/api/v1/plugins/ctfd-chall-manager/instance?challengeId=" + challenge_id ;
45-
111+ const pendingStart = isInstanceStarting ( challenge_id ) ;
112+ if ( pendingStart ) {
113+ setBootButtonStarting ( ) ;
114+ } else {
115+ resetBootButton ( ) ;
116+ }
46117
47118 CTFd . fetch ( url , {
48119 method : 'GET' ,
@@ -67,11 +138,19 @@ function loadInfo() {
67138 clearInterval ( window . t ) ;
68139 window . t = undefined ;
69140 }
70- if ( response . success ) response = response . data ;
71- else CTFd . _functions . events . eventAlert ( {
72- title : "Fail" ,
73- html : response . data . message ,
74- } ) ;
141+ if ( response . success ) {
142+ response = response . data ;
143+ clearInstanceStarting ( challenge_id ) ;
144+ } else {
145+ const code = response . data ?. code || response . code ;
146+ if ( pendingStart && ( code === 5 || code === 404 || code === 14 ) ) {
147+ renderStartingState ( ) ;
148+ return ;
149+ }
150+ clearInstanceStarting ( challenge_id ) ;
151+ renderErrorAlert ( response ) ;
152+ return ;
153+ }
75154 $ ( '#cm-panel-loading' ) . hide ( ) ;
76155 $ ( '#cm-panel-until' ) . hide ( ) ;
77156
@@ -100,9 +179,14 @@ function loadInfo() {
100179 $ ( '#whale-challenge-count-down' ) . text ( formatCountDown ( count_down ) ) ;
101180 } , 1000 ) ;
102181 } else {
103- $ ( '#whale-panel-started' ) . hide ( ) ; // hide the panel instance is up
104- $ ( '#whale-panel-stopped' ) . show ( ) ; // show the panel instance is down
105- $ ( '#whale-challenge-lan-domain' ) . html ( '' ) ;
182+ // expired but likely still being cleaned up server-side
183+ $ ( '#whale-panel-started' ) . show ( ) ; // show the panel instance is up
184+ $ ( '#whale-panel-stopped' ) . hide ( ) ; // hide the panel instance is down
185+ $ ( '#whale-challenge-lan-domain' ) . html ( response . connectionInfo || '' ) ;
186+ $ ( '#whale-challenge-count-down' ) . text ( 'terminating...' ) ;
187+ $ ( '#cm-panel-until' ) . show ( ) ;
188+ // prevent booting a second instance while termination pending
189+ $ ( '#whale-button-boot' ) . prop ( 'disabled' , true ) . text ( 'Terminating...' ) ;
106190 }
107191
108192 } else if ( response . since ) { // if instance has no until
@@ -113,6 +197,7 @@ function loadInfo() {
113197 $ ( '#whale-panel-started' ) . hide ( ) ; // hide the panel instance is up
114198 $ ( '#whale-panel-stopped' ) . show ( ) ; // show the panel instance is down
115199 $ ( '#whale-challenge-lan-domain' ) . html ( '' ) ;
200+ resetBootButton ( ) ;
116201 }
117202
118203
@@ -261,6 +346,8 @@ CTFd._internal.challenge.boot = function() {
261346
262347 $ ( '#whale-button-boot' ) . text ( "Waiting..." ) ;
263348 $ ( '#whale-button-boot' ) . prop ( 'disabled' , true ) ;
349+ markInstanceStarting ( challenge_id ) ;
350+ setBootButtonStarting ( ) ;
264351
265352 var params = {
266353 "challengeId" : challenge_id . toString ( )
@@ -286,18 +373,13 @@ CTFd._internal.challenge.boot = function() {
286373 title : "Success" ,
287374 html : "Your instance has been deployed!" ,
288375 } ) ;
376+ clearInstanceStarting ( challenge_id ) ;
289377 resolve ( ) ;
290378 } else {
291- CTFd . _functions . events . eventAlert ( {
292- title : "Fail" ,
293- html : response . data . message ,
294- } ) ;
295- }
296- } ) . catch ( error => {
379+
297380 reject ( error ) ;
298- } ) . finally ( ( ) => {
299- $ ( '#whale-button-boot' ) . text ( "Launch an instance" ) ;
300- $ ( '#whale-button-boot' ) . prop ( 'disabled' , false ) ;
381+ } . finally ( ( ) => {
382+ resetBootButton ( ) ;
301383 } ) ;
302384 } ) ;
303385} ;
@@ -352,4 +434,4 @@ CTFd._internal.challenge.submit = function(preview) {
352434 }
353435 return response
354436 } )
355- } ;
437+ } ;
0 commit comments