release: v0.1.5 — test VPN, journal 15 j + verdicts par application, page À propos refondue, Makefile Féalu
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6086e8aeff
commit
790397da0e
15 files changed
+637
-180
No files matched your search
@@ -12,30 +12,49 @@ import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Journal de diagnostic : garde les [MAX_LINES] dernières lignes en mémoire et dans un fichier
|
||||
* (survit au redémarrage de l'app), consultable dans l'onglet « Journal ».
|
||||
* Journal de diagnostic : conservé [RETENTION_DAYS] jours (avec un plafond de [MAX_LINES] lignes),
|
||||
* en mémoire et dans un fichier qui survit au redémarrage de l'app. Consultable dans l'onglet « Journal ».
|
||||
*
|
||||
* Format du fichier : une ligne par entrée, `horodatage-ms<TAB>texte affiché`. Le fichier est complété
|
||||
* au fil de l'eau puis réécrit (purgé) de temps en temps, pour ne pas le réécrire à chaque ligne.
|
||||
*/
|
||||
object AppLog {
|
||||
private const val MAX_LINES = 400
|
||||
const val RETENTION_DAYS = 15
|
||||
private const val RETENTION_MS = RETENTION_DAYS * 24L * 3600 * 1000
|
||||
private const val MAX_LINES = 20_000
|
||||
private const val COMPACT_EVERY_PRUNED = 200
|
||||
private const val COMPACT_MIN_INTERVAL_MS = 3600_000L
|
||||
private const val FILE_NAME = "journal.txt"
|
||||
|
||||
private class Entry(val ts: Long, val text: String)
|
||||
|
||||
private val io = Executors.newSingleThreadExecutor()
|
||||
private val fmt = SimpleDateFormat("MM-dd HH:mm:ss", Locale.FRANCE)
|
||||
private val lines = ArrayDeque<String>()
|
||||
private val lines = ArrayDeque<Entry>()
|
||||
private val _entries = MutableStateFlow<List<String>>(emptyList())
|
||||
val entries: StateFlow<List<String>> = _entries.asStateFlow()
|
||||
|
||||
@Volatile private var file: File? = null
|
||||
private var prunedSinceCompact = 0
|
||||
private var lastCompactAt = 0L
|
||||
|
||||
fun init(context: Context) {
|
||||
if (file != null) return
|
||||
val f = File(context.filesDir, FILE_NAME)
|
||||
file = f
|
||||
synchronized(lines) {
|
||||
runCatching { if (f.exists()) lines.addAll(f.readLines().takeLast(MAX_LINES)) }
|
||||
_entries.value = lines.toList()
|
||||
runCatching {
|
||||
if (f.exists()) f.readLines().forEach { raw ->
|
||||
val tab = raw.indexOf('\t')
|
||||
val ts = if (tab > 0) raw.substring(0, tab).toLongOrNull() else null
|
||||
if (ts != null) lines.addLast(Entry(ts, raw.substring(tab + 1)))
|
||||
}
|
||||
}
|
||||
prune(System.currentTimeMillis())
|
||||
publish()
|
||||
compactLocked()
|
||||
}
|
||||
i("App", "Journal ouvert")
|
||||
i("App", "Journal ouvert (conservation : $RETENTION_DAYS jours)")
|
||||
}
|
||||
|
||||
fun i(tag: String, message: String) = add("I", tag, message)
|
||||
@@ -45,23 +64,51 @@ object AppLog {
|
||||
|
||||
private fun add(level: String, tag: String, message: String) {
|
||||
Log.println(when (level) { "E" -> Log.ERROR; "W" -> Log.WARN; else -> Log.INFO }, "DataControl/$tag", message)
|
||||
val line = "${fmt.format(Date())} $level $tag: $message"
|
||||
val now = System.currentTimeMillis()
|
||||
val entry = Entry(now, "${fmt.format(Date(now))} $level $tag: $message")
|
||||
synchronized(lines) {
|
||||
lines.addLast(line)
|
||||
while (lines.size > MAX_LINES) lines.removeFirst()
|
||||
_entries.value = lines.toList()
|
||||
val snapshot = lines.toList()
|
||||
io.execute { runCatching { file?.writeText(snapshot.joinToString("\n")) } }
|
||||
lines.addLast(entry)
|
||||
prune(now)
|
||||
publish()
|
||||
if (prunedSinceCompact >= COMPACT_EVERY_PRUNED && now - lastCompactAt > COMPACT_MIN_INTERVAL_MS) {
|
||||
compactLocked()
|
||||
} else {
|
||||
io.execute { runCatching { file?.appendText("${entry.ts}\t${entry.text}\n") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Retire les entrées plus vieilles que la rétention et au-delà du plafond. À appeler sous verrou. */
|
||||
private fun prune(now: Long) {
|
||||
while (lines.isNotEmpty() && (now - lines.first().ts > RETENTION_MS || lines.size > MAX_LINES)) {
|
||||
lines.removeFirst()
|
||||
prunedSinceCompact++
|
||||
}
|
||||
}
|
||||
|
||||
private fun publish() {
|
||||
_entries.value = lines.map { it.text }
|
||||
}
|
||||
|
||||
/** Réécrit le fichier avec les seules entrées conservées. À appeler sous verrou. */
|
||||
private fun compactLocked() {
|
||||
prunedSinceCompact = 0
|
||||
lastCompactAt = System.currentTimeMillis()
|
||||
val snapshot = lines.toList()
|
||||
io.execute {
|
||||
runCatching { file?.writeText(snapshot.joinToString("") { "${it.ts}\t${it.text}\n" }) }
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
synchronized(lines) {
|
||||
lines.clear()
|
||||
_entries.value = emptyList()
|
||||
io.execute { runCatching { file?.writeText("") } }
|
||||
publish()
|
||||
compactLocked()
|
||||
}
|
||||
}
|
||||
|
||||
fun asText(): String = synchronized(lines) { lines.joinToString("\n") }
|
||||
/** Texte à copier/partager : les [maxLines] dernières lignes (le presse-papiers et le partage ont une taille limite). */
|
||||
fun asText(maxLines: Int = 3000): String =
|
||||
synchronized(lines) { lines.takeLast(maxLines).joinToString("\n") { it.text } }
|
||||
}
|
||||
Reference in new issue
Block a user