mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-04 17:45:24 +00:00
v26.11.0 (#1756)
* fix: only show connection lost when the server check fails too * fix: load the macOS tray icon from the right unpacked folder (#1728) * fix: hide update and review buttons on disabled plugins (#1745) * fix: save the database without blocking the server, bump to 26.11.0 * fix: try the key before the password for hosts with both (Plugin-SSH-Terminal#50) * fix: add macOS Window menu item to reopen main window * feat: upload Mac App Store screenshots on submit * chore: drop internal note from changelog * chore: update translations and plugin pins * chore: lint, format, and bump version to 26.11.0
This commit is contained in:
1 parent
c759a147c6
commit
1d4eac3e20
24 files changed
+337
-56
No files matched your search
@@ -16,6 +16,8 @@ interface EncryptedFileMetadata {
|
||||
dataSize?: number;
|
||||
}
|
||||
|
||||
const ENCRYPT_CHUNK_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
class DatabaseFileEncryption {
|
||||
private static readonly VERSION = "v2";
|
||||
private static readonly ALGORITHM = "aes-256-gcm";
|
||||
@@ -27,7 +29,7 @@ class DatabaseFileEncryption {
|
||||
buffer: Buffer,
|
||||
targetPath: string,
|
||||
): Promise<string> {
|
||||
const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}`;
|
||||
const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
||||
const metadataPath = `${targetPath}${this.METADATA_FILE_SUFFIX}`;
|
||||
|
||||
try {
|
||||
@@ -38,7 +40,21 @@ class DatabaseFileEncryption {
|
||||
key,
|
||||
iv,
|
||||
) as crypto.CipherGCM;
|
||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||
// This runs on every save, so a big database must not hold up the event
|
||||
// loop for the whole encryption.
|
||||
const parts: Buffer[] = [];
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < buffer.length;
|
||||
offset += ENCRYPT_CHUNK_BYTES
|
||||
) {
|
||||
parts.push(
|
||||
cipher.update(buffer.subarray(offset, offset + ENCRYPT_CHUNK_BYTES)),
|
||||
);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
}
|
||||
parts.push(cipher.final());
|
||||
const encrypted = Buffer.concat(parts);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
const metadata: EncryptedFileMetadata = {
|
||||
@@ -62,8 +78,8 @@ class DatabaseFileEncryption {
|
||||
encrypted,
|
||||
]);
|
||||
|
||||
fs.writeFileSync(tmpPath, finalBuffer);
|
||||
fs.renameSync(tmpPath, targetPath);
|
||||
await fs.promises.writeFile(tmpPath, finalBuffer);
|
||||
await fs.promises.rename(tmpPath, targetPath);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(metadataPath)) {
|
||||
|
||||
@@ -6,6 +6,17 @@ const DEBOUNCE_MS = 2000;
|
||||
// Every save rewrites the whole database file, so background writes (metrics
|
||||
// samples, audit rows) may only cause one save per this window.
|
||||
const MIN_LAZY_SAVE_GAP_MS = 30_000;
|
||||
const MAX_LAZY_SAVE_GAP_MS = 5 * 60_000;
|
||||
// A database that takes long to save is saved less often by background writes.
|
||||
const LAZY_GAP_PER_SAVE_MS = 30;
|
||||
|
||||
/** How long background writes wait after a save that took saveMs. */
|
||||
export function lazySaveGapMs(saveMs: number): number {
|
||||
return Math.min(
|
||||
MAX_LAZY_SAVE_GAP_MS,
|
||||
Math.max(MIN_LAZY_SAVE_GAP_MS, saveMs * LAZY_GAP_PER_SAVE_MS),
|
||||
);
|
||||
}
|
||||
|
||||
export class DatabaseSaveTrigger {
|
||||
private static saveFunction: (() => Promise<void>) | null = null;
|
||||
@@ -14,6 +25,7 @@ export class DatabaseSaveTrigger {
|
||||
private static activeSave: Promise<void> | null = null;
|
||||
private static saveTimeout: NodeJS.Timeout | null = null;
|
||||
private static lastSaveAt = 0;
|
||||
private static lastSaveMs = 0;
|
||||
private static _dirty = false;
|
||||
private static batch = new AsyncLocalStorage<{
|
||||
open: boolean;
|
||||
@@ -56,7 +68,7 @@ export class DatabaseSaveTrigger {
|
||||
|
||||
const delay = Math.max(
|
||||
DEBOUNCE_MS,
|
||||
this.lastSaveAt + MIN_LAZY_SAVE_GAP_MS - Date.now(),
|
||||
this.lastSaveAt + lazySaveGapMs(this.lastSaveMs) - Date.now(),
|
||||
);
|
||||
|
||||
this.saveTimeout = setTimeout(async () => {
|
||||
@@ -64,7 +76,6 @@ export class DatabaseSaveTrigger {
|
||||
|
||||
try {
|
||||
await this.runSave();
|
||||
this._dirty = false;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Database save failed", error, {
|
||||
operation: "db_save_trigger_failed",
|
||||
@@ -122,7 +133,6 @@ export class DatabaseSaveTrigger {
|
||||
|
||||
try {
|
||||
await this.runSave();
|
||||
this._dirty = false;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Database force save failed", error, {
|
||||
operation: "db_save_trigger_force_failed",
|
||||
@@ -152,10 +162,17 @@ export class DatabaseSaveTrigger {
|
||||
this.activeSave = save;
|
||||
this.pendingSave = true;
|
||||
this.lastSaveAt = Date.now();
|
||||
// Cleared up front: saves are slow, and a write landing during one must
|
||||
// stay dirty for the next.
|
||||
this._dirty = false;
|
||||
|
||||
try {
|
||||
await save;
|
||||
} catch (error) {
|
||||
this._dirty = true;
|
||||
throw error;
|
||||
} finally {
|
||||
this.lastSaveMs = Date.now() - this.lastSaveAt;
|
||||
if (this.activeSave === save) {
|
||||
this.activeSave = null;
|
||||
this.pendingSave = false;
|
||||
@@ -181,6 +198,7 @@ export class DatabaseSaveTrigger {
|
||||
this.saveTimeout = null;
|
||||
}
|
||||
this.lastSaveAt = 0;
|
||||
this.lastSaveMs = 0;
|
||||
|
||||
this.pendingSave = false;
|
||||
this.activeSave = null;
|
||||
|
||||
Reference in new issue
Block a user