Make the ChatTwo→HellionChat migration loud about locked files
A tester migrating from upstream Chat 2 ended up with a zero-row database in the new layout: Chat 2 was still loaded when Hellion Chat first started, the SQLite handle kept chat-sqlite.db locked, and File.Move silently fell into the catch-all without telling the user anything. Anyone hitting this would think Hellion Chat lost their history when it just hadn't been allowed to take it. Wrap each move on its own so a single locked file no longer abandons the rest of the migration: the JSON config, font cache and EmoteCacheV1 directory still travel even if the database itself is held open. When any IOException fires during the moves, flag a sticky 30-second warning notification on plugin start that tells the user exactly what's going on — disable Chat 2, fully close the game, restart — and points at the README troubleshooting section. The README now spells out the migration order step by step in two sections (fresh install vs. coming from Chat 2) and includes the manual mv/Move-Item one-liner for both Linux and Windows so users can recover without waiting for the next plugin update.
This commit is contained in:
+77
-30
@@ -272,61 +272,108 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
private static void MigrateFromChatTwoLayout()
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is null)
|
||||
return;
|
||||
|
||||
var legacyConfigFile = Path.Combine(pluginConfigsDir, "ChatTwo.json");
|
||||
var legacyConfigDir = Path.Combine(pluginConfigsDir, "ChatTwo");
|
||||
var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json");
|
||||
var ourConfigDir = Interface.ConfigDirectory.FullName;
|
||||
|
||||
// Track whether anything legitimately blocked us. The most common
|
||||
// cause is upstream Chat 2 still being loaded — its SQLite handle
|
||||
// keeps chat-sqlite.db locked and File.Move throws IOException.
|
||||
var lockedBlocker = false;
|
||||
|
||||
try
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is null)
|
||||
return;
|
||||
|
||||
var legacyConfigFile = Path.Combine(pluginConfigsDir, "ChatTwo.json");
|
||||
var legacyConfigDir = Path.Combine(pluginConfigsDir, "ChatTwo");
|
||||
var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json");
|
||||
var ourConfigDir = Interface.ConfigDirectory.FullName;
|
||||
|
||||
if (!File.Exists(ourConfigFile) && File.Exists(legacyConfigFile))
|
||||
{
|
||||
File.Move(legacyConfigFile, ourConfigFile);
|
||||
Log.Information($"HellionChat: migrated config file {legacyConfigFile} → {ourConfigFile}");
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: config file move blocked, leaving {legacyConfigFile} in place");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
|
||||
// The plugin's ConfigDirectory may already exist on first load
|
||||
// (Dalamud creates it), so check at the file level instead of
|
||||
// skipping when the directory is present. Move every legacy
|
||||
// entry whose target name is not occupied yet, then remove the
|
||||
// source dir if it ends up empty.
|
||||
if (Directory.Exists(legacyConfigDir))
|
||||
// The plugin's ConfigDirectory may already exist on first load
|
||||
// (Dalamud creates it), so check at the file level instead of
|
||||
// skipping when the directory is present. Move every legacy
|
||||
// entry whose target name is not occupied yet, then remove the
|
||||
// source dir if it ends up empty. Each move is wrapped on its
|
||||
// own so a single locked file (the SQLite db while ChatTwo still
|
||||
// runs) does not abandon the rest of the migration.
|
||||
if (!Directory.Exists(legacyConfigDir))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ourConfigDir);
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(legacyConfigDir))
|
||||
{
|
||||
Directory.CreateDirectory(ourConfigDir);
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(legacyConfigDir))
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(file));
|
||||
if (File.Exists(target))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(file));
|
||||
if (File.Exists(target))
|
||||
continue;
|
||||
File.Move(file, target);
|
||||
Log.Information($"HellionChat: migrated file {file} → {target}");
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.EnumerateDirectories(legacyConfigDir))
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: file move blocked for {file}, will retry on next load");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.EnumerateDirectories(legacyConfigDir))
|
||||
{
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(dir));
|
||||
if (Directory.Exists(target))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(dir));
|
||||
if (Directory.Exists(target))
|
||||
continue;
|
||||
Directory.Move(dir, target);
|
||||
Log.Information($"HellionChat: migrated subdir {dir} → {target}");
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(legacyConfigDir).Any())
|
||||
catch (IOException e)
|
||||
{
|
||||
Directory.Delete(legacyConfigDir);
|
||||
Log.Information($"HellionChat: removed empty legacy dir {legacyConfigDir}");
|
||||
Log.Warning(e, $"HellionChat: subdir move blocked for {dir}, will retry on next load");
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(legacyConfigDir).Any())
|
||||
{
|
||||
Directory.Delete(legacyConfigDir);
|
||||
Log.Information($"HellionChat: removed empty legacy dir {legacyConfigDir}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "HellionChat: layout migration failed, continuing with whatever exists");
|
||||
}
|
||||
|
||||
if (lockedBlocker)
|
||||
{
|
||||
// Surface the most common cause to the user as a notification
|
||||
// so they don't think Hellion Chat lost their history when in
|
||||
// fact upstream Chat 2 was still holding the database file.
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content = "Could not migrate the Chat 2 database — the file appears to be in use. " +
|
||||
"Disable Chat 2, fully close the game, then start it again. " +
|
||||
"See the README troubleshooting section if the issue persists.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(30),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenMainUi()
|
||||
|
||||
@@ -43,7 +43,9 @@ custom-repo / dev-plugin while the architecture stabilises.
|
||||
## Install (testers)
|
||||
|
||||
Hellion Chat is shipped via a Dalamud **custom repository** during the
|
||||
bootstrap phase. To install:
|
||||
bootstrap phase.
|
||||
|
||||
### If you have never used Chat 2
|
||||
|
||||
1. Open Dalamud settings (`/xlsettings`) → **Experimental**.
|
||||
2. Add a new entry under **Custom Plugin Repositories**:
|
||||
@@ -51,13 +53,59 @@ bootstrap phase. To install:
|
||||
https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json
|
||||
```
|
||||
3. Click **Save**, then back in `/xlplugins` hit **All Plugins** and refresh.
|
||||
4. **Hellion Chat** now appears in the list — install it from there.
|
||||
5. If you previously had **Chat 2** installed, disable it first. The two
|
||||
share their database and config dir until Hellion Chat's first launch
|
||||
migrates everything into `pluginConfigs/HellionChat/`.
|
||||
4. **Hellion Chat** now appears in the list — install it.
|
||||
|
||||
### If you are migrating from Chat 2 (and want to keep your history)
|
||||
|
||||
The two plugins share `pluginConfigs/ChatTwo/` (database) and
|
||||
`pluginConfigs/ChatTwo.json` (settings). Hellion Chat moves both into
|
||||
`pluginConfigs/HellionChat/` on first start, but only if the upstream
|
||||
plugin isn't holding the database file open. Order matters:
|
||||
|
||||
1. **Disable Chat 2** in `/xlplugins` (don't uninstall, just disable).
|
||||
2. **Close FFXIV completely** so SQLite releases its file lock — a plain
|
||||
plugin reload is not enough.
|
||||
3. Re-launch the game.
|
||||
4. Add the custom repo URL as in the previous section.
|
||||
5. Install Hellion Chat. On its first start it migrates the Chat 2 config
|
||||
file and the entire database directory into the HellionChat layout
|
||||
without losing data.
|
||||
6. Verify in **Settings → Privacy → Apply filter to existing database →
|
||||
Refresh preview** that the message count is what you expect (millions
|
||||
of rows if you used Chat 2 for a while).
|
||||
|
||||
If the message count comes back as zero, the migration was blocked
|
||||
(usually because Chat 2 was still active or the previous game session
|
||||
hadn't fully closed). See the troubleshooting section below.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Hellion Chat shows zero messages but I had Chat 2 history:**
|
||||
The migration either didn't run or hit a locked file. Close the game,
|
||||
then move the data manually:
|
||||
|
||||
Linux / XIVLauncher Core:
|
||||
```bash
|
||||
mv ~/.xlcore/pluginConfigs/ChatTwo/chat-sqlite.db \
|
||||
~/.xlcore/pluginConfigs/HellionChat/chat-sqlite.db
|
||||
[ -d ~/.xlcore/pluginConfigs/ChatTwo/EmoteCacheV1 ] && \
|
||||
mv ~/.xlcore/pluginConfigs/ChatTwo/EmoteCacheV1 \
|
||||
~/.xlcore/pluginConfigs/HellionChat/
|
||||
```
|
||||
|
||||
Windows / standard XIVLauncher:
|
||||
```powershell
|
||||
Move-Item "$env:AppData\XIVLauncher\pluginConfigs\ChatTwo\chat-sqlite.db" `
|
||||
"$env:AppData\XIVLauncher\pluginConfigs\HellionChat\chat-sqlite.db" -Force
|
||||
```
|
||||
|
||||
Then start the game and Hellion Chat — your full history is back.
|
||||
|
||||
### Updates
|
||||
|
||||
Updates land in the same plugin list once the maintainer pushes a new
|
||||
`v0.1.x` tag.
|
||||
`v0.1.x` tag and re-publishes the GitHub release. No re-installation
|
||||
needed.
|
||||
|
||||
## Why a fork
|
||||
|
||||
|
||||
Reference in New Issue
Block a user