├── src-tauri ├── tests │ └── fixtures │ │ ├── 1 │ │ ├── fd │ │ │ └── 1 │ │ ├── cmdline │ │ ├── stat │ │ └── smaps_rollup │ │ ├── uptime │ │ └── stat ├── build.rs ├── .gitignore ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── gen │ └── schemas │ │ └── capabilities.json ├── src │ ├── journal │ │ ├── unit.rs │ │ ├── boot.rs │ │ ├── query.rs │ │ ├── journal_entries.rs │ │ ├── journal_fields.rs │ │ ├── libsdjournal_bindings.rs │ │ ├── query_builder.rs │ │ ├── libsdjournal.rs │ │ └── mod.rs │ ├── monitor │ │ ├── system_status.rs │ │ ├── process_status.rs │ │ ├── uptime.rs │ │ ├── cmdline.rs │ │ ├── fd.rs │ │ ├── pid_stat.rs │ │ ├── smaps_rollup.rs │ │ ├── stat.rs │ │ ├── NOTES.md │ │ └── mod.rs │ ├── main.rs │ ├── journal_controller.rs │ └── monitor_controller.rs ├── capabilities │ └── migrated.json ├── Cargo.toml └── tauri.conf.json ├── .prettierrc.json ├── env.d.ts ├── .gitignore ├── public └── favicon.ico ├── docs ├── screenshot.png ├── screenshot-dark.png └── publish_new_version.md ├── src ├── model │ ├── ProcessQuery.ts │ ├── Unit.ts │ ├── Boot.ts │ ├── Filter.ts │ ├── JournalEntries.ts │ └── Process.ts ├── main.ts ├── common │ └── DateFormatter.ts ├── components │ ├── SearchBar.vue │ ├── ProcessTable.vue │ ├── SummaryBar.vue │ ├── LogTable.vue │ └── FilterSidebar.vue ├── pages │ ├── SystemMonitor.vue │ └── LogViewer.vue └── App.vue ├── .github ├── scripts │ ├── build-dev.sh │ ├── build-release.sh │ └── build-artifacts.sh ├── release.yaml └── workflows │ ├── build.yaml │ └── release-ubuntu22.04.yaml ├── package ├── journal-viewer.desktop ├── PKGBUILD-bin.tmpl └── PKGBUILD-src.tmpl ├── tsconfig.config.json ├── tsconfig.json ├── index.html ├── .eslintrc.cjs ├── README.md ├── vite.config.ts ├── .vscode ├── tasks.json └── launch.json ├── package.json ├── TODO.md └── LICENSE /src-tauri/tests/fixtures/1/fd/1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/tests/fixtures/1/cmdline: -------------------------------------------------------------------------------- 1 | /sbin/init -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/tests/fixtures/uptime: -------------------------------------------------------------------------------- 1 | 567520.93 2074397.49 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | .next 4 | dist/ 5 | out/ -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /docs/screenshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/docs/screenshot-dark.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src/model/ProcessQuery.ts: -------------------------------------------------------------------------------- 1 | export type ProcessQuery = { 2 | sortBy: string; 3 | sortOrder: string; 4 | }; -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src/model/Unit.ts: -------------------------------------------------------------------------------- 1 | export type Unit = { 2 | unit_file: string; 3 | state: string; 4 | preset: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mingue/journal-viewer/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src/model/Boot.ts: -------------------------------------------------------------------------------- 1 | export type Boot = { 2 | index: number; 3 | boot_id: string; 4 | first_entry: number; 5 | last_entry: number; 6 | }; 7 | -------------------------------------------------------------------------------- /.github/scripts/build-dev.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | npm install 6 | npm run build-only 7 | cd src-tauri 8 | cargo clippy 9 | cargo build 10 | -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default"]}} -------------------------------------------------------------------------------- /.github/scripts/build-release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | npm install 6 | pkgver=${GITHUB_REF_NAME:1} 7 | sed -i "s/0.0.1/$pkgver/g" ./src-tauri/tauri.conf.json 8 | npm run tauri-build 9 | -------------------------------------------------------------------------------- /src/model/Filter.ts: -------------------------------------------------------------------------------- 1 | export type Filter = { 2 | priority: string; 3 | services: string[]; 4 | transports: string[]; 5 | datetimeFrom: string; 6 | datetimeTo: string; 7 | bootIds: string[]; 8 | }; 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | import "bootstrap/dist/css/bootstrap.css"; 5 | import "bootstrap-icons/font/bootstrap-icons.css"; 6 | 7 | createApp(App).mount("#app"); 8 | -------------------------------------------------------------------------------- /src-tauri/tests/fixtures/1/stat: -------------------------------------------------------------------------------- 1 | 1 (systemd) S 0 1 1 0 -1 4194560 110521 225951668 821 17649 7278 4048 2989874 207182 20 0 1 0 12 23248896 1986 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 2 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /src-tauri/src/journal/unit.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct Unit { 5 | pub unit_file: String, 6 | pub state: String, 7 | pub preset: Option 8 | } -------------------------------------------------------------------------------- /package/journal-viewer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Journal Viewer 3 | Comment=Visualize systemd logs 4 | Exec=journal-viewer 5 | Terminal=false 6 | Icon=utilities-log-viewer 7 | StartupNotify=false 8 | Type=Application 9 | Categories=System; -------------------------------------------------------------------------------- /src/model/JournalEntries.ts: -------------------------------------------------------------------------------- 1 | export type JournalEntries = { 2 | headers: Array; 3 | rows: Array>; 4 | }; 5 | 6 | export type JournalEntry = { 7 | headers: Array; 8 | values: Array; 9 | }; 10 | -------------------------------------------------------------------------------- /src-tauri/src/journal/boot.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct Boot { 5 | index: i32, 6 | boot_id: String, 7 | first_entry: i64, 8 | last_entry: i64, 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default" 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "vite.config.*", 4 | "vitest.config.*", 5 | "cypress.config.*", 6 | "playwright.config.*" 7 | ], 8 | "compilerOptions": { 9 | "composite": true, 10 | "types": [ 11 | "node" 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /src-tauri/src/monitor/system_status.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Default)] 4 | pub struct SystemStatus { 5 | pub uptime_seconds: f32, 6 | pub user_mode_clicks: usize, 7 | pub kernel_mode_clicks: usize, 8 | pub idle_time_clicks: usize, 9 | } -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Breaking Changes 🛠 4 | labels: 5 | - Semver-Major 6 | - breaking-change 7 | - title: Exciting New Features 🎉 8 | labels: 9 | - Semver-Minor 10 | - enhancement 11 | - title: Other Changes 12 | labels: 13 | - "*" 14 | -------------------------------------------------------------------------------- /src/model/Process.ts: -------------------------------------------------------------------------------- 1 | export type Process = { 2 | pid: string; 3 | cmd: string; 4 | process_name: string; 5 | pss_in_kb: number; 6 | rss_in_kb: number; 7 | uss_in_kb: number; 8 | time_userspace_miliseconds: number; 9 | time_kernel_miliseconds: number; 10 | start_time: number; 11 | cpu_usage_percentage: number; 12 | fds: number; 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | 11 | "references": [ 12 | { 13 | "path": "./tsconfig.config.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript", 10 | "@vue/eslint-config-prettier", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: "latest", 14 | }, 15 | rules: { 16 | "vue/require-v-for-key": "warn", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src-tauri/src/journal/query.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Query { 3 | pub(crate) pid: u32, 4 | pub(crate) fields: Vec, 5 | pub(crate) minimum_priority: u32, 6 | pub(crate) units: Vec, 7 | pub(crate) slice: String, 8 | pub(crate) limit: u64, 9 | pub(crate) date_less_than: u64, 10 | pub(crate) date_more_than: u64, 11 | pub(crate) transports: Vec, 12 | pub(crate) quick_search: String, 13 | pub(crate) reset_position: bool, 14 | pub(crate) boot_ids: Vec, 15 | } 16 | -------------------------------------------------------------------------------- /docs/publish_new_version.md: -------------------------------------------------------------------------------- 1 | # How to publish a new release 2 | 3 | - Update package version on tauri.conf.json package.version 4 | - Build release binaries with: `npm run tauri build` 5 | - Create tar.gz file with release binary and obtain the sha256 6 | - Update PKGBUILD pkgver, pkgrel and sha256sums 7 | - Generate SRCINFO Package `makepkg --printsrcinfo > .SRCINFO` 8 | - Copy tar.gz to package folder 9 | - Make package & install `makepkg --install` 10 | - Run `journal-viewer` to test new binary 11 | - Create new release on GH, upload deb and tar.gz, create new tag 12 | - Copy PKGBUILD and .SRCINFO into AUR repo & push changes 13 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/process_status.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Default, Clone)] 5 | pub struct ProcessStatus { 6 | pub pid: usize, 7 | pub cmd: String, 8 | pub process_name: String, 9 | pub pss_in_kb: usize, 10 | pub rss_in_kb: usize, 11 | pub uss_in_kb: usize, // Pss - Pss_Shmem 12 | pub(super)time_userspace_clicks: usize, 13 | pub(super)time_kernel_clicks: usize, 14 | pub time_userspace_miliseconds: f32, 15 | pub time_kernel_miliseconds: f32, 16 | pub start_time: usize, 17 | pub cpu_usage_percentage: f32, 18 | pub(super) scrapped_timestamp: DateTime, 19 | pub fds: u64, 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/uptime.rs: -------------------------------------------------------------------------------- 1 | use super::SystemStatus; 2 | 3 | pub fn read_file(path: &str, ss: &mut SystemStatus) -> anyhow::Result<()> { 4 | let uptime = std::fs::read_to_string(format!("{path}/uptime"))?; 5 | let fields: Vec<&str> = uptime.split(' ').collect(); 6 | ss.uptime_seconds = fields[0].parse::()?; 7 | 8 | Ok(()) 9 | } 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use anyhow::Result; 14 | use crate::monitor::{uptime, SystemStatus}; 15 | 16 | #[test] 17 | fn read_file() -> Result<()>{ 18 | let mut ss = SystemStatus::default(); 19 | uptime::read_file("./tests/fixtures", &mut ss)?; 20 | assert_eq!(ss.uptime_seconds, 567520.93); 21 | 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/cmdline.rs: -------------------------------------------------------------------------------- 1 | use super::ProcessStatus; 2 | 3 | pub fn read_file(path: &str, pid: &usize, pe: &mut ProcessStatus) -> anyhow::Result<()> { 4 | if let Ok(mut cmd_line) = std::fs::read_to_string(format!("{path}/{pid}/cmdline")) { 5 | cmd_line = cmd_line.trim_matches(char::from(0)).to_owned(); 6 | pe.cmd = cmd_line; 7 | } 8 | 9 | Ok(()) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use crate::monitor::{ProcessStatus, cmdline}; 15 | use anyhow::Result; 16 | 17 | #[test] 18 | fn read_file() -> Result<()> { 19 | let mut pe = ProcessStatus::default(); 20 | cmdline::read_file("./tests/fixtures", &1, &mut pe)?; 21 | assert_eq!(pe.cmd, "/sbin/init"); 22 | 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/DateFormatter.ts: -------------------------------------------------------------------------------- 1 | export const dateFormat = { 2 | month: "2-digit", 3 | day: "numeric", 4 | year: "2-digit", 5 | hour: "2-digit", 6 | minute: "2-digit", 7 | second: "2-digit", 8 | hour12: false, 9 | }; 10 | 11 | export const dateFormatNoSecs = { 12 | month: "2-digit", 13 | day: "numeric", 14 | year: "2-digit", 15 | hour: "2-digit", 16 | minute: "2-digit", 17 | hour12: false, 18 | }; 19 | 20 | export const formatEpoch = (epochTime: string, includeSecs: boolean = false) => { 21 | if (epochTime != null) { 22 | try { 23 | return new Date(parseInt(epochTime)).toLocaleString(undefined, includeSecs ? dateFormat : dateFormatNoSecs); 24 | } catch (error) { 25 | return epochTime; 26 | } 27 | } 28 | return epochTime; 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 18 13 | - uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | components: clippy 16 | - name: add-dependencies 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install -y build-essential curl wget libssl-dev \ 20 | libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev \ 21 | libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev 22 | - name: build 23 | run: ./.github/scripts/build-dev.sh 24 | shell: bash 25 | -------------------------------------------------------------------------------- /src-tauri/tests/fixtures/1/smaps_rollup: -------------------------------------------------------------------------------- 1 | 5e92b4181000-7ffd5abf3000 ---p 00000000 00:00 0 [rollup] 2 | Rss: 86828 kB 3 | Pss: 50469 kB 4 | Pss_Dirty: 39884 kB 5 | Pss_Anon: 32880 kB 6 | Pss_File: 10533 kB 7 | Pss_Shmem: 7056 kB 8 | Shared_Clean: 34312 kB 9 | Shared_Dirty: 14112 kB 10 | Private_Clean: 5576 kB 11 | Private_Dirty: 32828 kB 12 | Referenced: 65604 kB 13 | Anonymous: 32880 kB 14 | KSM: 0 kB 15 | LazyFree: 0 kB 16 | AnonHugePages: 0 kB 17 | ShmemPmdMapped: 0 kB 18 | FilePmdMapped: 2048 kB 19 | Shared_Hugetlb: 0 kB 20 | Private_Hugetlb: 0 kB 21 | Swap: 125304 kB 22 | SwapPss: 76876 kB 23 | Locked: 0 kB -------------------------------------------------------------------------------- /src-tauri/src/journal/journal_entries.rs: -------------------------------------------------------------------------------- 1 | use std::vec; 2 | use serde::{Serialize, Deserialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug)] 5 | pub struct JournalEntries { 6 | pub headers: Vec, 7 | pub rows: Vec>, 8 | } 9 | 10 | impl JournalEntries { 11 | pub fn new(lenght: usize) -> JournalEntries { 12 | JournalEntries{ 13 | headers: vec![], 14 | rows: Vec::with_capacity(lenght) 15 | } 16 | } 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug)] 20 | pub struct JournalEntry { 21 | pub headers: Vec, 22 | pub values: Vec, 23 | } 24 | 25 | impl JournalEntry { 26 | pub fn new() -> JournalEntry { 27 | JournalEntry{ 28 | headers: vec![], 29 | values: vec![], 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/fd.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::prelude::MetadataExt; 2 | 3 | use super::ProcessStatus; 4 | 5 | pub fn read_file( 6 | procs_path: &str, 7 | pid: &usize, 8 | process_entry: &mut ProcessStatus, 9 | ) -> anyhow::Result<()> { 10 | // Obtain size from stats of fd for fast access to open fds 11 | if let Ok(stat) = std::fs::metadata(format!("{procs_path}/{pid}/fd")) { 12 | process_entry.fds = stat.size(); 13 | } 14 | 15 | Ok(()) 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use crate::monitor::{ProcessStatus, fd}; 21 | use anyhow::Result; 22 | 23 | #[test] 24 | fn read_file() -> Result<()> { 25 | let mut pe = ProcessStatus::default(); 26 | fd::read_file("./tests/fixtures", &1, &mut pe)?; 27 | 28 | assert_eq!(pe.fds, 2); 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Journal Viewer 2 | 3 | A modern linux desktop application to visualize systemd logs. 4 | 5 | ## Dark Theme 6 | 7 | ![Journal Viewer Dark Theme](docs/screenshot-dark.png) 8 | 9 | ## Light Theme 10 | 11 | ![Journal Viewer Light Theme](docs/screenshot.png) 12 | 13 | ## Features 14 | 15 | - Summary graph with the latest log entries for the last 5 days or 10k entries 16 | - A quick search to filter messages containing some text (case insensitive). 17 | - A more advanced filter bar where you can search by: 18 | - Log Priority 19 | - Date Range 20 | - Service unit, including init service (systemd) 21 | - Transport, journal, kernel... 22 | - System Boot 23 | - Graphical indication for log level 24 | - Expand log entry on selection 25 | - Infinite scrolling. 26 | - Refresh logs button 27 | - Light/Dark theme 28 | 29 | ## Built with 30 | 31 | - Rust 32 | - Systemd Journald 33 | - Tauri 34 | - Vue 35 | - Bootstrap 36 | -------------------------------------------------------------------------------- /package/PKGBUILD-bin.tmpl: -------------------------------------------------------------------------------- 1 | # Maintainer: Victor Mingueza 2 | pkgname=journal-viewer-bin 3 | pkgver={{pkgver}} 4 | pkgrel=1 5 | pkgdesc="A modern linux desktop application to visualize systemd logs." 6 | arch=('x86_64') 7 | url="https://github.com/mingue/journal-viewer" 8 | license=('GPL3') 9 | depends=( 10 | 'systemd' 11 | 'webkit2gtk-4.1' 12 | ) 13 | optdepends=( 14 | ) 15 | provides=('journal-viewer') 16 | conflicts=('journal-viewer') 17 | 18 | source_x86_64=( 19 | "$url/releases/download/v${pkgver}/${pkgname/-bin/}_${pkgver}_x86_64.tar.gz" 20 | "${pkgname/-bin/}.desktop" 21 | ) 22 | sha256sums_x86_64=( 23 | '{{sha256sumBin}}' 24 | '{{sha256sumDesktop}}' 25 | ) 26 | 27 | package() { 28 | _output="${srcdir}" 29 | install -Dm755 "${_output}/${pkgname/-bin/}" "${pkgdir}/usr/bin/${pkgname/-bin/}" 30 | install -Dm644 "${_output}/${pkgname/-bin/}.desktop" -t "$pkgdir/usr/share/applications/" 31 | } 32 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | "@": fileURLToPath(new URL("./src", import.meta.url)), 12 | }, 13 | }, 14 | // prevent vite from obscuring rust errors 15 | clearScreen: false, 16 | // Tauri expects a fixed port, fail if that port is not available 17 | server: { 18 | strictPort: true, 19 | }, 20 | // to make use of `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`, 21 | // `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE` and `TAURI_DEBUG` 22 | // env variables 23 | envPrefix: ['VITE_', 'TAURI_'], 24 | build: { 25 | // Tauri supports es2021 26 | target: ['es2021', 'chrome100', 'safari13'], 27 | // don't minify for debug builds 28 | minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, 29 | // produce sourcemaps for debug builds 30 | sourcemap: !!process.env.TAURI_DEBUG, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "ui:dev", 8 | "type": "shell", 9 | // `dev` keeps running in the background 10 | // ideally you should also configure a `problemMatcher` 11 | // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson 12 | "isBackground": true, 13 | // change this to your `beforeDevCommand`: 14 | "problemMatcher":"$vite", 15 | "command": "npm", 16 | "args": [ 17 | "run", 18 | "dev" 19 | ] 20 | }, 21 | { 22 | "label": "ui:build", 23 | "type": "shell", 24 | // change this to your `beforeBuildCommand`: 25 | "problemMatcher":"$vite", 26 | "command": "npm", 27 | "args": [ 28 | "run", 29 | "build-only" 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.github/workflows/release-ubuntu22.04.yaml: -------------------------------------------------------------------------------- 1 | name: release2204 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | - uses: actions-rust-lang/setup-rust-toolchain@v1 17 | with: 18 | components: clippy 19 | - name: add-dependencies 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install -y build-essential curl wget libssl-dev \ 23 | libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev \ 24 | libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev 25 | - name: build 26 | run: ./.github/scripts/build-release.sh 27 | shell: bash 28 | - name: build-artifacts 29 | run: ./.github/scripts/build-artifacts.sh 30 | shell: bash 31 | - name: create-draft-release 32 | uses: softprops/action-gh-release@v1 33 | with: 34 | files: | 35 | ./out/*.* 36 | draft: true 37 | prerelease: true 38 | generate_release_notes: true 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "journal-viewer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "run-p build-only", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "tauri-dev": "export RUST_LOG='debug' && export JV_MONITOR_ENABLED='true' && tauri dev", 14 | "tauri-build": "tauri build" 15 | }, 16 | "dependencies": { 17 | "@tauri-apps/api": "^2", 18 | "@vueform/multiselect": "~2", 19 | "@vuepic/vue-datepicker": "~11", 20 | "bootstrap": "~5", 21 | "bootstrap-icons": "~1", 22 | "vue": "~3" 23 | }, 24 | "devDependencies": { 25 | "@rushstack/eslint-patch": "~1", 26 | "@tauri-apps/cli": "^2.4.0", 27 | "@tsconfig/node20": "^20", 28 | "@types/node": "^22", 29 | "@vitejs/plugin-vue": "~5", 30 | "@vue/eslint-config-prettier": "~10", 31 | "@vue/eslint-config-typescript": "~14", 32 | "@vue/tsconfig": "~0.7", 33 | "eslint": "~9", 34 | "eslint-plugin-vue": "~10", 35 | "npm-run-all": "~4", 36 | "prettier": "~3", 37 | "typescript": "~5", 38 | "vite": "~6", 39 | "vue-tsc": "~2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 39 | 40 | 58 | -------------------------------------------------------------------------------- /package/PKGBUILD-src.tmpl: -------------------------------------------------------------------------------- 1 | # Maintainer: Victor Mingueza 2 | pkgname=journal-viewer 3 | pkgver={{pkgver}} 4 | pkgrel=1 5 | pkgdesc="A modern linux desktop application to visualize systemd logs." 6 | arch=('x86_64') 7 | url="https://github.com/mingue/journal-viewer" 8 | license=('GPL3') 9 | depends=( 10 | 'systemd' 11 | 'webkit2gtk-4.1' 12 | ) 13 | makedepends=( 14 | 'base-devel' 15 | 'rustup' 16 | 'npm' 17 | ) 18 | optdepends=( 19 | ) 20 | provides=('journal-viewer') 21 | conflicts=('journal-viewer') 22 | 23 | source_x86_64=( 24 | "$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz" 25 | "$pkgname.desktop" 26 | ) 27 | sha256sums_x86_64=( 28 | '{{sha256sumSrc}}' 29 | '{{sha256sumDesktop}}' 30 | ) 31 | 32 | prepare() { 33 | cd "$pkgname-$pkgver" 34 | npm config set cache "$srcdir/npm-cache" 35 | npm install 36 | 37 | cd src-tauri 38 | export RUSTUP_TOOLCHAIN=stable 39 | cargo fetch --target "$CARCH-unknown-linux-gnu" 40 | } 41 | 42 | build() { 43 | cd "$pkgname-$pkgver" 44 | export RUSTUP_TOOLCHAIN=stable 45 | npm config set cache "$srcdir/npm-cache" 46 | npm run build 47 | npm run tauri build 48 | } 49 | 50 | package() { 51 | cd "$pkgname-$pkgver" 52 | install -Dm755 "src-tauri/target/release/$pkgname" -t "$pkgdir/usr/bin/" 53 | install -Dm644 "$srcdir/$pkgname.desktop" -t "$pkgdir/usr/share/applications/" 54 | } -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Victor Mingueza"] 3 | default-run = "journal-viewer" 4 | description = "A modern desktop log viewer for systemd journal" 5 | edition = "2024" 6 | license = "GPL-3.0" 7 | name = "journal-viewer" 8 | repository = "https://github.com/mingue/journal-viewer" 9 | rust-version = "1.85" 10 | version = "0.3.0" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = {version = "2", features = [] } 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | bitflags = "2" 20 | chrono = "0.4" 21 | env_logger = "0.11" 22 | lazy_static = "1" 23 | libc = "0.2" 24 | nom = "7" 25 | rayon = "1" 26 | serde = {version = "1", features = ["derive"] } 27 | serde_json = "1" 28 | serde_with = {version = "3", features = ["chrono"] } 29 | tauri = {version = "2", features = [] } 30 | thiserror = "1" 31 | tracing = {version = "0.1.41", features = ["log", "attributes"] } 32 | tracing-subscriber = {version = "0.3.20", features = ["std", "env-filter"] } 33 | 34 | [features] 35 | # by default Tauri runs in production mode 36 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 37 | default = ["custom-protocol"] 38 | # this feature is used for production builds where `devPath` points to the filesystem 39 | # DO NOT remove this 40 | custom-protocol = ["tauri/custom-protocol"] 41 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/pid_stat.rs: -------------------------------------------------------------------------------- 1 | use super::ProcessStatus; 2 | 3 | pub fn read_file( 4 | procs_path: &str, 5 | pid: &usize, 6 | process_entry: &mut ProcessStatus, 7 | ) -> anyhow::Result<()> { 8 | if let Ok(stat) = std::fs::read_to_string(format!("{procs_path}/{pid}/stat")) { 9 | let fields: Vec<&str> = stat.split(' ').collect(); 10 | 11 | // Get process name and remove parentesis 12 | process_entry.process_name = fields[1][1..fields[1].len() - 1].to_owned(); 13 | // Get userspace time in clicks 14 | // Get kernel/system time in clicks 15 | process_entry.time_userspace_clicks = fields[13].parse::()?; 16 | process_entry.time_kernel_clicks = fields[14].parse::()?; 17 | process_entry.start_time = fields[21].parse::()?; 18 | process_entry.scrapped_timestamp = chrono::Utc::now(); 19 | } 20 | 21 | Ok(()) 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use crate::monitor::{pid_stat, ProcessStatus}; 27 | use anyhow::Result; 28 | 29 | #[test] 30 | fn read_file() -> Result<()> { 31 | let mut pe = ProcessStatus::default(); 32 | pid_stat::read_file("./tests/fixtures", &1, &mut pe)?; 33 | 34 | assert_eq!(pe.process_name, "systemd"); 35 | assert_eq!(pe.time_userspace_clicks, 7278); 36 | assert_eq!(pe.time_kernel_clicks, 4048); 37 | assert_eq!(pe.start_time, 12); 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Future Improvements 2 | 3 | ## Load more logs as you scroll 4 | 5 | - [x] Load more entries when reching the bottom of the logs 6 | 7 | ## Reuse journal pointer to keep position on journal 8 | 9 | - [x] Allow async queries 10 | - [x] Reuse the journal pointer between queries 11 | 12 | ## Journal filter 13 | 14 | - [x] Priority 15 | - [x] Message text 16 | - [x] Unit 17 | - [x] Time range 18 | - [x] Transports 19 | - [x] Boot 20 | 21 | ## Quick filter bar 22 | 23 | - [x] Search for a specific message, command 24 | 25 | ## Dark theme 26 | 27 | - [x] Add dark theme 28 | - [x] Add auto-detection to select dark theme 29 | 30 | ## Break down components 31 | 32 | - [x] Break down vue components 33 | 34 | ## Refactor to use anyhow 35 | 36 | - [x] Use anyhow crate for app errors 37 | 38 | ## Publish to AUR 39 | 40 | - [x] Published in 41 | 42 | ## Automate relase process 43 | 44 | - [x] Document build & publish process 45 | - [x] Automate process, partially done, missing aur upload 46 | 47 | ## Investigate possibility of flatpak 48 | 49 | ## Bugs 50 | 51 | - [x] Invalid date on most recent block summary, some entries don't have a valid date, so we just filter them out from summary 52 | - [x] Log entry: Could not find the field -2, time field is not found, change message to display field and the JournalError code 53 | - [x] Loading new entries on scroll might overlap and require other kind of filter or maintain the position on the open journal 54 | -------------------------------------------------------------------------------- /.github/scripts/build-artifacts.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | pkgver=${GITHUB_REF_NAME:1} 6 | echo "Publishing version: $pkgver" 7 | 8 | mkdir -p out 9 | rm -rf ./out/* 10 | 11 | cd ./src-tauri/target/release/ 12 | tar czf 'journal-viewer_'$pkgver'_x86_64.tar.gz' journal-viewer 13 | cd ../../../ 14 | 15 | cp './src-tauri/target/release/journal-viewer_'$pkgver'_x86_64.tar.gz' ./out/ 16 | cp './src-tauri/target/release/bundle/deb/journal-viewer_'$pkgver'_amd64.deb' ./out/ 17 | cp './src-tauri/target/release/bundle/rpm/journal-viewer-'$pkgver'-1.x86_64.rpm' ./out/ 18 | cp ./package/journal-viewer.desktop ./out/ 19 | cp ./package/PKGBUILD-bin.tmpl ./out/PKGBUILD-bin 20 | cp ./package/PKGBUILD-src.tmpl ./out/PKGBUILD-src 21 | 22 | cd out 23 | sha256sumBin=$(sha256sum 'journal-viewer_'$pkgver'_x86_64.tar.gz' | awk '{print $1}') 24 | echo "bin SHA: $sha256sumBin" 25 | sha256sumDesktop=$(sha256sum journal-viewer.desktop | awk '{print $1}') 26 | echo "desktop SHA: $sha256sumDesktop" 27 | 28 | sed -i "s/{{pkgver}}/$pkgver/g" PKGBUILD-bin 29 | sed -i "s/{{pkgver}}/$pkgver/g" PKGBUILD-src 30 | sed -i "s/{{sha256sumBin}}/$sha256sumBin/g" PKGBUILD-bin 31 | #sha256sumSrc should be replaced before pushing to AUR as we don't have tar.gz of src code yet 32 | sed -i "s/{{sha256sumDesktop}}/$sha256sumDesktop/g" PKGBUILD-bin 33 | sed -i "s/{{sha256sumDesktop}}/$sha256sumDesktop/g" PKGBUILD-src 34 | 35 | tar czf 'journal-viewer_'$pkgver'_x86_64_AUR.tar.gz' PKGBUILD-bin PKGBUILD-src 36 | 37 | rm journal-viewer.desktop 38 | rm PKGBUILD-bin 39 | rm PKGBUILD-src 40 | 41 | ls -la -h 42 | 43 | cd .. 44 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "command": "npm run tauri dev", 9 | "name": "tauri dev", 10 | "request": "launch", 11 | "type": "node-terminal", 12 | "env": { 13 | "RUST_BACKTRACE": "1", 14 | "RUST_LOG": "debug", 15 | "RUST_LOG_STYLE": "always" 16 | } 17 | }, 18 | { 19 | "type": "lldb", 20 | "request": "launch", 21 | "name": "Tauri Development Debug", 22 | "cargo": { 23 | "args": [ 24 | "build", 25 | "--manifest-path=./src-tauri/Cargo.toml", 26 | "--no-default-features" 27 | ] 28 | }, 29 | // task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json` 30 | "preLaunchTask": "ui:dev", 31 | "env": { 32 | "RUST_BACKTRACE": "1", 33 | "RUST_LOG": "debug", 34 | "RUST_LOG_STYLE": "always" 35 | } 36 | }, 37 | { 38 | "type": "lldb", 39 | "request": "launch", 40 | "name": "Tauri Production Debug", 41 | "cargo": { 42 | "args": [ 43 | "build", 44 | "--release", 45 | "--manifest-path=./src-tauri/Cargo.toml" 46 | ] 47 | }, 48 | // task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json` 49 | "preLaunchTask": "ui:build" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /src/pages/SystemMonitor.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /src-tauri/src/journal/journal_fields.rs: -------------------------------------------------------------------------------- 1 | /// message id 2 | pub const MESSAGE_ID: &str = "MESSAGE_ID"; 3 | /// human-readable message string for this entry 4 | pub const MESSAGE: &str = "MESSAGE"; 5 | /// priority value between 0 ("emerg") and 7 ("debug") 6 | pub const PRIORITY: &str = "PRIORITY"; 7 | /// low-level Unix error number causing this entry, if any 8 | pub const ERRNO: &str = "ERRNO"; 9 | /// This is the time in microseconds since the epoch UTC, formatted as a decimal string 10 | pub const SOURCE_REALTIME_TIMESTAMP: &str = "_SOURCE_REALTIME_TIMESTAMP"; 11 | 12 | /// The process ID of the process the journal entry originates from 13 | pub const PID: &str = "_PID"; 14 | /// The user ID of the process the journal entry originates from 15 | pub const UID: &str = "_UID"; 16 | /// The group ID of the process the journal entry originates from 17 | pub const GID: &str = "_GID"; 18 | 19 | /// The name of the process 20 | pub const COMM: &str = "_COMM"; 21 | /// the executable path 22 | pub const EXE: &str = "_EXE"; 23 | /// command line of the process 24 | pub const CMDLINE: &str = "_CMDLINE"; 25 | 26 | /// the systemd slice unit name 27 | pub const SYSTEMD_SLICE: &str = "_SYSTEMD_SLICE"; 28 | /// the systemd unit name 29 | pub const SYSTEMD_UNIT: &str = "_SYSTEMD_UNIT"; 30 | /// The control group path in the systemd hierarchy 31 | pub const SYSTEMD_CGROUP: &str = "_SYSTEMD_CGROUP"; 32 | 33 | /// unit is used for filtering 34 | pub const UNIT_FILTER: &str = "UNIT"; 35 | /// The kernel boot ID 36 | pub const BOOT_ID: &str = "_BOOT_ID"; 37 | 38 | /// How the entry was received by the journal service 39 | /// Valid transports are: 40 | /// audit: for those read from the kernel audit subsystem 41 | /// driver: for internally generated messages 42 | /// syslog: for those received via the local syslog socket 43 | /// journal: for those received via the native journal protocol 44 | /// stdout: for those read from a service's standard output or error output 45 | /// kernel: for those read from the kernel 46 | pub const TRANSPORT: &str = "_TRANSPORT"; 47 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/smaps_rollup.rs: -------------------------------------------------------------------------------- 1 | use nom::{bytes::complete::*, character::complete::*}; 2 | 3 | use super::ProcessStatus; 4 | 5 | pub fn read_file( 6 | procs_path: &str, 7 | pid: &usize, 8 | process_entry: &mut ProcessStatus, 9 | ) -> anyhow::Result<()> { 10 | if let Ok(smaps_rollup) = std::fs::read_to_string(format!("{procs_path}/{pid}/smaps_rollup")) { 11 | let lines = smaps_rollup.lines(); 12 | 13 | let lines: Vec = lines.skip(1).map(|l| l.to_string()).collect(); 14 | 15 | let fields: Vec = lines 16 | .iter() 17 | .map(|l| parse_line(l)) 18 | .filter_map(|r| r.ok()) 19 | .map(|r| r.1) 20 | .collect(); 21 | 22 | process_entry.pss_in_kb = fields.iter().find(|sl| sl.label == "Pss").unwrap().kb; 23 | process_entry.rss_in_kb = fields.iter().find(|sl| sl.label == "Rss").unwrap().kb; 24 | process_entry.uss_in_kb = 25 | process_entry.pss_in_kb - fields.iter().find(|sl| sl.label == "Pss_Shmem").unwrap().kb; 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | fn parse_line(l: &'_ str) -> nom::IResult<&'_ str, SmapLine<'_>> { 32 | let (r, label) = take_until(":")(l)?; 33 | let (r, _) = tag(":")(r)?; 34 | let (r, _) = multispace0(r)?; 35 | let (r, kb) = digit0(r)?; 36 | let kb = kb.parse::().expect("error parsing kb"); 37 | let (r, _) = multispace0(r)?; 38 | let (_, _) = tag("kB")(r)?; 39 | 40 | Ok((l, SmapLine { label, kb })) 41 | } 42 | 43 | struct SmapLine<'a> { 44 | label: &'a str, 45 | kb: usize, 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use crate::monitor::{ProcessStatus, smaps_rollup}; 51 | use anyhow::Result; 52 | 53 | #[test] 54 | fn read_file() -> Result<()> { 55 | let mut pe: ProcessStatus = ProcessStatus::default(); 56 | smaps_rollup::read_file("./tests/fixtures", &1, &mut pe)?; 57 | assert_eq!(pe.pss_in_kb, 50469); 58 | assert_eq!(pe.rss_in_kb, 86828); 59 | assert_eq!(pe.uss_in_kb, 50469 - 7056); 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "frontendDist": "../dist", 7 | "devUrl": "http://localhost:5173" 8 | }, 9 | "bundle": { 10 | "active": true, 11 | "category": "Utility", 12 | "copyright": "", 13 | "license": "GPL-3.0", 14 | "licenseFile": "../LICENSE", 15 | "targets": [ 16 | "deb", 17 | "rpm" 18 | ], 19 | "externalBin": [], 20 | "icon": [ 21 | "icons/32x32.png", 22 | "icons/128x128.png", 23 | "icons/128x128@2x.png", 24 | "icons/icon.icns", 25 | "icons/icon.ico" 26 | ], 27 | "windows": { 28 | "certificateThumbprint": null, 29 | "digestAlgorithm": "sha256", 30 | "timestampUrl": "" 31 | }, 32 | "longDescription": "A modern desktop log viewer for systemd journal", 33 | "macOS": { 34 | "entitlements": null, 35 | "exceptionDomain": "", 36 | "frameworks": [], 37 | "providerShortName": null, 38 | "signingIdentity": null 39 | }, 40 | "resources": [], 41 | "shortDescription": "A modern desktop log viewer for systemd journal", 42 | "linux": { 43 | "deb": { 44 | "depends": [ 45 | "systemd", 46 | "libwebkit2gtk-4.1-0" 47 | ] 48 | }, 49 | "rpm": { 50 | "depends": [ 51 | "systemd", 52 | "webkit2gtk4.1", 53 | "libappindicator-gtk3", 54 | "xdotool", 55 | "openssl-libs", 56 | "librsvg2" 57 | ], 58 | "desktopTemplate": "../package/journal-viewer.desktop" 59 | } 60 | } 61 | }, 62 | "productName": "journal-viewer", 63 | "mainBinaryName": "journal-viewer", 64 | "version": "0.0.1", 65 | "identifier": "com.vmingueza.journal-viewer", 66 | "plugins": {}, 67 | "app": { 68 | "windows": [ 69 | { 70 | "fullscreen": false, 71 | "height": 800, 72 | "resizable": true, 73 | "title": "Journal Viewer", 74 | "width": 1024, 75 | "useHttpsScheme": true 76 | } 77 | ], 78 | "security": { 79 | "csp": null 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src-tauri/src/monitor/stat.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | Needed, bytes::complete::*, 3 | }; 4 | 5 | use super::SystemStatus; 6 | 7 | pub fn read_file(procs_path: &str, system_status: &mut SystemStatus) -> anyhow::Result<()> { 8 | let stat = std::fs::read_to_string(format!("{procs_path}/stat"))?; 9 | let lines = stat.lines(); 10 | 11 | let lines: Vec = lines.map(|l| l.to_string()).collect(); 12 | 13 | let fields: Vec = lines 14 | .iter() 15 | .filter(|l| l.starts_with("cpu")) 16 | .map(|l| parse_line(l)) 17 | .filter_map(|r| r.ok()) 18 | .map(|r| r.1) 19 | .collect(); 20 | 21 | system_status.idle_time_clicks = fields[0].idle_time_clicks; 22 | system_status.kernel_mode_clicks = fields[0].kernel_mode_clicks; 23 | system_status.user_mode_clicks = fields[0].user_mode_clicks; 24 | // TODO: Add per cpu usage 25 | 26 | Ok(()) 27 | } 28 | 29 | fn parse_line(l: &str) -> nom::IResult<&str, StatLine> { 30 | let (r, _) = tag("cpu")(l)?; 31 | let (r, core_nr) = take_until(" ")(r)?; 32 | let core_nr = core_nr 33 | .parse::() 34 | .map_err(|_e| nom::Err::Incomplete(Needed::Unknown))?; 35 | let (r, _) = take_while(|c| c == ' ')(r)?; 36 | let fields: Vec<&str> = r.split(' ').collect(); 37 | let normal = fields[0] 38 | .parse::() 39 | .map_err(|_e| nom::Err::Incomplete(Needed::Unknown))?; 40 | let kernel = fields[2] 41 | .parse::() 42 | .map_err(|_e| nom::Err::Incomplete(Needed::Unknown))?; 43 | let idle = fields[3] 44 | .parse::() 45 | .map_err(|_e| nom::Err::Incomplete(Needed::Unknown))?; 46 | 47 | Ok(( 48 | l, 49 | StatLine { 50 | core_nr, 51 | user_mode_clicks: normal, 52 | kernel_mode_clicks: kernel, 53 | idle_time_clicks: idle, 54 | }, 55 | )) 56 | } 57 | 58 | struct StatLine { 59 | core_nr: usize, 60 | user_mode_clicks: usize, 61 | kernel_mode_clicks: usize, 62 | idle_time_clicks: usize, 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use crate::monitor::{smaps_rollup, ProcessStatus}; 68 | use anyhow::Result; 69 | 70 | #[test] 71 | fn read_file() -> Result<()> { 72 | let mut pe: ProcessStatus = ProcessStatus::default(); 73 | smaps_rollup::read_file("./tests/fixtures", &1, &mut pe)?; 74 | assert_eq!(pe.pss_in_kb, 50469); 75 | assert_eq!(pe.rss_in_kb, 86828); 76 | assert_eq!(pe.uss_in_kb, 50469 - 7056); 77 | Ok(()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/NOTES.md: -------------------------------------------------------------------------------- 1 | # Process Summary 2 | 3 | ## Memory 4 | 5 | - [x] Obtain memory from /proc/PID/smaps_rollup Pss (Proportional Set Size) 6 | Proportional part of memory pages shares with other processes 7 | - [x] Rss (Resident Set Size) /proc/PID/smaps_rollup 8 | - [x] Pid /proc/PID/stat 1st field (obtained from folder name) 9 | - [x] process name /proc/PID/stat 2nd field 10 | - [x] cmdline /proc/PID/cmdline 11 | - [x] read count of fd open 12 | 13 | - [x] Use Pss, Uss, Rss https://github.com/kwkroeger/smem/blob/master/smem for more details 14 | 15 | - [x] Calculate CPU Usage https://www.baeldung.com/linux/total-process-cpu-usage 16 | - [x] /proc/uptime 17 | 1st time in seconds since the system started 18 | - [x]/proc/pid/stat 19 | - utime 14th time of process in user mode 20 | - stime 15th time of process in kernel mode 21 | - 22nd start time 22 | - [x] Calc 23 | let PROCESS_ELAPSED_SEC="$SYSTEM_UPTIME_SEC - $PROCESS_STARTTIME_SEC" 24 | let PROCESS_USAGE_SEC="$PROCESS_UTIME_SEC + $PROCESS_STIME_SEC" 25 | let PROCESS_USAGE="$PROCESS_USAGE_SEC * 100 / $PROCESS_ELAPSED_SEC" 26 | 27 | - [x] getconf CLK_TCK to obtain clicks and transform stat times to seconds 28 | - [x] Parallel https://docs.rs/rayon/latest/rayon/ 29 | 30 | - Processes 31 | - [x] Record scraping time 32 | - [ ] Accumulate up to 60 records every 5s 33 | - [x] Calculate diff with the last record 34 | - [x] Store Process in Hashmap 35 | 36 | - System 37 | - [ ] Get CPU Usage from /proc/stat, time spend on different kinds of work on clicks 38 | 1st user: normal processes executing in user mode 39 | 3rd system: processes executing in kernel mode 40 | 4th idle 41 | - [ ] Get Mem Usage from /proc/meminfo 42 | MemTotal: 15667100 kB - System memory 43 | MemAvailable: 3762864 kB - Memory available before having to swap 44 | - [ ] Accumulate up to 60 records every 5s 45 | 46 | - Styling 47 | - https://getbootstrap.com/docs/5.3/components/navs-tabs/ 48 | - Gnome System Monitor https://www.google.com/search?sca_esv=00efb85e6f8ba711&sxsrf=ADLYWIJt2slCChT7v3p23-qyCsihQzIvXQ:1716454289057&q=system+monitor&uds=ADvngMgB_2oBFo8AreHDKVw-lNLWaFlzoge-zM-ViyheC_aJJ4j1Gxo8ZWzaUXe9hyclgS6oaqzQO5icfNDFW3XV2bQlwlVongAjmdsVz4G2VTTdUQWtZLvsx3tM3Ee8fLwflTJSsmBgQSl_2FmchuoG5wLSoec-OiMyv0r146ofKLRRAV8tXQZQW-_hwg3iKY25koIaMiTd-IUnffHD8aGRd2104jQZXCr1QwWPdPHzhsVx5JjXg3p3c9dPrO_0Ofsjll2r33pdheVmVaoCV0Azz9r_BTXpWvxTmDKvb5uFRsNyYdyHHneYDxt0UbK0nLctvx0wZuVU&udm=2&prmd=ivnmbt&sa=X&ved=2ahUKEwiYirDBsqOGAxX-2wIHHSH6AQgQtKgLegQIDhAB&biw=1760&bih=838&dpr=1.09#vhid=pkL0-KJCGhzr5M&vssid=mosaic 49 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::pedantic)] 2 | #![allow(dead_code)] 3 | #![cfg_attr( 4 | all(not(debug_assertions), target_os = "windows"), 5 | windows_subsystem = "windows" 6 | )] 7 | 8 | mod journal; 9 | mod journal_controller; 10 | mod monitor; 11 | mod monitor_controller; 12 | 13 | #[macro_use] 14 | extern crate tracing; 15 | #[macro_use] 16 | extern crate lazy_static; 17 | 18 | use std::env; 19 | use std::str::FromStr; 20 | 21 | use crate::journal::Journal; 22 | use crate::journal::JournalError; 23 | use crate::journal::OpenFlags; 24 | use crate::monitor::Monitor; 25 | use serde::Deserialize; 26 | use serde::Serialize; 27 | use tauri::async_runtime::Mutex; 28 | use tracing_subscriber::filter::EnvFilter; 29 | use tracing_subscriber::fmt; 30 | use tracing_subscriber::prelude::*; 31 | 32 | fn main() { 33 | let fmt_layer = fmt::layer(); 34 | let filter_layer = EnvFilter::try_from_default_env() 35 | .or_else(|_| EnvFilter::try_new("info")) 36 | .unwrap(); 37 | 38 | tracing_subscriber::registry() 39 | .with(filter_layer) 40 | .with(fmt_layer) 41 | .init(); 42 | 43 | let j = Journal::open( 44 | OpenFlags::SD_JOURNAL_LOCAL_ONLY 45 | | OpenFlags::SD_JOURNAL_SYSTEM 46 | | OpenFlags::SD_JOURNAL_CURRENT_USER, 47 | ) 48 | .unwrap(); 49 | 50 | let m = Monitor::new(); 51 | 52 | info!("Starting journal logger"); 53 | tauri::Builder::default() 54 | .manage(Mutex::new(j)) 55 | .manage(Mutex::new(m)) 56 | .invoke_handler(tauri::generate_handler![ 57 | journal_controller::get_logs, 58 | journal_controller::get_summary, 59 | journal_controller::get_services, 60 | journal_controller::get_full_entry, 61 | journal_controller::get_boots, 62 | monitor_controller::get_system_status, 63 | monitor_controller::get_processes, 64 | get_config, 65 | ]) 66 | .run(tauri::generate_context!()) 67 | .expect("error while running tauri application"); 68 | } 69 | 70 | #[derive(Serialize, Deserialize, Debug)] 71 | #[serde(rename_all = "camelCase")] 72 | pub struct Config { 73 | system_monitor_enabled: bool, 74 | } 75 | 76 | #[tauri::command] 77 | #[instrument] 78 | fn get_config() -> Result { 79 | let mut config = Config { 80 | system_monitor_enabled: false, 81 | }; 82 | 83 | if let Ok(monitor_enabled) = env::var("JV_MONITOR_ENABLED") { 84 | config.system_monitor_enabled = FromStr::from_str(&monitor_enabled).unwrap(); 85 | } 86 | Ok(config) 87 | } 88 | -------------------------------------------------------------------------------- /src-tauri/tests/fixtures/stat: -------------------------------------------------------------------------------- 1 | cpu 214130 414 67440 4804944 5381 17652 7675 0 0 0 2 | cpu0 29350 74 7354 385942 553 1323 894 0 0 0 3 | cpu1 28489 122 5419 391029 421 783 556 0 0 0 4 | cpu2 17338 12 5859 398365 560 3569 423 0 0 0 5 | cpu3 10510 7 3782 411021 475 772 263 0 0 0 6 | cpu4 10354 6 4786 406978 476 1975 1828 0 0 0 7 | cpu5 4975 1 2365 419015 360 516 117 0 0 0 8 | cpu6 17895 20 5509 400450 520 1149 311 0 0 0 9 | cpu7 14269 15 5645 403309 409 2469 233 0 0 0 10 | cpu8 28102 58 10453 382732 515 2232 2286 0 0 0 11 | cpu9 27853 82 5448 392434 342 684 164 0 0 0 12 | cpu10 14488 6 6011 403558 475 1298 327 0 0 0 13 | cpu11 10500 4 4804 410106 271 877 267 0 0 0 14 | intr 37811686 44 7579 0 0 0 0 0 143723 0 3106 2311524 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1237589 0 0 0 0 0 0 0 0 167 0 0 0 0 0 0 0 0 1091 46 43863 49960 62018 42066 17866 16319 29917 1231909 1766416 143723 0 0 0 23409 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 | ctxt 54160597 16 | btime 1716883248 17 | processes 32134 18 | procs_running 6 19 | procs_blocked 0 20 | softirq 12165166 1023546 1000932 380 1781820 132 0 228063 4373752 1096 3755445 21 | -------------------------------------------------------------------------------- /src-tauri/src/journal/libsdjournal_bindings.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_char, c_int, c_ulong, c_void, size_t}; 2 | 3 | #[link(name = "libsystemd.so.0", kind = "dylib", modifiers = "+verbatim")] 4 | unsafe extern "C" { 5 | // int sd_journal_open(sd_journal **ret, int flags); 6 | pub fn sd_journal_open(sd_journal: &mut *mut c_void, flags: u32) -> c_int; 7 | 8 | //void sd_journal_close(sd_journal *j); 9 | pub fn sd_journal_close(sd_journal: *mut c_void); 10 | 11 | //int sd_journal_next(sd_journal *j); 12 | pub fn sd_journal_next(sd_journal: *mut c_void) -> c_int; 13 | 14 | //int sd_journal_previous(sd_journal *j); 15 | pub fn sd_journal_previous(sd_journal: *mut c_void) -> c_int; 16 | 17 | //int sd_journal_get_data(sd_journal *j, const char *field, const void **data, size_t *length); 18 | pub fn sd_journal_get_data( 19 | sd_journal: *mut c_void, 20 | field: *const c_char, 21 | data: &mut *mut c_void, 22 | size: *mut size_t, 23 | ) -> c_int; 24 | 25 | //int sd_journal_add_match(sd_journal *j, const void *data, size_t size); 26 | pub fn sd_journal_add_match( 27 | sd_journal: *mut c_void, 28 | data: *const c_char, 29 | size: size_t, 30 | ) -> c_int; 31 | 32 | //int sd_journal_seek_head(sd_journal *j); 33 | pub fn sd_journal_seek_head(sd_journal: *mut c_void) -> c_int; 34 | 35 | //int sd_journal_seek_tail(sd_journal *j); 36 | pub fn sd_journal_seek_tail(sd_journal: *mut c_void) -> c_int; 37 | 38 | //int sd_journal_next_skip(sd_journal *j, uint64_t skip); 39 | pub fn sd_journal_next_skip(sd_journal: *mut c_void, skip: c_ulong) -> c_int; 40 | 41 | //int sd_journal_previous_skip(sd_journal *j, uint64_t skip); 42 | pub fn sd_journal_previous_skip(sd_journal: *mut c_void, skip: u64) -> c_int; 43 | 44 | //int sd_journal_add_disjunction(sd_journal *j); 45 | pub fn sd_journal_add_disjunction(sd_journal: *mut c_void) -> c_int; 46 | 47 | //int sd_journal_add_conjunction(sd_journal *j); 48 | pub fn sd_journal_add_conjunction(sd_journal: *mut c_void) -> c_int; 49 | 50 | //void sd_journal_flush_matches(sd_journal *j); 51 | pub fn sd_journal_flush_matches(sd_journal: *mut c_void); 52 | 53 | //int sd_journal_get_realtime_usec(sd_journal *j, uint64_t *usec); 54 | pub fn sd_journal_get_realtime_usec(sd_journal: *mut c_void, microseconds: *mut u64) -> c_int; 55 | 56 | //int sd_journal_seek_realtime_usec(sd_journal *j, uint64_t usec); 57 | pub fn sd_journal_seek_realtime_usec(sd_journal: *mut c_void, microseconds: u64) -> c_int; 58 | 59 | // int sd_journal_enumerate_data(sd_journal *j, const void **data, size_t *length); 60 | pub fn sd_journal_enumerate_data( 61 | sd_journal: *mut c_void, 62 | data: &mut *mut c_void, 63 | size: *mut size_t, 64 | ) -> c_int; 65 | 66 | // TODO: Add support when debian12 is released 67 | // int sd_journal_enumerate_available_data(sd_journal *j, const void **data, size_t *length); 68 | // pub fn sd_journal_enumerate_available_data( 69 | // sd_journal: *mut c_void, 70 | // data: &mut *mut c_void, 71 | // size: *mut size_t, 72 | // ) -> c_int; 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/LogViewer.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 128 | 129 | 131 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 78 | 79 | 134 | -------------------------------------------------------------------------------- /src-tauri/src/journal/query_builder.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use super::{journal_fields, query::Query}; 4 | 5 | pub struct QueryBuilder { 6 | query: Query, 7 | } 8 | 9 | impl QueryBuilder { 10 | pub fn default() -> Self { 11 | let query = Query { 12 | pid: 0, 13 | fields: vec![], 14 | minimum_priority: 4, 15 | units: vec![], 16 | slice: String::new(), 17 | boot_ids: vec![], 18 | limit: 100, 19 | transports: vec!["syslog".into(), "journal".into(), "stdout".into()], 20 | date_less_than: 0, 21 | date_more_than: 0, 22 | quick_search: String::new(), 23 | reset_position: true, 24 | }; 25 | 26 | let mut qb = QueryBuilder { query }; 27 | 28 | qb.with_default_fields(); 29 | 30 | qb 31 | } 32 | 33 | pub fn with_default_fields(&mut self) -> &mut Self { 34 | self.query.fields.clear(); 35 | 36 | self.query.fields.extend([ 37 | journal_fields::MESSAGE.to_owned(), 38 | journal_fields::PRIORITY.to_owned(), 39 | journal_fields::ERRNO.to_owned(), 40 | journal_fields::SOURCE_REALTIME_TIMESTAMP.to_owned(), 41 | journal_fields::PID.to_owned(), 42 | journal_fields::UID.to_owned(), 43 | journal_fields::COMM.to_owned(), 44 | journal_fields::SYSTEMD_SLICE.to_owned(), 45 | journal_fields::SYSTEMD_UNIT.to_owned(), 46 | journal_fields::SYSTEMD_CGROUP.to_owned(), 47 | journal_fields::BOOT_ID.to_owned(), 48 | journal_fields::TRANSPORT.to_owned(), 49 | ]); 50 | 51 | self 52 | } 53 | 54 | pub fn with_fields(&mut self, fields: Vec) -> &mut Self { 55 | self.query.fields.clear(); 56 | 57 | self.query.fields.extend(fields); 58 | 59 | self 60 | } 61 | 62 | pub fn with_transports(&mut self, transports: Vec) -> &mut Self { 63 | self.query.transports = transports; 64 | self 65 | } 66 | 67 | pub fn with_pid(&mut self, pid: u32) -> &mut Self { 68 | self.query.pid = pid; 69 | self 70 | } 71 | 72 | pub fn with_limit(&mut self, limit: u64) -> &mut Self { 73 | self.query.limit = limit; 74 | self 75 | } 76 | 77 | pub fn with_quick_search(&mut self, quick_search: String) -> &mut Self { 78 | self.query.quick_search = quick_search.to_lowercase(); 79 | self 80 | } 81 | 82 | pub fn reset_position(&mut self, reset_position: bool) -> &mut Self { 83 | self.query.reset_position = reset_position; 84 | self 85 | } 86 | 87 | pub fn with_date_less_than(&mut self, from_epoch: u64) -> &mut Self { 88 | self.query.date_less_than = from_epoch; 89 | self 90 | } 91 | 92 | pub fn with_date_more_than(&mut self, to_epoch: u64) -> &mut Self { 93 | self.query.date_more_than = to_epoch; 94 | self 95 | } 96 | 97 | pub fn with_priority_above_or_equal_to(&mut self, minimum_priority: u32) -> &mut Self { 98 | if minimum_priority > 7 { 99 | self.query.minimum_priority = 7; 100 | } else { 101 | self.query.minimum_priority = minimum_priority; 102 | } 103 | 104 | self 105 | } 106 | 107 | pub fn with_units(&mut self, units: Vec) -> &mut Self { 108 | self.query.units = units; 109 | self 110 | } 111 | 112 | pub fn within_slice(&mut self, slice: &str) -> &mut Self { 113 | self.query.slice = String::from(slice); 114 | self 115 | } 116 | 117 | pub fn with_boot_ids(&mut self, boot_ids: Vec) -> &mut Self { 118 | self.query.boot_ids = boot_ids; 119 | self 120 | } 121 | 122 | pub fn build(&mut self) -> Query { 123 | let qb = QueryBuilder::default(); 124 | let old_qb = mem::replace(self, qb); 125 | 126 | old_qb.query 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src-tauri/src/journal_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::journal::Boot; 2 | use crate::journal::JournalError; 3 | use crate::journal::Unit; 4 | use crate::journal::{INIT_UNIT, QueryBuilder}; 5 | use crate::journal::{Journal, OpenFlags}; 6 | use crate::journal::{JournalEntries, JournalEntry}; 7 | use chrono::{DateTime, Duration, Utc}; 8 | use serde::Deserialize; 9 | use tauri::async_runtime::Mutex; 10 | 11 | #[derive(Debug, Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct JournalQuery { 14 | fields: Vec, 15 | priority: u32, 16 | limit: u64, 17 | quick_search: String, 18 | reset_position: bool, 19 | services: Vec, 20 | transports: Vec, 21 | datetime_from: String, 22 | datetime_to: String, 23 | boot_ids: Vec, 24 | } 25 | 26 | #[tauri::command] 27 | #[instrument] 28 | pub(crate) async fn get_logs( 29 | mut query: JournalQuery, 30 | journal: tauri::State<'_, Mutex>, 31 | ) -> Result { 32 | debug!("Getting logs..."); 33 | 34 | // If systemd service is specified, remove from unit filter 35 | // and add it back later as pid filter 36 | let mut add_init_filter = false; 37 | if !query.services.is_empty() && query.services[0] == INIT_UNIT { 38 | query.services.remove(0); 39 | add_init_filter = true; 40 | } 41 | 42 | let mut qb = QueryBuilder::default(); 43 | let q = qb 44 | .with_fields(query.fields) 45 | .with_limit(query.limit) 46 | .with_quick_search(query.quick_search) 47 | .reset_position(query.reset_position) 48 | .with_priority_above_or_equal_to(query.priority) 49 | .with_units(query.services) 50 | .with_transports(query.transports) 51 | .with_boot_ids(query.boot_ids); 52 | 53 | // Add back filter for systemd service as pid=1 54 | if add_init_filter { 55 | q.with_pid(1); 56 | } 57 | 58 | let date_from = DateTime::parse_from_rfc3339(&query.datetime_from).ok(); 59 | let date_to = DateTime::parse_from_rfc3339(&query.datetime_to).ok(); 60 | 61 | if let Some(x) = date_from { 62 | q.with_date_more_than(x.timestamp_micros() as u64); 63 | } 64 | 65 | if let Some(x) = date_to { 66 | q.with_date_less_than(x.timestamp_micros() as u64); 67 | } else { 68 | let datetime_to = Utc::now() + Duration::days(1); 69 | q.with_date_less_than(datetime_to.timestamp_micros() as u64); 70 | } 71 | 72 | let q = q.build(); 73 | 74 | let lock = journal.lock().await; 75 | let logs = lock.query_logs(&q)?; 76 | debug!("Found {} entries.", logs.rows.len()); 77 | 78 | Ok(logs) 79 | } 80 | 81 | #[tauri::command] 82 | #[instrument] 83 | pub(crate) async fn get_full_entry(timestamp: u64) -> Result { 84 | debug!("Getting full entry for timestamp {}...", timestamp); 85 | 86 | let j = Journal::open( 87 | OpenFlags::SD_JOURNAL_LOCAL_ONLY 88 | | OpenFlags::SD_JOURNAL_SYSTEM 89 | | OpenFlags::SD_JOURNAL_CURRENT_USER, 90 | ) 91 | .unwrap(); 92 | 93 | let entry = j.get_full_entry(timestamp)?; 94 | 95 | debug!("Found entry for timestamp {}", timestamp); 96 | 97 | Ok(entry) 98 | } 99 | 100 | #[derive(Debug, Deserialize)] 101 | #[serde(rename_all = "camelCase")] 102 | pub struct SummaryQuery { 103 | priority: u32, 104 | } 105 | 106 | #[tauri::command] 107 | #[instrument] 108 | pub(crate) async fn get_summary(query: SummaryQuery) -> Result { 109 | debug!("Getting summary..."); 110 | let j = Journal::open( 111 | OpenFlags::SD_JOURNAL_LOCAL_ONLY 112 | | OpenFlags::SD_JOURNAL_SYSTEM 113 | | OpenFlags::SD_JOURNAL_CURRENT_USER, 114 | ) 115 | .unwrap(); 116 | 117 | let datetime_from = Utc::now() - Duration::days(5); 118 | let datetime_to = Utc::now() + Duration::days(1); 119 | let mut qb = QueryBuilder::default(); 120 | let q = qb 121 | .with_fields(vec!["__REALTIME".into()]) 122 | .with_limit(10_000) 123 | .with_date_more_than(datetime_from.timestamp_micros() as u64) 124 | .with_date_less_than(datetime_to.timestamp_micros() as u64) 125 | .with_priority_above_or_equal_to(query.priority) 126 | .build(); 127 | 128 | let logs = j.query_logs(&q)?; 129 | debug!("Found {} entries.", logs.rows.len()); 130 | 131 | Ok(logs) 132 | } 133 | 134 | #[tauri::command] 135 | #[instrument] 136 | pub(crate) async fn get_services() -> Result, JournalError> { 137 | debug!("Getting services..."); 138 | let services = Journal::list_services(); 139 | debug!("found {} services", services.len()); 140 | 141 | Ok(services) 142 | } 143 | 144 | #[tauri::command] 145 | #[instrument] 146 | pub(crate) async fn get_boots() -> Result, JournalError> { 147 | debug!("Getting boots..."); 148 | let boots = Journal::list_boots(); 149 | debug!("found {} boots", boots.len()); 150 | 151 | Ok(boots) 152 | } 153 | -------------------------------------------------------------------------------- /src-tauri/src/monitor_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::journal::JournalError; 2 | use crate::monitor::Monitor; 3 | use crate::monitor::ProcessStatus; 4 | use crate::monitor::SystemStatus; 5 | use serde::Deserialize; 6 | use tauri::async_runtime::Mutex; 7 | 8 | #[tauri::command] 9 | #[instrument] 10 | pub(crate) async fn get_system_status( 11 | monitor: tauri::State<'_, Mutex>, 12 | ) -> Result { 13 | debug!("Getting system status..."); 14 | let m = monitor.lock().await; 15 | 16 | match m.get_system_status() { 17 | Ok(ss) => { 18 | debug!("Got system status..."); 19 | Ok(ss) 20 | } 21 | Err(e) => { 22 | error!("{:?}", e); 23 | Err(JournalError::Internal(1)) 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct ProcessQuery { 31 | sort_by: String, 32 | sort_order: String, 33 | } 34 | 35 | #[tauri::command] 36 | #[instrument] 37 | pub(crate) async fn get_processes( 38 | query: ProcessQuery, 39 | monitor: tauri::State<'_, Mutex>, 40 | ) -> Result, JournalError> { 41 | debug!("Getting processes..."); 42 | let mut m = monitor.lock().await; 43 | 44 | match m.get_processes() { 45 | Some(p) => { 46 | debug!("Got {} processes", p.len()); 47 | let mut processes: Vec = p.clone().into_values().collect(); 48 | 49 | sort_processes(&mut processes, &query); 50 | Ok(processes[..30].to_vec()) 51 | } 52 | None => { 53 | debug!("No processes"); 54 | Err(JournalError::Internal(1)) 55 | } 56 | } 57 | } 58 | 59 | fn sort_processes(processes: &mut [ProcessStatus], query: &ProcessQuery) { 60 | match query.sort_by.as_str() { 61 | "pid" => { 62 | processes.sort_by(|a, b| { 63 | if query.sort_order.to_lowercase() == "desc" { 64 | b.pid.cmp(&a.pid) 65 | } else { 66 | a.pid.cmp(&b.pid) 67 | } 68 | }); 69 | } 70 | "cpu_usage_percentage" => { 71 | processes.sort_by(|a, b| { 72 | if query.sort_order.to_lowercase() == "desc" { 73 | b.cpu_usage_percentage.total_cmp(&a.cpu_usage_percentage) 74 | } else { 75 | a.cpu_usage_percentage.total_cmp(&b.cpu_usage_percentage) 76 | } 77 | }); 78 | } 79 | "rss_in_kb" => { 80 | processes.sort_by(|a, b| { 81 | if query.sort_order.to_lowercase() == "desc" { 82 | b.rss_in_kb.cmp(&a.rss_in_kb) 83 | } else { 84 | a.rss_in_kb.cmp(&b.rss_in_kb) 85 | } 86 | }); 87 | } 88 | "uss_in_kb" => { 89 | processes.sort_by(|a, b| { 90 | if query.sort_order.to_lowercase() == "desc" { 91 | b.uss_in_kb.cmp(&a.uss_in_kb) 92 | } else { 93 | a.uss_in_kb.cmp(&b.uss_in_kb) 94 | } 95 | }); 96 | } 97 | "pss_in_kb" => { 98 | processes.sort_by(|a, b| { 99 | if query.sort_order.to_lowercase() == "desc" { 100 | b.pss_in_kb.cmp(&a.pss_in_kb) 101 | } else { 102 | a.pss_in_kb.cmp(&b.pss_in_kb) 103 | } 104 | }); 105 | } 106 | "process_name" => { 107 | processes.sort_by(|a, b| { 108 | if query.sort_order.to_lowercase() == "desc" { 109 | b.process_name.cmp(&a.process_name) 110 | } else { 111 | a.process_name.cmp(&b.process_name) 112 | } 113 | }); 114 | } 115 | "time_userspace_miliseconds" => { 116 | processes.sort_by(|a, b| { 117 | if query.sort_order.to_lowercase() == "desc" { 118 | b.time_userspace_miliseconds 119 | .total_cmp(&a.time_userspace_miliseconds) 120 | } else { 121 | a.time_userspace_miliseconds 122 | .total_cmp(&b.time_userspace_miliseconds) 123 | } 124 | }); 125 | } 126 | "time_kernel_miliseconds" => { 127 | processes.sort_by(|a, b| { 128 | if query.sort_order.to_lowercase() == "desc" { 129 | b.time_kernel_miliseconds 130 | .total_cmp(&a.time_kernel_miliseconds) 131 | } else { 132 | a.time_kernel_miliseconds 133 | .total_cmp(&b.time_kernel_miliseconds) 134 | } 135 | }); 136 | } 137 | "cmd" => { 138 | processes.sort_by(|a, b| { 139 | if query.sort_order.to_lowercase() == "desc" { 140 | b.cmd.cmp(&a.cmd) 141 | } else { 142 | a.cmd.cmp(&b.cmd) 143 | } 144 | }); 145 | } 146 | "fds" => { 147 | processes.sort_by(|a, b| { 148 | if query.sort_order.to_lowercase() == "desc" { 149 | b.fds.cmp(&a.fds) 150 | } else { 151 | a.fds.cmp(&b.fds) 152 | } 153 | }); 154 | } 155 | default => { 156 | info!("Unknown sort_by field: {}", default); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/components/ProcessTable.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 194 | 195 | 200 | -------------------------------------------------------------------------------- /src/components/SummaryBar.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 127 | 128 | 212 | -------------------------------------------------------------------------------- /src/components/LogTable.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 146 | 147 | 250 | -------------------------------------------------------------------------------- /src-tauri/src/monitor/mod.rs: -------------------------------------------------------------------------------- 1 | mod cmdline; 2 | mod pid_stat; 3 | mod smaps_rollup; 4 | mod stat; 5 | mod uptime; 6 | // mod meminfo 7 | mod fd; 8 | mod process_status; 9 | mod system_status; 10 | 11 | use anyhow::Result; 12 | pub use process_status::ProcessStatus; 13 | use rayon::prelude::{IntoParallelIterator, ParallelIterator}; 14 | use std::{collections::HashMap, fs::read_dir, process::Command}; 15 | pub use system_status::SystemStatus; 16 | 17 | lazy_static! { 18 | static ref CLICKS: usize = get_clicks(); 19 | } 20 | 21 | fn get_clicks() -> usize { 22 | let clicks = Command::new("getconf").arg("CLK_TCK").output().unwrap(); 23 | let clicks = String::from_utf8(clicks.stdout).unwrap(); 24 | let clicks = clicks.trim(); 25 | 26 | clicks.parse::().unwrap() 27 | } 28 | 29 | #[derive(Default, Debug)] 30 | pub struct Monitor { 31 | procs_path: &'static str, 32 | last_processes: Option>, 33 | } 34 | 35 | impl Monitor { 36 | pub fn new() -> Monitor { 37 | Monitor { 38 | procs_path: "/proc", 39 | last_processes: None, 40 | } 41 | } 42 | 43 | pub fn get_system_status(&self) -> Result { 44 | let mut ss = SystemStatus::default(); 45 | 46 | uptime::read_file(self.procs_path, &mut ss)?; 47 | Ok(ss) 48 | } 49 | 50 | fn get_running_pids(&self) -> Result> { 51 | let pids: Vec = read_dir(self.procs_path)? 52 | .filter_map(|d| { 53 | match d { Err(e) => { 54 | debug!("couldn't read pid info {}", e); 55 | None 56 | } _ => { 57 | d.ok() 58 | }} 59 | }) 60 | .filter(|d| d.file_type().unwrap().is_dir()) 61 | .filter_map(|d| d.file_name().into_string().ok()) 62 | .filter_map(|d| d.parse::().ok()) 63 | .collect(); 64 | 65 | Ok(pids) 66 | } 67 | 68 | pub fn get_processes(&mut self) -> Option<&HashMap> { 69 | debug!("processes started"); 70 | 71 | let pids = self.get_running_pids().ok()?; 72 | debug!("processes found {} pids", pids.len()); 73 | 74 | let mut process_entries: HashMap = pids 75 | .into_par_iter() 76 | .filter_map(|pid| { 77 | let pe = self.create_process_entry(&pid); 78 | match pe { 79 | Ok(pe) => Some((pid, pe)), 80 | Err(e) => { 81 | debug!("couldn't get process info {} ", e); 82 | None 83 | } 84 | } 85 | }) 86 | .collect(); 87 | debug!("processes found {} process entries", process_entries.len()); 88 | 89 | process_entries.iter_mut().for_each(|(k, v)| { 90 | v.time_userspace_miliseconds = 91 | (v.time_userspace_clicks as f32 / *CLICKS as f32) * 1000f32; 92 | v.time_kernel_miliseconds = (v.time_kernel_clicks as f32 / *CLICKS as f32) * 1000f32; 93 | 94 | if let Some(last_processes) = &self.last_processes { 95 | match last_processes.get(k) { 96 | Some(last) => { 97 | let elapsed_miliseconds = 98 | (v.scrapped_timestamp - last.scrapped_timestamp).num_milliseconds(); 99 | let current_cpu_usage_miliseconds = 100 | v.time_userspace_miliseconds + v.time_kernel_miliseconds; 101 | let last_cpu_usage_miliseconds = 102 | last.time_userspace_miliseconds + last.time_kernel_miliseconds; 103 | let cpu_usage_miliseconds = 104 | current_cpu_usage_miliseconds - last_cpu_usage_miliseconds; 105 | v.cpu_usage_percentage = 106 | cpu_usage_miliseconds * 100f32 / elapsed_miliseconds as f32; 107 | } 108 | None => debug!("New process"), 109 | } 110 | } 111 | }); 112 | 113 | trace!("processes {:?}", &process_entries); 114 | 115 | // Not supported by borrow checker, so have to use get_or_insert to trick it 116 | // Ok(&self.last_processes.unwrap()) 117 | 118 | self.last_processes = Some(process_entries); 119 | 120 | match &self.last_processes { 121 | Some(lp) => { 122 | debug!("processes {:?}", lp.len()); 123 | Some(lp) 124 | } 125 | None => { 126 | debug!("processes none found"); 127 | None 128 | } 129 | } 130 | } 131 | 132 | fn create_process_entry(&self, pid: &usize) -> Result { 133 | let mut pe = ProcessStatus { 134 | pid: *pid, 135 | ..ProcessStatus::default() 136 | }; 137 | 138 | cmdline::read_file(self.procs_path, pid, &mut pe)?; 139 | pid_stat::read_file(self.procs_path, pid, &mut pe)?; 140 | smaps_rollup::read_file(self.procs_path, pid, &mut pe)?; 141 | fd::read_file(self.procs_path, pid, &mut pe)?; 142 | 143 | Ok(pe) 144 | } 145 | } 146 | 147 | #[cfg(test)] 148 | mod tests { 149 | use anyhow::Result; 150 | use chrono::Duration; 151 | 152 | use crate::monitor::{Monitor, ProcessStatus, CLICKS}; 153 | 154 | #[test] 155 | fn get_running_pids() -> Result<()> { 156 | let monitor = Monitor { 157 | procs_path: "./tests/fixtures/", 158 | last_processes: None, 159 | }; 160 | let pids = monitor.get_running_pids()?; 161 | assert!(!pids.is_empty()); 162 | 163 | Ok(()) 164 | } 165 | 166 | #[test] 167 | fn get_process_entries() -> Result<()> { 168 | let mut monitor = Monitor { 169 | procs_path: "./tests/fixtures/", 170 | last_processes: None, 171 | }; 172 | let processes = monitor.get_processes().unwrap(); 173 | assert!(!processes.is_empty()); 174 | 175 | Ok(()) 176 | } 177 | 178 | #[test] 179 | fn get_process_entries_with_cpu_percentage() -> Result<()> { 180 | let mut monitor = Monitor { 181 | procs_path: "./tests/fixtures/", 182 | last_processes: None, 183 | }; 184 | let processes = monitor.get_processes().unwrap(); 185 | unsafe { 186 | // Hack to modify state of existing process read 187 | let p1 = processes.get(&1).unwrap() as *const ProcessStatus; 188 | let p1 = p1 as *mut ProcessStatus; 189 | (*p1).scrapped_timestamp = chrono::Utc::now() - Duration::seconds(5); 190 | (*p1).time_userspace_miliseconds -= 4000f32; 191 | } 192 | 193 | let processes = monitor.get_processes().unwrap(); 194 | let p1 = processes.get(&1).unwrap(); 195 | assert!(p1.cpu_usage_percentage >= 80f32); 196 | 197 | Ok(()) 198 | } 199 | 200 | #[test] 201 | fn system_status() -> Result<()> { 202 | let monitor = Monitor { 203 | procs_path: "./tests/fixtures/", 204 | last_processes: None, 205 | }; 206 | let _ss = monitor.get_system_status()?; 207 | 208 | Ok(()) 209 | } 210 | 211 | #[test] 212 | fn clicks() { 213 | assert_eq!(*CLICKS, 100); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/components/FilterSidebar.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 200 | 201 | 227 | 258 | -------------------------------------------------------------------------------- /src-tauri/src/journal/libsdjournal.rs: -------------------------------------------------------------------------------- 1 | use super::libsdjournal_bindings; 2 | use libc::{c_char, c_void, size_t}; 3 | use serde::Serialize; 4 | use std::ffi::{CStr, CString}; 5 | use thiserror::Error; 6 | 7 | #[derive(Error, Debug, Serialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub enum JournalError { 10 | #[error("Internal error while invoking systemd. Error Code: {0}")] 11 | Internal(i32), 12 | #[error("Reached the end of the cursor")] 13 | EndOfFile, 14 | } 15 | 16 | pub fn sd_journal_open(sd_journal: &mut *mut c_void, flags: u32) -> Result<(), JournalError> { 17 | let ret: libc::c_int; 18 | 19 | unsafe { 20 | ret = libsdjournal_bindings::sd_journal_open(sd_journal, flags); 21 | } 22 | if ret != 0 { 23 | return Err(JournalError::Internal(ret)); 24 | } 25 | 26 | Ok(()) 27 | } 28 | 29 | pub fn sd_journal_close(sd_journal: *mut c_void) { 30 | unsafe { 31 | libsdjournal_bindings::sd_journal_close(sd_journal); 32 | } 33 | } 34 | 35 | pub fn sd_journal_next(sd_journal: *mut c_void) -> Result { 36 | let ret: libc::c_int; 37 | 38 | unsafe { 39 | ret = libsdjournal_bindings::sd_journal_next(sd_journal); 40 | } 41 | 42 | if ret < 0 { 43 | return Err(JournalError::Internal(ret)); 44 | } 45 | 46 | Ok(ret > 0) 47 | } 48 | 49 | pub fn sd_journal_previous(sd_journal: *mut c_void) -> Result { 50 | let ret: libc::c_int; 51 | 52 | unsafe { 53 | ret = libsdjournal_bindings::sd_journal_previous(sd_journal); 54 | } 55 | 56 | if ret < 0 { 57 | return Err(JournalError::Internal(ret)); 58 | } 59 | 60 | Ok(ret > 0) 61 | } 62 | 63 | pub fn sd_journal_next_skip(sd_journal: *mut c_void, skip: u64) -> Result { 64 | let ret: libc::c_int; 65 | 66 | unsafe { 67 | ret = libsdjournal_bindings::sd_journal_next_skip(sd_journal, skip); 68 | } 69 | 70 | if ret < 0 { 71 | return Err(JournalError::Internal(ret)); 72 | } 73 | 74 | Ok(ret > 0) 75 | } 76 | 77 | pub fn sd_journal_previous_skip(sd_journal: *mut c_void, skip: u64) -> Result { 78 | let ret: libc::c_int; 79 | 80 | unsafe { 81 | ret = libsdjournal_bindings::sd_journal_previous_skip(sd_journal, skip); 82 | } 83 | 84 | if ret < 0 { 85 | return Err(JournalError::Internal(ret)); 86 | } 87 | 88 | Ok(ret > 0) 89 | } 90 | 91 | pub fn sd_journal_get_data(sd_journal: *mut c_void, field: &str) -> Result { 92 | let mut data: *mut c_void = std::ptr::null_mut(); 93 | let mut length: size_t = 0; 94 | let c_field = CString::new(field).expect("CString failed"); 95 | let ret: libc::c_int; 96 | unsafe { 97 | ret = libsdjournal_bindings::sd_journal_get_data( 98 | sd_journal, 99 | c_field.as_ptr(), 100 | &mut data, 101 | &mut length, 102 | ); 103 | } 104 | 105 | if ret < 0 { 106 | return Err(JournalError::Internal(ret)); 107 | } 108 | 109 | let result = unsafe { 110 | match CStr::from_ptr(data as *mut c_char).to_str() { 111 | Ok(s) => { 112 | let s = String::from(s); 113 | let remove = format!("{}=", field); 114 | if let Some(value) = s.strip_prefix(&remove) { 115 | return Ok(value.to_string()); 116 | } 117 | Ok(s) 118 | } 119 | Err(_) => Err(-1), 120 | } 121 | }; 122 | 123 | if let Err(e) = result { 124 | return Err(JournalError::Internal(e)); 125 | } 126 | 127 | Ok(result.unwrap()) 128 | } 129 | 130 | pub fn sd_journal_add_match(sd_journal: *mut c_void, data: String) -> Result<(), JournalError> { 131 | let ret: libc::c_int; 132 | 133 | unsafe { 134 | let data = CString::new(data).expect("Could not set pid"); 135 | ret = libsdjournal_bindings::sd_journal_add_match(sd_journal, data.as_ptr(), 0usize); 136 | } 137 | 138 | if ret < 0 { 139 | return Err(JournalError::Internal(ret)); 140 | } 141 | 142 | Ok(()) 143 | } 144 | 145 | pub fn sd_journal_seek_head(sd_journal: *mut c_void) -> Result<(), JournalError> { 146 | let ret: libc::c_int; 147 | 148 | unsafe { 149 | ret = libsdjournal_bindings::sd_journal_seek_head(sd_journal); 150 | } 151 | if ret != 0 { 152 | return Err(JournalError::Internal(ret)); 153 | } 154 | 155 | Ok(()) 156 | } 157 | 158 | pub fn sd_journal_seek_tail(sd_journal: *mut c_void) -> Result<(), JournalError> { 159 | let ret: libc::c_int; 160 | 161 | unsafe { 162 | ret = libsdjournal_bindings::sd_journal_seek_tail(sd_journal); 163 | } 164 | if ret != 0 { 165 | return Err(JournalError::Internal(ret)); 166 | } 167 | 168 | Ok(()) 169 | } 170 | 171 | pub fn sd_journal_add_conjunction(sd_journal: *mut c_void) -> Result<(), JournalError> { 172 | let ret: libc::c_int; 173 | 174 | unsafe { 175 | ret = libsdjournal_bindings::sd_journal_add_conjunction(sd_journal); 176 | } 177 | 178 | if ret < 0 { 179 | return Err(JournalError::Internal(ret)); 180 | } 181 | 182 | Ok(()) 183 | } 184 | 185 | pub fn sd_journal_add_disjunction(sd_journal: *mut c_void) -> Result<(), JournalError> { 186 | let ret: libc::c_int; 187 | 188 | unsafe { 189 | ret = libsdjournal_bindings::sd_journal_add_disjunction(sd_journal); 190 | } 191 | 192 | if ret < 0 { 193 | return Err(JournalError::Internal(ret)); 194 | } 195 | 196 | Ok(()) 197 | } 198 | 199 | pub fn sd_journal_flush_matches(sd_journal: *mut c_void) { 200 | unsafe { 201 | libsdjournal_bindings::sd_journal_flush_matches(sd_journal); 202 | } 203 | } 204 | 205 | pub fn sd_journal_get_realtime_usec( 206 | sd_journal: *mut c_void, 207 | microseconds: &mut u64, 208 | ) -> Result<(), JournalError> { 209 | let ret: libc::c_int; 210 | 211 | unsafe { 212 | ret = libsdjournal_bindings::sd_journal_get_realtime_usec(sd_journal, microseconds); 213 | } 214 | 215 | if ret < 0 { 216 | return Err(JournalError::Internal(ret)); 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | pub fn sd_journal_seek_realtime_usec( 223 | sd_journal: *mut c_void, 224 | microseconds: u64, 225 | ) -> Result<(), JournalError> { 226 | let ret: libc::c_int; 227 | 228 | unsafe { 229 | ret = libsdjournal_bindings::sd_journal_seek_realtime_usec(sd_journal, microseconds); 230 | } 231 | 232 | if ret < 0 { 233 | return Err(JournalError::Internal(ret)); 234 | } 235 | 236 | Ok(()) 237 | } 238 | 239 | pub fn sd_journal_enumerate_data( 240 | sd_journal: *mut c_void, 241 | ) -> Result<(String, String), JournalError> { 242 | // TODO: Refactor so that it returns the whole record not field by field 243 | let mut data: *mut c_void = std::ptr::null_mut(); 244 | let mut length: size_t = 0; 245 | let ret: libc::c_int; 246 | 247 | unsafe { 248 | ret = libsdjournal_bindings::sd_journal_enumerate_data(sd_journal, &mut data, &mut length); 249 | } 250 | 251 | // Skip field in this situation 252 | if ret == -libc::E2BIG || ret == -libc::ENOBUFS || ret == -libc::EPROTONOSUPPORT { 253 | return Ok(("".to_owned(), "".to_owned())); 254 | } 255 | 256 | if ret < 0 { 257 | return Err(JournalError::Internal(ret)); 258 | } 259 | 260 | if ret == 0 { 261 | return Err(JournalError::EndOfFile); 262 | } 263 | 264 | let result = unsafe { 265 | match CStr::from_ptr(data as *mut c_char).to_str() { 266 | Ok(s) => { 267 | let s = String::from(s); 268 | let values = s.split_once('=').unwrap(); 269 | return Ok((values.0.to_owned(), values.1.to_owned())); 270 | } 271 | Err(_) => Err(-1), 272 | } 273 | }; 274 | 275 | if let Err(e) = result { 276 | return Err(JournalError::Internal(e)); 277 | } 278 | 279 | Ok(result.unwrap()) 280 | } 281 | 282 | // pub fn sd_journal_enumerate_available_data( 283 | // sd_journal: *mut c_void, 284 | // ) -> Result<(String, String), JournalError> { 285 | // // TODO: Refactor so that it returns the whole record not field by field 286 | // let mut data: *mut c_void = std::ptr::null_mut(); 287 | // let mut length: size_t = 0; 288 | // let ret: libc::c_int; 289 | 290 | // unsafe { 291 | // ret = libsdjournal_bindings::sd_journal_enumerate_available_data( 292 | // sd_journal, 293 | // &mut data, 294 | // &mut length, 295 | // ); 296 | // } 297 | 298 | // if ret < 0 { 299 | // return Err(JournalError::Internal(ret)); 300 | // } 301 | 302 | // if ret == 0 { 303 | // return Err(JournalError::EndOfFile); 304 | // } 305 | 306 | // let result = unsafe { 307 | // match CStr::from_ptr(data as *mut c_char).to_str() { 308 | // Ok(s) => { 309 | // let s = String::from(s); 310 | // let values = s.split_once('=').unwrap(); 311 | // return Ok((values.0.to_owned(), values.1.to_owned())); 312 | // } 313 | // Err(_) => Err(-1), 314 | // } 315 | // }; 316 | 317 | // if let Err(e) = result { 318 | // return Err(JournalError::Internal(e)); 319 | // } 320 | 321 | // Ok(result.unwrap()) 322 | // } 323 | -------------------------------------------------------------------------------- /src-tauri/src/journal/mod.rs: -------------------------------------------------------------------------------- 1 | mod boot; 2 | mod journal_entries; 3 | mod journal_fields; 4 | mod libsdjournal; 5 | mod libsdjournal_bindings; 6 | mod query; 7 | mod query_builder; 8 | mod unit; 9 | 10 | use bitflags::bitflags; 11 | pub use boot::Boot; 12 | pub use journal_entries::JournalEntries; 13 | pub use journal_entries::JournalEntry; 14 | use journal_fields::MESSAGE; 15 | use journal_fields::SOURCE_REALTIME_TIMESTAMP; 16 | use libc::c_void; 17 | pub use libsdjournal::JournalError; 18 | use libsdjournal::*; 19 | use query::Query; 20 | pub use query_builder::QueryBuilder; 21 | use std::process::Command; 22 | pub use unit::Unit; 23 | 24 | bitflags! { 25 | #[repr(C)] 26 | pub struct OpenFlags: u32 { 27 | /// Only files generated on the local machine 28 | const SD_JOURNAL_LOCAL_ONLY = 1 << 0; 29 | /// Only volatile journal files excluding persisted 30 | const SD_JOURNAL_RUNTIME_ONLY = 1 << 1; 31 | /// System services and the Kernel 32 | const SD_JOURNAL_SYSTEM = 1 << 2; 33 | /// Current user 34 | const SD_JOURNAL_CURRENT_USER = 1 << 3; 35 | } 36 | } 37 | 38 | pub const INIT_UNIT: &str = "Init (Systemd)"; 39 | 40 | #[derive(Debug)] 41 | pub struct Journal { 42 | ptr: *mut c_void, 43 | } 44 | 45 | impl Journal { 46 | fn new() -> Journal { 47 | Journal { 48 | ptr: std::ptr::null_mut(), 49 | } 50 | } 51 | 52 | pub fn open(open_flags: OpenFlags) -> Result { 53 | let mut journal = Journal::new(); 54 | sd_journal_open(&mut journal.ptr, open_flags.bits())?; 55 | 56 | Ok(journal) 57 | } 58 | 59 | pub fn get_logs(&self) -> Result { 60 | let q = QueryBuilder::default().build(); 61 | 62 | self.get_logs_internal(&q) 63 | } 64 | 65 | pub fn query_logs(&self, q: &Query) -> Result { 66 | self.get_logs_internal(q) 67 | } 68 | 69 | fn get_logs_internal(&self, q: &Query) -> Result { 70 | sd_journal_flush_matches(self.ptr); 71 | 72 | self.apply_pid_filter(q); 73 | self.apply_minimum_priority(q); 74 | self.apply_units(q); 75 | self.apply_slice(q); 76 | self.apply_boot_ids(q); 77 | self.apply_transports_filter(q); 78 | 79 | let mut journal_entries = JournalEntries::new(q.limit as usize); 80 | 81 | for field in q.fields.iter() { 82 | journal_entries.headers.push((*field).to_string()) 83 | } 84 | 85 | if q.reset_position { 86 | sd_journal_seek_tail(self.ptr)?; 87 | } 88 | 89 | if q.reset_position && q.date_less_than > 0 { 90 | sd_journal_seek_realtime_usec(self.ptr, q.date_less_than)?; 91 | } 92 | 93 | let mut count: u64 = 0; 94 | let mut last_timestamp: u64 = 0; 95 | 96 | loop { 97 | let more = sd_journal_previous(self.ptr)?; 98 | 99 | if !more { 100 | debug!("No more entries"); 101 | break; 102 | } 103 | 104 | if let Ok(updated_timestamp) = self.get_field(SOURCE_REALTIME_TIMESTAMP) { 105 | last_timestamp = updated_timestamp.parse().unwrap(); 106 | trace!( 107 | "Last timestamp {:?}", 108 | chrono::DateTime::from_timestamp_micros(last_timestamp.try_into().unwrap()) 109 | ) 110 | } 111 | 112 | if !q.quick_search.is_empty() { 113 | if let Ok(message) = self.get_field(MESSAGE) { 114 | if !message.to_lowercase().contains(&q.quick_search) { 115 | continue; 116 | } 117 | } 118 | } 119 | 120 | if q.limit > 0 && count >= q.limit { 121 | debug!("Reached limit of {}", q.limit); 122 | break; 123 | } 124 | 125 | if q.date_more_than > 0 && q.date_more_than >= last_timestamp { 126 | debug!("Reached epoch time of {}", q.date_more_than); 127 | break; 128 | } 129 | 130 | let mut row: Vec = Vec::with_capacity(q.fields.len()); 131 | 132 | for field in q.fields.iter() { 133 | match field.as_str() { 134 | "__REALTIME" => { 135 | let mut realtime: u64 = 0; 136 | match sd_journal_get_realtime_usec(self.ptr, &mut realtime) { 137 | Ok(()) => row.push(realtime.to_string()), 138 | Err(JournalError::Internal(e)) => { 139 | row.push(String::new()); 140 | warn!("Could not get realtime field, error: {}", e); 141 | } 142 | Err(JournalError::EndOfFile) => { 143 | panic!("should not return end of file") 144 | } 145 | } 146 | } 147 | _ => match self.get_field(field) { 148 | Ok(data) => { 149 | row.push(data); 150 | } 151 | Err(e) => { 152 | row.push(String::new()); 153 | warn!("Could not find the field: {}, JournalError: {}", &field, e); 154 | } 155 | }, 156 | } 157 | } 158 | 159 | journal_entries.rows.push(row); 160 | count += 1; 161 | } 162 | 163 | Ok(journal_entries) 164 | } 165 | 166 | pub fn get_full_entry(&self, timestamp: u64) -> Result { 167 | sd_journal_seek_realtime_usec(self.ptr, timestamp)?; 168 | 169 | let more = sd_journal_previous(self.ptr)?; 170 | 171 | if !more { 172 | error!("Entry not found by the timestamp"); 173 | return Err(JournalError::Internal(0)); 174 | } 175 | 176 | let mut entry = JournalEntry::new(); 177 | 178 | loop { 179 | match sd_journal_enumerate_data(self.ptr) { 180 | Ok(x) => { 181 | if !x.0.is_empty() { 182 | entry.headers.push(x.0); 183 | entry.values.push(x.1); 184 | } 185 | } 186 | Err(err) => match err { 187 | JournalError::EndOfFile => break, // If we reach the end just ignore 188 | _ => return Err(err), // Other type of error, return it 189 | }, 190 | } 191 | } 192 | 193 | Ok(entry) 194 | } 195 | 196 | fn get_field(&self, field: &str) -> Result { 197 | sd_journal_get_data(self.ptr, field) 198 | } 199 | 200 | fn apply_pid_filter(&self, q: &Query) { 201 | if q.pid > 0 { 202 | let query = format!("{}={}", journal_fields::PID, q.pid); 203 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 204 | warn!("Could not apply filter {}", e); 205 | } 206 | } 207 | } 208 | 209 | fn apply_transports_filter(&self, q: &Query) { 210 | if !q.transports.is_empty() { 211 | for transport in q.transports.iter() { 212 | let query = format!("{}={}", journal_fields::TRANSPORT, transport); 213 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 214 | warn!("Could not apply filter {}", e); 215 | } 216 | } 217 | } 218 | } 219 | 220 | fn apply_minimum_priority(&self, q: &Query) { 221 | for p in 0..=q.minimum_priority { 222 | let query = format!("{}={}", journal_fields::PRIORITY, p); 223 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 224 | warn!("Could not apply filter {}", e); 225 | } 226 | } 227 | } 228 | 229 | fn apply_units(&self, q: &Query) { 230 | if !q.units.is_empty() { 231 | for unit in q.units.iter() { 232 | let query = format!("{}={}", journal_fields::UNIT_FILTER, unit); 233 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 234 | warn!("Could not apply filter {}", e); 235 | } 236 | } 237 | } 238 | } 239 | 240 | fn apply_slice(&self, q: &Query) { 241 | if !q.slice.is_empty() { 242 | let query = format!("{}={}", journal_fields::SYSTEMD_SLICE, q.slice); 243 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 244 | warn!("Could not apply filter {}", e); 245 | } 246 | } 247 | } 248 | 249 | fn apply_boot_ids(&self, q: &Query) { 250 | if !q.boot_ids.is_empty() { 251 | for boot_id in q.boot_ids.iter() { 252 | let query = format!("{}={}", journal_fields::BOOT_ID, boot_id); 253 | if let Err(e) = sd_journal_add_match(self.ptr, query) { 254 | warn!("Could not apply filter {}", e); 255 | } 256 | } 257 | } 258 | } 259 | 260 | pub fn list_services() -> Vec { 261 | let output = Command::new("systemctl") 262 | .arg("list-unit-files") 263 | .arg("*.service") 264 | .arg("-o") 265 | .arg("json") 266 | .output() 267 | .expect("Failed to execute command"); 268 | 269 | let stdout = String::from_utf8(output.stdout).unwrap(); 270 | 271 | let mut units: Vec = serde_json::from_str(&stdout).unwrap(); 272 | units.insert( 273 | 0, 274 | Unit { 275 | unit_file: INIT_UNIT.into(), 276 | state: String::new(), 277 | preset: Option::None, 278 | }, 279 | ); 280 | 281 | units 282 | } 283 | 284 | pub fn list_boots() -> Vec { 285 | let output = Command::new("journalctl") 286 | .arg("--list-boots") 287 | .arg("-r") 288 | .arg("-o") 289 | .arg("json") 290 | .output() 291 | .expect("Failed to execute command"); 292 | 293 | let stdout = String::from_utf8(output.stdout).unwrap(); 294 | 295 | serde_json::from_str(&stdout).unwrap() 296 | } 297 | } 298 | 299 | impl Drop for Journal { 300 | fn drop(&mut self) { 301 | warn!("Dropping the journal"); 302 | sd_journal_close(self.ptr); 303 | } 304 | } 305 | 306 | unsafe impl Send for Journal {} 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and`show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------