Add journalctl export button (#693)

* Add journalctl export button

* Run spotless

* Split into 2 tabs
This commit is contained in:
Matt
2023-01-03 19:42:19 -08:00
committed by GitHub
parent 0b5256df12
commit af6f5eb0c4
6 changed files with 349 additions and 81 deletions

View File

@@ -12,12 +12,21 @@
color="secondary"
style="margin-left: auto;"
depressed
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
@click="$refs.exportLogFile.click()"
>
<v-icon left>
mdi-download
</v-icon>
Download Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="'http://' + this.$address + '/api/settings/photonvision-journalctl.txt'"
download="photonvision-journalctl.txt"
/>
</v-btn>
</v-card-title>
<div class="pr-6 pl-6">

View File

@@ -37,7 +37,8 @@
import Networking from './SettingsViews/Networking'
import Lighting from "./SettingsViews/Lighting";
import cvImage from '../components/common/cv-image'
import General from "./SettingsViews/General";
import Stats from "./SettingsViews/Stats";
import DeviceControl from "./SettingsViews/DeviceControl";
export default {
name: 'SettingsTab',
@@ -69,7 +70,7 @@
},
tabList: {
get() {
return [General, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
return [Stats, DeviceControl, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
}
}
},

View File

@@ -0,0 +1,289 @@
<template>
<div>
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartProgram()">
<v-icon left>
mdi-restart
</v-icon>
Restart PhotonVision
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartDevice()">
<v-icon left>
mdi-restart-alert
</v-icon>
Restart Device
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="$refs.offlineUpdate.click()">
<v-icon left>
mdi-update
</v-icon>
Offline Update
</v-btn>
</v-col>
</v-row>
<v-divider />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="$refs.exportSettings.click()">
<v-icon left>
mdi-download
</v-icon>
Export Settings
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="$refs.importSettings.click()">
<v-icon left>
mdi-upload
</v-icon>
Import Settings
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="$refs.exportLogFile.click()">
<v-icon left>
mdi-file
</v-icon>
Export current log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="
'http://' +
this.$address +
'/api/settings/photonvision-journalctl.txt'
"
download="photonvision-journalctl.txt"
/>
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="showLogs()">
<v-icon left>
mdi-bug
</v-icon>
Show log viewer
</v-btn>
</v-col>
</v-row>
<v-snackbar v-model="snack" top :color="snackbar.color" timeout="-1">
<span>{{ snackbar.text }}</span>
</v-snackbar>
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
<input
ref="importSettings"
type="file"
accept=".zip, .json"
style="display: none;"
@change="readImportedSettings"
/>
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="
'http://' + this.$address + '/api/settings/photonvision_config.zip'
"
download="photonvision-settings.zip"
/>
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
<input
ref="offlineUpdate"
type="file"
accept=".jar"
style="display: none;"
@change="doOfflineUpdate"
/>
</div>
</template>
<script>
export default {
name: "Device Control",
data() {
return {
snack: false,
uploadPercentage: 0.0,
snackbar: {
color: "success",
text: "",
},
};
},
computed: {
settings() {
return this.$store.state.settings.general;
},
version() {
return `${this.settings.version}`;
},
hwModel() {
if (this.settings.hardwareModel !== "") {
return `${this.settings.hardwareModel}`;
} else {
return `Unknown`;
}
},
platform() {
return `${this.settings.hardwarePlatform}`;
},
gpuAccel() {
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${
this.settings.gpuAcceleration
? "(" + this.settings.gpuAcceleration + ")"
: ""
}`;
},
metrics() {
// console.log(this.$store.state.metrics);
return this.$store.state.metrics;
},
},
methods: {
restartProgram() {
this.axios.post("http://" + this.$address + "/api/restartProgram", {});
},
restartDevice() {
this.axios.post("http://" + this.$address + "/api/restartDevice", {});
},
readImportedSettings(event) {
let formData = new FormData();
formData.append("zipData", event.target.files[0]);
this.axios
.post("http://" + this.$address + "/api/settings/import", formData, {
headers: { "Content-Type": "multipart/form-data" },
})
.then(() => {
this.snackbar = {
color: "success",
text:
"Settings imported successfully! PhotonVision will restart in the background...",
};
this.snack = true;
})
.catch((err) => {
if (err.response) {
this.snackbar = {
color: "error",
text:
"Error while uploading settings file! Could not process provided file.",
};
} else if (err.request) {
this.snackbar = {
color: "error",
text:
"Error while uploading settings file! No respond to upload attempt.",
};
} else {
this.snackbar = {
color: "error",
text: "Error while uploading settings file!",
};
}
this.snack = true;
});
},
doOfflineUpdate(event) {
this.snackbar = {
color: "secondary",
text: "New Software Upload in Process...",
};
this.snack = true;
let formData = new FormData();
formData.append("jarData", event.target.files[0]);
this.axios
.post(
"http://" + this.$address + "/api/settings/offlineUpdate",
formData,
{
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: function(progressEvent) {
this.uploadPercentage = parseInt(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
if (this.uploadPercentage < 99.5) {
this.snackbar.text =
"New Software Upload in Process, " +
this.uploadPercentage +
"% complete";
} else {
this.snackbar.text = "Installing uploaded software...";
}
}.bind(this),
}
)
.then(() => {
this.snackbar = {
color: "success",
text:
"New .jar copied successfully! PhotonVision will restart in the background...",
};
this.snack = true;
})
.catch((err) => {
if (err.response) {
this.snackbar = {
color: "error",
text:
"Error while uploading new .jar file! Could not process provided file.",
};
} else if (err.request) {
this.snackbar = {
color: "error",
text:
"Error while uploading new .jar file! No respond to upload attempt.",
};
} else {
this.snackbar = {
color: "error",
text: "Error while uploading new .jar file!",
};
}
this.snack = true;
});
},
showLogs(event) {
event;
this.$store.state.logsOverlay = true;
},
},
};
</script>
<style lang="css" scoped>
.v-btn {
width: 100%;
}
.infoTable {
border: 1px solid;
border-collapse: separate;
border-spacing: 0px;
border-radius: 5px;
text-align: left;
margin-bottom: 10px;
width: 100%;
display: block;
overflow-x: auto;
}
.infoElem {
padding-right: 15px;
padding-bottom: 1px;
padding-top: 1px;
padding-left: 10px;
border-right: 1px solid;
}
</style>

View File

@@ -117,85 +117,11 @@
</table>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
md="4"
>
<v-btn
color="secondary"
@click="$refs.exportSettings.click()"
>
<v-icon left>
mdi-download
</v-icon>
Export Settings
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<v-btn
color="secondary"
@click="$refs.importSettings.click()"
>
<v-icon left>
mdi-upload
</v-icon>
Import Settings
</v-btn>
</v-col>
<v-col
cols="12"
md="4"
>
<v-btn
color="secondary"
@click="$refs.offlineUpdate.click()"
>
<v-icon left>
mdi-update
</v-icon>
Offline Update
</v-btn>
</v-col>
<v-col
cols="12"
lg="6"
>
<v-btn
color="red"
@click="restartProgram()"
>
<v-icon left>
mdi-restart
</v-icon>
Restart PhotonVision
</v-btn>
</v-col>
<v-col
cols="12"
lg="6"
>
<v-btn
color="red"
@click="restartDevice()"
>
<v-icon left>
mdi-restart-alert
</v-icon>
Restart Device
</v-btn>
</v-col>
</v-row>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="0"
timeout="-1"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
@@ -230,7 +156,7 @@
<script>
export default {
name: 'General',
name: 'Stats',
data() {
return {
snack: false,
@@ -262,8 +188,8 @@ export default {
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
},
metrics() {
console.log(this.$store.state.metrics);
return this.$store.state.metrics;
// console.log(this.$store.state.metrics);
return this.$store.state.metrics;
}
},
methods: {
@@ -349,6 +275,10 @@ export default {
this.snack = true;
});
},
showLogs(event) {
event;
this.$store.state.logsOverlay = true;
}
}
}
</script>

View File

@@ -25,6 +25,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
@@ -193,6 +194,43 @@ public class RequestHandler {
}
}
private static ShellExec shell = new ShellExec();
public static void onExportCurrentLogs(Context ctx) {
if (!Platform.isLinux()) {
logger.warn("Cannot export journalctl on non-Linux platforms! Ignoring");
ctx.status(500);
return;
}
try {
var tempPath = Files.createTempFile("photonvision-journalctl", ".txt");
shell.executeBashCommand(
"journalctl -u photonvision.service > " + tempPath.toAbsolutePath().toString());
while (!shell.isOutputCompleted()) {
// TODO: add timeout
}
if (shell.getExitCode() == 0) {
// Wrote to the temp file! Add it to the ctx
var stream = new FileInputStream(tempPath.toFile());
logger.info("Uploading settings with size " + stream.available());
ctx.result(stream);
ctx.contentType("application/zip");
ctx.header("Content-Disposition: attachment; filename=\"photonvision-journalctl.txt\"");
ctx.status(200);
} else {
logger.error("Could not export journactl logs! (exit code != 0)");
ctx.status(500);
}
} catch (IOException e) {
// TODO Auto-generated catch block
logger.error("Could not export journactl logs! (IOexception)", e);
ctx.status(500);
}
}
public static void onCalibrationEnd(Context ctx) {
logger.info("Calibrating camera! This will take a long time...");
var index = Integer.parseInt(ctx.body());

View File

@@ -84,6 +84,7 @@ public class Server {
app.post("/api/settings/import", RequestHandler::onSettingUpload);
app.post("/api/settings/offlineUpdate", RequestHandler::onOfflineUpdate);
app.get("/api/settings/photonvision_config.zip", RequestHandler::onSettingsDownload);
app.get("/api/settings/photonvision-journalctl.txt", RequestHandler::onExportCurrentLogs);
app.post("/api/settings/camera", RequestHandler::onCameraSettingsSave);
app.post("/api/settings/general", RequestHandler::onGeneralSettings);
app.post("/api/settings/endCalibration", RequestHandler::onCalibrationEnd);