View Javadoc
1   package de.spiritscorp.datasync.controller;
2   
3   /*-
4    * 		Data Sync
5    *
6    * 		Copyright ©   2022    The Spirit
7    * 		@email                        thespirit@spiritscorp.network
8    *
9    * 		This program is free software; you can redistribute it and/or modify
10   * 		it under the terms of the GNU General Public License as published by
11   * 		the Free Software Foundation; either version 3 of the License, or
12   * 		(at your option) any later version.
13   *
14   * 		This program is distributed in the hope that it will be useful,
15   * 		but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * 		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17   * 		See the GNU General Public License for more details.
18   *
19   * 		You should have received a copy of the GNU General Public License
20   * 		along with this program. If not, see <http://www.gnu.org/licenses/>.
21   */
22  
23  import java.awt.AWTException;
24  import java.awt.SystemTray;
25  import java.awt.TrayIcon;
26  import java.util.concurrent.ExecutorService;
27  import java.util.concurrent.Executors;
28  import java.util.concurrent.ScheduledExecutorService;
29  import java.util.concurrent.TimeUnit;
30  
31  import javafx.application.Platform;
32  import javafx.collections.ObservableList;
33  
34  import de.spiritscorp.datasync.Main;
35  import de.spiritscorp.datasync.gui.BgView;
36  import de.spiritscorp.datasync.gui.Gui;
37  import de.spiritscorp.datasync.io.Debug;
38  import de.spiritscorp.datasync.io.Logger;
39  import de.spiritscorp.datasync.model.BgModel;
40  import de.spiritscorp.datasync.model.Model;
41  
42  /**
43   * Central orchestration engine handling asynchronous background file synchronization routines.
44   * <p>
45   * The {@code BgController} manages the application's daemon lifecycle. It leverages a dedicated
46   * two-tier concurrent executor architecture to decouple continuous time-threshold monitoring from
47   * high-overhead disk I/O operations. This design prevents resource starvation and avoids system
48   * UI freezes by offloading execution workloads to isolated worker threads.
49   * <p>
50   * System state integration is maintained via an operating system {@link SystemTray} proxy interface,
51   * allowing the core UI application framework to seamlessly minimize into background execution lanes.
52   * <p>
53   *
54   * @author Tom Spirit
55   */
56  public class BgController {
57  
58  	static final long INITIAL_DELAY = 1000;
59  	static final long BOOT_START_DELAY = 30000;
60  
61  	private final SystemTray sysTray;
62  	private final ObservableList<SyncJobContext> jobList;
63  	private final Logger logger;
64  	private final ViewController controller;
65  	private BgView bgView;
66  	private final Gui gui;
67  
68  	// Modern thread-pool architecture for accurate execution timing and disk I/O optimization
69  	private ScheduledExecutorService scheduler;
70  	private ExecutorService workerQueue;
71  
72  	// Test interface: Allows accelerating intervals inside JUnit execution tasks
73  	private double timeMultiplier = 1.0;
74  
75  	/**
76  	 * Constructs a fully operational background engine attached to the primary interface layers.
77  	 * <p>
78  	 * The initialization phase maps structural JavaFX core properties, bindings, and multi-job tracking
79  	 * contexts. It automatically registers native {@link SystemTray} hardware capacity parameters to bind
80  	 * the decoupled visual notification framework shell.
81  	 * <p>
82  	 *
83  	 * @param gui        The visual primary graphical user interface facade wrapper
84  	 * @param controller The central master view controller orchestrating active window transitions
85  	 * @param jobList    The reactive data backing list containing operational task metrics and execution state tokens
86  	 * @param logger     The standardized system logging framework interface
87  	 */
88  	BgController( final Gui gui, final ViewController controller, final ObservableList<SyncJobContext> jobList, final Logger logger ) {
89  		this.gui = gui;
90  		this.controller = controller;
91  		this.jobList = jobList;
92  		this.logger = logger;
93  		this.sysTray = SystemTray.isSupported() ? SystemTray.getSystemTray() : null;
94  		setEnvironment( timeMultiplier, new BgView( this ), Executors.newSingleThreadScheduledExecutor(), Executors.newSingleThreadExecutor() );
95  	}
96  
97  	/**
98  	 * Initiates a global application termination sequence triggered from the background context.
99  	 * <p>
100 	 * This method acts as the bridge for the {@code BgView} (SystemTray) to command a full system exit.
101 	 * It systematically deallocates and dismantles internal concurrent tracking structures using a
102 	 * standardized background grace period before delegating downstream lifecycle teardown protocols
103 	 * to the central application controller.
104 	 * <p>
105 	 *
106 	 * @see #shutdownExecutors(long)
107 	 */
108 	public void requestApplicationShutdown() {
109 		// Disassemble concurrent tracking frameworks before global window exit procedures trigger
110 		shutdownExecutors( Main.BACKGROUND_THREAD_TIMEOUT );
111 		controller.handleApplicationShutdown();
112 	}
113 
114 	/**
115 	 * Interrupts the active background execution cycle and restores the primary user interface.
116 	 * <p>
117 	 * This dual-purpose lifecycle hook is invoked by both the primary workspace ({@code MainView})
118 	 * and the system notification shell ({@code BgView}). It enforces an immediate visibility state
119 	 * transition on the main window stage and guarantees a deterministic, timed collapse of all
120 	 * active thread pool frames.
121 	 * <p>
122 	 *
123 	 * @param timeoutPerThreadMs The maximum allocation window in milliseconds granted to active
124 	 *                           worker threads to complete processing cycles before a hard
125 	 *                           interruption signal is enforced.
126 	 * @see #shutdownExecutors(long)
127 	 */
128 	public void interruptBgJob( final long timeoutPerThreadMs ) {
129 		if( Platform.isFxApplicationThread() ) {
130 			gui.getWindowStage().show();
131 		}
132 		shutdownExecutors( timeoutPerThreadMs );
133 		Debug.printDebug( "[BgController] Background routine interrupted" );
134 	}
135 
136 	/**
137 	 * Initiates the continuous background daemon monitoring pipeline and minimizes the user interface.
138 	 * <p>
139 	 * Activating this boot phase suppresses the primary desktop window frame and binds the visual notifications
140 	 * infrastructure into the native operating system taskbar environment. It dynamically analyzes user scheduling
141 	 * rules to compute an optimal, non-blocking check frequency tick rate.
142 	 * <p>
143 	 * Once configurations are parsed, an initial delay configuration is selected—differentiating between fresh
144 	 * application boots ({@code BOOT_START_DELAY}) and quick UI toggle states ({@code INITIAL_DELAY}). The continuous
145 	 * tracking routine is then permanently registered inside the internal {@link ScheduledExecutorService} core thread framework.
146 	 * <p>
147 	 *
148 	 * @param bootDelay Enforces an extended cold-boot initialization timeout buffer if set to {@code true};
149 	 *                  allocates a standard near-instant scheduling offset if set to {@code false}.
150 	 */
151 	void startBgJob( final boolean bootDelay ) {
152 		gui.getWindowStage().hide();
153 		if( sysTray != null && bgView.getTrayIcon() != null ) {
154 			try {
155 				sysTray.add( bgView.getTrayIcon() );
156 			}catch( final AWTException e ) {
157 				Debug.printError( "[BgController] Failed to register TrayIcon context." );
158 				Debug.printException( getClass(), e );
159 				gui.getWindowStage().show();
160 				return;
161 			}
162 		}
163 
164 		Debug.printDebug( "[BgController] Multi-Job Background-Daemon initialization started." );
165 
166 		// Dynamically determine the optimal check interval based on active jobs
167 		final long calculatedTick = determineOptimalCheckTime();
168 		final long tickInterval = (long) ( calculatedTick * timeMultiplier );
169 		final long initialDelay = (long) ( ( bootDelay ? BOOT_START_DELAY : INITIAL_DELAY ) * timeMultiplier );
170 		Debug.printDebug( "[BgController] Heartbeat configured to tick every %d ms based on job preferences.", calculatedTick );
171 		jobList.stream()
172 				.filter( ( job ) -> job.getPreference()
173 						.isBgSync() )
174 				.forEach( ( job ) -> Debug.printDebug( "[BgController] Executing background routine is activated for task: %s", job.getJobName() ) );
175 		// Begin tracking task list rules loops
176 		this.scheduler.scheduleAtFixedRate( this::checkAndQueueJobs, initialDelay, tickInterval, TimeUnit.MILLISECONDS );
177 	}
178 
179 	/**
180 	 * Determines the smallest defined checkTime among all active background jobs.
181 	 * Falls back to a default interval (10 sec) if no matching jobs are active.
182 	 */
183 	private long determineOptimalCheckTime() {
184 		long minCheckTime = 10000; // Default fallback: 10 seconds
185 		boolean foundActiveJob = false;
186 
187 		for( final SyncJobContext job : jobList ) {
188 			final var pref = job.getPreference();
189 			if( pref != null && pref.isBgSync() && pref.getBgTime() != null ) {
190 				final long currentCheck = pref.getBgTime().getCheckTime();
191 				if( !foundActiveJob || currentCheck < minCheckTime ) {
192 					minCheckTime = currentCheck;
193 					foundActiveJob = true;
194 				}
195 			}
196 		}
197 		return minCheckTime;
198 	}
199 
200 	/**
201 	 * Evaluates temporal boundaries across registered task configurations to schedule overdue synchronization pipelines.
202 	 * <p>
203 	 * This core evaluation loop acts as the engine's processing heartbeat. It scans all configured
204 	 * synchronization definitions, applies an accelerated time scaling calculation using the {@code timeMultiplier},
205 	 * and determines if an individual task context has surpassed its requested execution interval threshold.
206 	 * <p>
207 	 * Overdue jobs are safely flag-locked to guarantee execution idempotency. The payload runnable is subsequently
208 	 * dispatched into a dedicated single-threaded sequential worker pool ({@code workerQueue}). This strict serialization
209 	 * strategy isolates concurrent I/O access and actively prevents multiple background tasks from triggering destructive
210 	 * physical disk drive thrashing.
211 	 * <p>
212 	 * To ensure resilient remote cancellation capabilities, the executing worker thread frame is explicitly mapped
213 	 * directly back to the target {@link SyncJobContext} token inside the processing boundary.
214 	 * <p>
215 	 */
216 	private void checkAndQueueJobs() {
217 		for( final SyncJobContext job : jobList ) {
218 			// Skip tasks if they are actively running or already waiting inside the execution queue lane
219 			if( job.isRunning() ) continue;
220 
221 			final var pref = job.getPreference();
222 			// Only process if background execution is explicitly requested for this task context
223 			if( pref != null && pref.isBgSync() ) {
224 				final long timeDelta = System.currentTimeMillis() - pref.getLastScanTime();
225 				final long targetInterval = (long) ( pref.getBgTime().getTime() * timeMultiplier );
226 				Debug.printDebug( "[BgController] time since last check: %d", System.currentTimeMillis() - pref.getLastScanTime() );
227 
228 				if( timeDelta > targetInterval ) {
229 					Debug.printDebug( "[BgController] Polling threshold triggered for task: %s. Queueing worker task.", job.getJobName() );
230 					job.setRunning( true );
231 					// Dispatch into the dedicated loop queue lane (prevents hardware disk I/O thrashing)
232 					workerQueue.execute( () -> {
233 						try {
234 							final BgModel bgModel = new BgModel( pref, logger, Model.createMap(), Model.createMap() );
235 
236 							// Map active thread to the context token to let external shutdown requests throw interrupts
237 							job.setActiveWorkerThread( Thread.currentThread() );
238 							Debug.printDebug( "[BgController] Executing background routine for task: %s", job.getJobName() );
239 							bgModel.runBgJob();
240 						}catch( final Exception exception ) {
241 							Debug.printDebug( "[BgController Error] Critical fault captured inside background thread execution pipeline for: %s", job.getJobName() );
242 							Debug.printException( this.getClass(), exception );
243 						}finally {
244 							job.setRunning( false );
245 							job.setActiveWorkerThread( null );
246 							Debug.printDebug( "[BgController] Finished executing background routine for task: %s", job.getJobName() );
247 						}
248 					} );
249 				}
250 			}
251 		}
252 	}
253 
254 	/**
255 	 * Deallocates the dual-tier concurrent execution infrastructure and dissolves operational states.
256 	 * <p>
257 	 * This structural shutdown hook safely liquidates asynchronous runtimes by issuing immediate
258 	 * cancellation signals via {@link ExecutorService#shutdownNow()} to both the high-frequency tick scheduler
259 	 * and the sequential data transfer queue. Active backup threads executing file system operations
260 	 * are granted a strict temporal grace window to cooperatively wind down file handles.
261 	 * <p>
262 	 * Upon pool expiration, all underlying job context data models are purged of volatile execution parameters
263 	 * and the associated hardware {@link TrayIcon} is stripped from the operating system shell to ensure
264 	 * zero resource leaks.
265 	 * <p>
266 	 *
267 	 * @param timeoutPerThreadMs The maximum synchronization epoch in milliseconds granted to active
268 	 *                           I/O operations to complete task evaluation loops before
269 	 *                           the lifecycle boundary is forcibly closed.
270 	 */
271 	private void shutdownExecutors( final long timeoutPerThreadMs ) {
272 		Debug.printDebug( "[BgController] Dissolving executor pools and cleaning up task contexts." );
273 
274 		if( scheduler != null ) {
275 			scheduler.shutdownNow();
276 		}
277 
278 		if( workerQueue != null ) {
279 			// Drops instant interrupt signals down to the thread executing the active copy sequence
280 			workerQueue.shutdownNow();
281 			try {
282 				if( !workerQueue.awaitTermination( timeoutPerThreadMs, TimeUnit.MILLISECONDS ) ) {
283 					Debug.printDebug( "[BgController] Worker queue termination delayed. Enforcing lifecycle exit." );
284 				}
285 			}catch( InterruptedException _ ) {
286 				Thread.currentThread().interrupt();
287 			}
288 		}
289 
290 		// Clean up framework tracking tokens across the execution stack
291 		for( final SyncJobContext job : jobList ) {
292 			job.setRunning( false );
293 			job.setActiveWorkerThread( null );
294 		}
295 
296 		// Remove indicator shell icons
297 		if( sysTray != null && bgView != null && bgView.getTrayIcon() != null ) {
298 			sysTray.remove( bgView.getTrayIcon() );
299 		}
300 
301 		Debug.printDebug( "[BgController] Background-Daemon terminated cleanly." );
302 	}
303 
304 	/**
305 	 * Configures the internal asynchronous execution environment for isolation testing.
306 	 * <p>
307 	 * This configuration interface swaps out production thread pools with deterministic
308 	 * mock implementations and scales execution time windows. It guarantees atomic evaluation
309 	 * boundaries without leaking OS threads during unit test runs.
310 	 * </p>
311 	 *
312 	 * @param multiplier  The scaling factor applied to time calculations (e.g., fractional values)
313 	 * @param bgView      The background view for ui interactions.
314 	 * @param scheduler   The scheduled executor tracking the heartbeat loops
315 	 * @param workerQueue The sequential worker queue processing pending sync transfers
316 	 */
317 	private void setEnvironment( final double multiplier, final BgView bgView, final ScheduledExecutorService scheduler, final ExecutorService workerQueue ) {
318 		if( multiplier > 0.0 ) this.timeMultiplier = multiplier;
319 		if( bgView != null ) this.bgView = bgView;
320 		if( scheduler != null ) this.scheduler = scheduler;
321 		if( workerQueue != null ) this.workerQueue = workerQueue;
322 	}
323 }