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 }