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 }