fix(privacy): the cleanup could never be applied, and three more from the audit
The cleanup preview marked itself stale before it could be drawn. The gate bumps a revision on release so a preview cannot survive a wipe; I then made the preview take the gate, so its own release invalidated it every single time and the apply button never appeared. The feature has been shipping non-functional since it was written, with a self-test that asserted the exact bump that killed it. Read-only operations no longer move the revision, and preview and maintenance have their own marks instead of borrowing Cleanup -- which also stops the five-second metadata refresh from expiring previews, and stops the UI announcing "another operation is running: cleanup" during a VACUUM. The JSON export produced invalid JSON. The chat relation kinds were interpolated straight into the output, and interpolating an enum writes its member name, so every message with a recognised relation came out as "source_kind":LocalPlayer. That is the file a GDPR access request goes out on. The self-test wrote a JSON file and never parsed it; it does now. Retention with the limit at zero still deleted. The slider is labelled "0 = never" and the sweep seeded 31 spec defaults unconditionally before reading the user's overrides, so zero still lost free company, linkshell and party history after ninety days -- and the short-circuit written for exactly this case could never be reached, because the map was never empty. A wipe that worked reported that it had failed. VACUUM needs the database to itself, the refilter walks a lazy reader on the primary connection outside the lock, and the two collide -- after the DELETE has committed. The delete paths no longer let that escape: the rows are gone, an uncompacted file is a housekeeping problem, and telling somebody their history is still there when it is not is a different kind of problem. Also: - CSV cells starting with =, +, - or @ get a leading apostrophe. The content is text other people typed into a chat channel and the file exists to be opened in a spreadsheet. - An export that matched nothing no longer replaces the previous one. It used to write its header, move it into place, and then report that nothing matched. Dalamud's save dialog offers no overwrite confirmation to fall back on, so this is the part that had to move. - The retention sweep says so when it loses the race for the gate, and routes its notifications through the teardown check like everything else.
This commit is contained in:
+42
-3
@@ -1064,9 +1064,23 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
return false;
|
||||
|
||||
// Snapshot the policy so the user can edit settings while the sweep runs.
|
||||
//
|
||||
// Seeded from the spec defaults only when the global limit is not "keep
|
||||
// forever". The slider is labelled "0 = never", and pre-filling 31
|
||||
// channels with 365- and 90-day windows made that label a lie: setting
|
||||
// it to zero still lost free company, linkshell and party history after
|
||||
// ninety days, and the short-circuit in DeleteByRetentionPolicy could
|
||||
// never be reached because the map was never empty.
|
||||
//
|
||||
// Explicit per-channel overrides still apply. Somebody who typed a
|
||||
// number for one channel meant that number.
|
||||
var policy = new Dictionary<int, int>();
|
||||
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
|
||||
policy[(int)(ushort)type] = days;
|
||||
if (Config.RetentionDefaultDays > 0)
|
||||
{
|
||||
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
|
||||
policy[(int)(ushort)type] = days;
|
||||
}
|
||||
|
||||
// This is the enumerator the wizard's Clear() cuts short. Reading under the
|
||||
// same lock the writers take keeps the policy snapshot whole.
|
||||
lock (ConfigMapsLock)
|
||||
@@ -1086,7 +1100,18 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
try
|
||||
{
|
||||
if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep))
|
||||
{
|
||||
// A run the user pressed a button for has to say something.
|
||||
// The pre-check in StartRetentionSweep only covers a gate
|
||||
// that was already busy; losing the race here is the same
|
||||
// outcome and used to be silent.
|
||||
if (notify)
|
||||
NotifySweep(
|
||||
Resources.HellionStrings.Retention_Error,
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1142,7 +1167,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
{
|
||||
Log.Error(e, "Retention sweep failed");
|
||||
if (notify)
|
||||
Util.WrapperUtil.AddNotification(
|
||||
NotifySweep(
|
||||
Resources.HellionStrings.Retention_Error,
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Error
|
||||
);
|
||||
@@ -1170,6 +1195,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
}
|
||||
|
||||
// The sweep is a background thread that can outlive an unload, same as the
|
||||
// settings-tab workers. A notification filed against a plugin that is gone
|
||||
// belongs to nobody.
|
||||
private void NotifySweep(
|
||||
string message,
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType type
|
||||
)
|
||||
{
|
||||
if (_isDisposing)
|
||||
return;
|
||||
|
||||
Util.WrapperUtil.AddNotification(message, type);
|
||||
}
|
||||
|
||||
// Read by the settings tab every frame so the manual button can say a run is
|
||||
// in progress. The gate itself cannot answer that: it goes busy only once
|
||||
// the worker reaches TryBegin, which is after Start returns.
|
||||
|
||||
Reference in New Issue
Block a user