mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
restart func
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Captures application command-line arguments at startup so they can be reused for restart
|
||||
* operations. This allows the application to restart with the same configuration.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AppArgsCapture implements ApplicationRunner {
|
||||
|
||||
public static final AtomicReference<List<String>> APP_ARGS = new AtomicReference<>(List.of());
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
APP_ARGS.set(List.of(args.getSourceArgs()));
|
||||
log.debug("Captured {} application arguments for restart capability", args.getSourceArgs().length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Utility class to locate JAR files at runtime for restart operations
|
||||
*/
|
||||
@Slf4j
|
||||
public class JarPathUtil {
|
||||
|
||||
/**
|
||||
* Gets the path to the currently running JAR file
|
||||
*
|
||||
* @return Path to the current JAR, or null if not running from a JAR
|
||||
*/
|
||||
public static Path currentJar() {
|
||||
try {
|
||||
Path jar =
|
||||
Paths.get(
|
||||
JarPathUtil.class
|
||||
.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI())
|
||||
.toAbsolutePath();
|
||||
|
||||
// Check if we're actually running from a JAR (not from IDE/classes directory)
|
||||
if (jar.toString().endsWith(".jar")) {
|
||||
log.debug("Current JAR located at: {}", jar);
|
||||
return jar;
|
||||
} else {
|
||||
log.warn("Not running from JAR, current location: {}", jar);
|
||||
return null;
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
log.error("Failed to determine current JAR location", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the restart-helper.jar file Expected to be in the same directory as the
|
||||
* main JAR
|
||||
*
|
||||
* @return Path to restart-helper.jar, or null if not found
|
||||
*/
|
||||
public static Path restartHelperJar() {
|
||||
Path appJar = currentJar();
|
||||
if (appJar == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Path helperJar = appJar.getParent().resolve("restart-helper.jar");
|
||||
|
||||
if (Files.isRegularFile(helperJar)) {
|
||||
log.debug("Restart helper JAR located at: {}", helperJar);
|
||||
return helperJar;
|
||||
} else {
|
||||
log.warn("Restart helper JAR not found at: {}", helperJar);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the java binary path for the current JVM
|
||||
*
|
||||
* @return Path to java executable
|
||||
*/
|
||||
public static String javaExecutable() {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
|
||||
|
||||
// On Windows, add .exe extension
|
||||
if (System.getProperty("os.name").toLowerCase().contains("win")) {
|
||||
javaBin += ".exe";
|
||||
}
|
||||
|
||||
return javaBin;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -32,7 +40,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.api.AdminApi;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.AppArgsCapture;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.JarPathUtil;
|
||||
import stirling.software.proprietary.security.model.api.admin.SettingValueResponse;
|
||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
|
||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
|
||||
@@ -45,6 +55,7 @@ public class AdminSettingsController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
// Track settings that have been modified but not yet applied (require restart)
|
||||
private static final ConcurrentHashMap<String, Object> pendingChanges =
|
||||
@@ -388,6 +399,106 @@ public class AdminSettingsController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/restart")
|
||||
@Operation(
|
||||
summary = "Restart the application",
|
||||
description =
|
||||
"Triggers a graceful restart of the Spring Boot application to apply pending settings changes. Uses a restart helper to ensure proper restart. Admin access required.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Restart initiated successfully"),
|
||||
@ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access denied - Admin role required"),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Failed to initiate restart")
|
||||
})
|
||||
public ResponseEntity<String> restartApplication() {
|
||||
try {
|
||||
log.warn("Admin initiated application restart");
|
||||
|
||||
// Get paths to current JAR and restart helper
|
||||
Path appJar = JarPathUtil.currentJar();
|
||||
Path helperJar = JarPathUtil.restartHelperJar();
|
||||
|
||||
if (appJar == null) {
|
||||
log.error("Cannot restart: not running from JAR (likely development mode)");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(
|
||||
"Restart not available in development mode. Please restart the application manually.");
|
||||
}
|
||||
|
||||
if (helperJar == null || !Files.isRegularFile(helperJar)) {
|
||||
log.error("Cannot restart: restart-helper.jar not found at expected location");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(
|
||||
"Restart helper not found. Please restart the application manually.");
|
||||
}
|
||||
|
||||
// Get current application arguments
|
||||
List<String> appArgs = AppArgsCapture.APP_ARGS.get();
|
||||
|
||||
// Write args to temp file to avoid command-line quoting issues
|
||||
Path argsFile = Files.createTempFile("stirling-app-args-", ".txt");
|
||||
Files.write(argsFile, appArgs, StandardCharsets.UTF_8);
|
||||
|
||||
// Get current process PID and java executable
|
||||
long pid = ProcessHandle.current().pid();
|
||||
String javaBin = JarPathUtil.javaExecutable();
|
||||
|
||||
// Build command to launch restart helper
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(javaBin);
|
||||
cmd.add("-jar");
|
||||
cmd.add(helperJar.toString());
|
||||
cmd.add("--pid");
|
||||
cmd.add(Long.toString(pid));
|
||||
cmd.add("--app");
|
||||
cmd.add(appJar.toString());
|
||||
cmd.add("--argsFile");
|
||||
cmd.add(argsFile.toString());
|
||||
cmd.add("--backoffMs");
|
||||
cmd.add("1000");
|
||||
|
||||
log.info("Launching restart helper: {}", String.join(" ", cmd));
|
||||
|
||||
// Launch restart helper process
|
||||
new ProcessBuilder(cmd)
|
||||
.directory(appJar.getParent().toFile())
|
||||
.inheritIO() // Forward logs
|
||||
.start();
|
||||
|
||||
// Clear pending changes since we're restarting
|
||||
pendingChanges.clear();
|
||||
|
||||
// Give the HTTP response time to complete, then exit
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
log.info("Shutting down for restart...");
|
||||
SpringApplication.exit(applicationContext, () -> 0);
|
||||
System.exit(0);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Restart interrupted: {}", e.getMessage(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Application restart initiated. The server will be back online shortly.");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initiate restart: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Failed to initiate application restart: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Object getSectionData(String sectionName) {
|
||||
if (sectionName == null || sectionName.trim().isEmpty()) {
|
||||
return null;
|
||||
|
||||
33
build.gradle
33
build.gradle
@@ -629,9 +629,40 @@ tasks.named('bootRun') {
|
||||
tasks.named('build') {
|
||||
group = 'build'
|
||||
description = 'Delegates to :stirling-pdf:bootJar'
|
||||
dependsOn ':stirling-pdf:bootJar'
|
||||
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper'
|
||||
|
||||
doFirst {
|
||||
println "Delegating to :stirling-pdf:bootJar"
|
||||
}
|
||||
}
|
||||
|
||||
// Task to compile RestartHelper.java
|
||||
tasks.register('compileRestartHelper', JavaCompile) {
|
||||
group = 'build'
|
||||
description = 'Compiles the RestartHelper utility'
|
||||
|
||||
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
|
||||
classpath = files()
|
||||
destinationDirectory = file("${buildDir}/restart-helper-classes")
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// Task to create restart-helper.jar
|
||||
tasks.register('buildRestartHelper', Jar) {
|
||||
group = 'build'
|
||||
description = 'Builds the restart-helper.jar'
|
||||
dependsOn 'compileRestartHelper'
|
||||
|
||||
from "${buildDir}/restart-helper-classes"
|
||||
archiveFileName = 'restart-helper.jar'
|
||||
destinationDirectory = file("${buildDir}/libs")
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': 'RestartHelper'
|
||||
}
|
||||
|
||||
doLast {
|
||||
println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ COPY scripts /scripts
|
||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
# first /app directory is for the build stage, second is for the final image
|
||||
COPY --from=build /app/app/core/build/libs/*.jar app.jar
|
||||
COPY --from=build /app/build/libs/restart-helper.jar restart-helper.jar
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
@@ -113,7 +114,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
# User permissions
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ COPY scripts /scripts
|
||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
# first /app directory is for the build stage, second is for the final image
|
||||
COPY --from=build /app/app/core/build/libs/*.jar app.jar
|
||||
COPY --from=build /app/build/libs/restart-helper.jar restart-helper.jar
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
@@ -104,7 +105,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
# User permissions
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
# Set user and run command
|
||||
|
||||
@@ -45,6 +45,7 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||
COPY --from=build /app/app/core/build/libs/*.jar app.jar
|
||||
COPY --from=build /app/build/libs/restart-helper.jar restart-helper.jar
|
||||
|
||||
# Set up necessary directories and permissions
|
||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||
@@ -65,7 +66,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
chmod +x /scripts/*.sh && \
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
|
||||
|
||||
# Set environment variables
|
||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Modal, Text, Group, Button, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ScheduleIcon from '@mui/icons-material/Schedule';
|
||||
|
||||
interface RestartConfirmationModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onRestart: () => void;
|
||||
}
|
||||
|
||||
export default function RestartConfirmationModal({
|
||||
opened,
|
||||
onClose,
|
||||
onRestart,
|
||||
}: RestartConfirmationModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={
|
||||
<Text fw={600} size="lg">
|
||||
{t('admin.settings.restart.title', 'Restart Required')}
|
||||
</Text>
|
||||
}
|
||||
centered
|
||||
size="md"
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Text size="sm">
|
||||
{t(
|
||||
'admin.settings.restart.message',
|
||||
'Settings have been saved successfully. A server restart is required for the changes to take effect.'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
'admin.settings.restart.question',
|
||||
'Would you like to restart the server now or later?'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<ScheduleIcon style={{ fontSize: 16 }} />}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('admin.settings.restart.later', 'Restart Later')}
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
leftSection={<RefreshIcon style={{ fontSize: 16 }} />}
|
||||
onClick={onRestart}
|
||||
>
|
||||
{t('admin.settings.restart.now', 'Restart Now')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Accordion, TextInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface AdvancedSettingsData {
|
||||
enableAlphaFunctionality?: boolean;
|
||||
@@ -16,6 +18,7 @@ export default function AdminAdvancedSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<AdvancedSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -65,11 +68,7 @@ export default function AdminAdvancedSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -197,6 +196,13 @@ export default function AdminAdvancedSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Badge, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import LocalIcon from '../../LocalIcon';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface OAuth2Settings {
|
||||
enabled?: boolean;
|
||||
@@ -34,6 +36,7 @@ export default function AdminConnectionsSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<ConnectionsSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -90,11 +93,7 @@ export default function AdminConnectionsSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -336,6 +335,13 @@ export default function AdminConnectionsSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Stack, Paper, Text, Loader, Group, MultiSelect } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface EndpointsSettingsData {
|
||||
toRemove?: string[];
|
||||
@@ -13,6 +15,7 @@ export default function AdminEndpointsSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<EndpointsSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -47,11 +50,7 @@ export default function AdminEndpointsSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -175,6 +174,13 @@ export default function AdminEndpointsSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface GeneralSettingsData {
|
||||
ui: {
|
||||
@@ -23,6 +25,7 @@ export default function AdminGeneralSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
const [settings, setSettings] = useState<GeneralSettingsData>({
|
||||
ui: {},
|
||||
system: {},
|
||||
@@ -75,11 +78,8 @@ export default function AdminGeneralSection() {
|
||||
]);
|
||||
|
||||
if (uiResponse.ok && systemResponse.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
// Show restart confirmation modal
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -245,6 +245,13 @@ export default function AdminGeneralSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { TextInput, Button, Stack, Paper, Text, Loader, Group, Alert } from '@mantine/core';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface LegalSettingsData {
|
||||
termsAndConditions?: string;
|
||||
@@ -16,6 +19,7 @@ export default function AdminLegalSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<LegalSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -50,11 +54,7 @@ export default function AdminLegalSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -86,6 +86,21 @@ export default function AdminLegalSection() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Legal Disclaimer */}
|
||||
<Alert
|
||||
icon={<WarningIcon style={{ fontSize: 18 }} />}
|
||||
title={t('admin.settings.legal.disclaimer.title', 'Legal Responsibility Warning')}
|
||||
color="yellow"
|
||||
variant="light"
|
||||
>
|
||||
<Text size="sm">
|
||||
{t(
|
||||
'admin.settings.legal.disclaimer.message',
|
||||
'By customizing these legal documents, you assume full responsibility for ensuring compliance with all applicable laws and regulations, including but not limited to GDPR and other EU data protection requirements. Only modify these settings if: (1) you are operating a personal/private instance, (2) you are outside EU jurisdiction and understand your local legal obligations, or (3) you have obtained proper legal counsel and accept sole responsibility for all user data and legal compliance. Stirling-PDF and its developers assume no liability for your legal obligations.'
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
@@ -145,6 +160,13 @@ export default function AdminLegalSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface MailSettingsData {
|
||||
enabled?: boolean;
|
||||
@@ -17,6 +19,7 @@ export default function AdminMailSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<MailSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -51,11 +54,7 @@ export default function AdminMailSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -158,6 +157,13 @@ export default function AdminMailSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface PremiumSettingsData {
|
||||
key?: string;
|
||||
@@ -29,6 +31,7 @@ export default function AdminPremiumSection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<PremiumSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -63,11 +66,7 @@ export default function AdminPremiumSection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -305,6 +304,13 @@ export default function AdminPremiumSection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Switch, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface PrivacySettingsData {
|
||||
enableAnalytics?: boolean;
|
||||
@@ -14,6 +16,7 @@ export default function AdminPrivacySection() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<PrivacySettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -66,11 +69,7 @@ export default function AdminPrivacySection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -161,6 +160,13 @@ export default function AdminPrivacySection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface SecuritySettingsData {
|
||||
enableLogin?: boolean;
|
||||
@@ -26,6 +28,7 @@ export default function AdminSecuritySection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
const [settings, setSettings] = useState<SecuritySettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,11 +64,7 @@ export default function AdminSecuritySection() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
showRestartModal();
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
@@ -271,6 +270,13 @@ export default function AdminSecuritySection() {
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Restart Confirmation Modal */}
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
60
frontend/src/components/shared/config/useRestartServer.ts
Normal file
60
frontend/src/components/shared/config/useRestartServer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { alert } from '../../toast';
|
||||
|
||||
export function useRestartServer() {
|
||||
const { t } = useTranslation();
|
||||
const [restartModalOpened, setRestartModalOpened] = useState(false);
|
||||
|
||||
const showRestartModal = () => {
|
||||
setRestartModalOpened(true);
|
||||
};
|
||||
|
||||
const closeRestartModal = () => {
|
||||
setRestartModalOpened(false);
|
||||
};
|
||||
|
||||
const restartServer = async () => {
|
||||
try {
|
||||
setRestartModalOpened(false);
|
||||
|
||||
alert({
|
||||
alertType: 'info',
|
||||
title: t('admin.settings.restarting', 'Restarting Server'),
|
||||
body: t(
|
||||
'admin.settings.restartingMessage',
|
||||
'The server is restarting. Please wait a moment...'
|
||||
),
|
||||
});
|
||||
|
||||
const response = await fetch('/api/v1/admin/settings/restart', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Wait a moment then reload the page
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error('Failed to restart');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t(
|
||||
'admin.settings.restartError',
|
||||
'Failed to restart server. Please restart manually.'
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
restartModalOpened,
|
||||
showRestartModal,
|
||||
closeRestartModal,
|
||||
restartServer,
|
||||
};
|
||||
}
|
||||
129
scripts/RestartHelper.java
Normal file
129
scripts/RestartHelper.java
Normal file
@@ -0,0 +1,129 @@
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* RestartHelper - Lightweight utility to restart Stirling-PDF
|
||||
*
|
||||
* This helper waits for the old process to exit, then starts the app again
|
||||
* with the same arguments. It's only active during restart and then exits.
|
||||
*
|
||||
* Usage:
|
||||
* java -jar restart-helper.jar --pid 1234 --app /path/app.jar
|
||||
* [--java /path/to/java] [--argsFile /path/args.txt]
|
||||
* [--backoffMs 1000]
|
||||
*/
|
||||
public class RestartHelper {
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
Map<String, String> cli = parseArgs(args);
|
||||
|
||||
long pid = Long.parseLong(req(cli, "pid"));
|
||||
Path appJar = Paths.get(req(cli, "app")).toAbsolutePath().normalize();
|
||||
String javaBin = cli.getOrDefault("java", "java");
|
||||
Path argsFile = cli.containsKey("argsFile") ? Paths.get(cli.get("argsFile")) : null;
|
||||
long backoffMs = Long.parseLong(cli.getOrDefault("backoffMs", "1000"));
|
||||
|
||||
if (!Files.isRegularFile(appJar)) {
|
||||
fail("App jar not found: " + appJar);
|
||||
}
|
||||
|
||||
System.out.println("[restart-helper] Waiting for PID " + pid + " to exit...");
|
||||
waitForPidToExit(pid);
|
||||
|
||||
// Brief pause to allow ports/files to release
|
||||
if (backoffMs > 0) {
|
||||
Thread.sleep(backoffMs);
|
||||
}
|
||||
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(javaBin);
|
||||
cmd.add("-jar");
|
||||
cmd.add(appJar.toString());
|
||||
|
||||
// Load application arguments from file if provided
|
||||
if (argsFile != null && Files.isRegularFile(argsFile)) {
|
||||
for (String line : Files.readAllLines(argsFile)) {
|
||||
if (!line.isBlank()) {
|
||||
cmd.add(line.trim());
|
||||
}
|
||||
}
|
||||
// Clean up args file after reading
|
||||
try {
|
||||
Files.deleteIfExists(argsFile);
|
||||
} catch (IOException ignored) {
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[restart-helper] Starting app: " + String.join(" ", cmd));
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.inheritIO(); // Forward logs to same console/service logs
|
||||
pb.start();
|
||||
|
||||
// Exit immediately - leave app running
|
||||
System.out.println("[restart-helper] App restarted successfully. Helper exiting.");
|
||||
System.exit(0);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("[restart-helper] ERROR: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the specified PID to exit
|
||||
*/
|
||||
private static void waitForPidToExit(long pid) throws InterruptedException {
|
||||
try {
|
||||
// Java 9+: ProcessHandle API
|
||||
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||
while (ph.isPresent() && ph.get().isAlive()) {
|
||||
Thread.sleep(300);
|
||||
ph = ProcessHandle.of(pid);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// Fallback for older JDKs or if ProcessHandle isn't available
|
||||
// Just sleep a bit - by the time main exits, socket should be freed
|
||||
System.out.println("[restart-helper] ProcessHandle not available, using fallback wait");
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command-line arguments in --key value format
|
||||
*/
|
||||
private static Map<String, String> parseArgs(String[] args) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith("--")) {
|
||||
String key = args[i].substring(2);
|
||||
String val = (i + 1 < args.length && !args[i + 1].startsWith("--"))
|
||||
? args[++i]
|
||||
: "true";
|
||||
map.put(key, val);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required parameter or fail
|
||||
*/
|
||||
private static String req(Map<String, String> map, String key) {
|
||||
String val = map.get(key);
|
||||
if (val == null || val.isBlank()) {
|
||||
fail("Missing required parameter: --" + key);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print error and exit
|
||||
*/
|
||||
private static void fail(String message) {
|
||||
System.err.println("[restart-helper] ERROR: " + message);
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user