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