Linux 原生支持(Photino + 跨平台音视频/CLI/打包)#69
Conversation
…dio on Linux Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… self-signed cert
…setStudio runtime dep)
…shell/appshell) Introduce IDesktopDialogService, ITaskbarProgress, IShellService and IAppShell under Platform/, with Windows (#if WINDOWS) WinForms-backed impls and Linux headless impls. Register them in ServerManager.StartApp via #if WINDOWS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f WinForms - csproj: <Compile Remove> AudioConvertToolController.cs on Linux (cluster D) - MusicTransferController: inject IDesktopDialogService/ITaskbarProgress; replace FolderBrowserDialog + Vanara ShellProgressDialog with abstractions; wrap audio (AudioConvert/Audio/CriUtils) blocks in #if WINDOWS - AssetDir/ImageToAb/VideoConvertTool controllers: use IDesktopDialogService (PickFolder/PickFile/Confirm) instead of WinForms dialogs/MessageBox - StaticSettings: throw InvalidOperationException instead of MessageBox+Application.Exit; use Environment.ProcessPath for exeDir - VideoConvert: wrap WinUtils taskbar calls in #if WINDOWS Remaining LinuxDebug errors are only cluster C (shell nav) + E (IAP), deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d builds and runs on Linux
…eadless on Linux); restore batch-export progress+cancel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GamePath 默认空串而非 null,AssetsDirs 在 StreamingAssets 不存在时返回空,避免 Path.Combine/枚举抛异常 - LinuxProgram 未配置游戏目录时加载 #/oobe 引导页(对齐 Windows AppMain 逻辑) - PhotinoAppShell 替换 HeadlessAppShell:用 PhotinoWindowHolder 导航单窗口,使 OOBE 完成后能切到主界面
- 新增 PlatformFile.cs(上一提交迁移了调用点但漏提交了类本身,导致分支无法编译) - StaticSettings 扫描用 ResolveSubDir 大小写不敏感解析子目录,去掉 Linux 区分大小写的 glob - PathUtils.ResolveIgnoreCase 逐段大小写不敏感解析路径 - VersionXml/GenreXml 的 FilePath 用 ResolveIgnoreCase,删除改用 PlatformFile
…ost 走后端原生对话框,消除不支持浏览器警告 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tGTK 下导出 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry 适配器,支持 WebKitGTK Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g.exe 失败;并在 Linux 启动时检测硬件加速 (本提交同时包含此前未提交的窗口尺寸 1600x800 改动)
…oryPicker/webkitdirectory) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LinuxProgram 注册 WebMessage handler,收到 open-window 消息时开一个 PhotinoWindow 子窗口加载预览页 - 前端 PreviewChartButton 在 Photino(isPhotino) 下走 window.external.sendMessage 而非 window.open - 附带新增 ShellController.OpenExternalUrl(系统浏览器打开 http/https,备外部链接用)
…r(native 抛无栈 SyntaxError)
…),MediaFoundation 换 ffmpeg - XV2-Tools submodule 切到 MuNET-OSS fork(含跨平台 AcbCore:ACB+VGAudio) - SonicAudioTools submodule 用 netstandard2.0 的 SonicAudioLib - 主项目两平台统一引 AcbCore + SonicAudioLib,弃用 net47 的 LB_Common/Xv2CoreLib - Audio.cs 的 MediaFoundation 解码改用系统 ffmpeg - 取消 Audio/CriUtils/AudioConvert + ImportChart/CueConvert/VrcProcess/AudioConvertTool 等的 Linux 排除,恢复音频 #if WINDOWS 守卫 - AudioConvertTool 改用 IDesktopDialogService.PickFile(跨平台) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…传给 ffmpeg 导致的 'Couldn't initialize muxer' - ConvertToWavViaFfmpeg / ConvertMp4AudioToWavViaFfmpeg 不再走 Xabe,直接 Process.Start - ArgumentList 每个参数独立传递,跨平台正确处理带空格路径、无引号问题 - FfmpegExePath: Windows 用内置 ffmpeg.exe,Linux 从 PATH 解析系统 ffmpeg Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Xabe.FFmpeg 在 Linux 上会把路径字面加引号传给 ffmpeg(issue #356), 导致 "Couldn't initialize muxer"。改用 FFMpegCore(参数数组,无引号问题), 迁移前后保持 ffmpeg 命令行参数等价。 - 用 GlobalFFOptions.Configure 替换 FFmpeg.SetExecutablesPath (Windows 指向内置 ffmpeg.exe,Linux 用 PATH 解析的系统 ffmpeg 目录) - VideoConvert/AudioConvert/Audio 全部改用 FFMpegArguments; GetMediaInfo→FFProbe,OnProgress→NotifyOnProgress,硬件加速检测照原逻辑 - 删除只服务于 Xabe 的 FFmpegHelper.Escape - 修复 Linux 视频转换回归:-hwaccel dxva2 是 Windows 专有 DXVA 解码, Linux 上会让 ffmpeg 直接报错中断(No device available for decoder), 故改为仅 #if WINDOWS 启用,Linux 走软件解码 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) UsmCreator 自带的 ffprobe/ffmpeg 解析器硬编码 .exe 路径并用 File.Exists 命中,在 Linux 上会命中随 Windows 构建拷进输出目录的 ffprobe.exe(PE 文件 无执行权限)→ 启动报 EACCES(权限不够)。改为复用 FFMpegCore 的 GlobalFFOptions.GetFF*BinaryPath(单一来源,按 OS 自动补后缀、尊重 BinaryFolder)。 2) FFMpegCore 的 NotifyOnProgress 只保留最后一次注册的回调(不像 Xabe 事件可叠加)。 AttachProgress 先注册前端进度、再注册任务栏进度,后者把前者覆盖;而任务栏回调在 Linux 是 #if WINDOWS 空实现,导致前端进度恒为 0。合并为单个回调解决(Windows 同样受益)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
这些 PE 文件在 Linux 上无用,且会被 File.Exists 命中导致误 exec(EACCES)。 给 <None Update="FFMpeg\**"> 加 IsLinuxBuild 条件,仅 Windows 复制。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CheckHardwareAcceleration 改为调用 VideoEncoderProbe(带 device 初始化的完整探测) - 软件专属参数 -cpu-used/-pix_fmt 只对软件编码器发出,不再泄漏给硬件编码器 - 硬件 profile 注入 -vaapi_device + format=nv12,hwupload(含 concat 的 filter_complex) - H264Encoder 改为暴露选中 profile 名,保持前端 API 契约 - DEBUG 下 dump [ffargs] 便于核对生成的 ffmpeg 参数 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AB 包/texture 处理已全面改用 AssetsTools.NET(.Texture) 跨平台路径 (ImageConvert 用 TextureFile.DecodeManagedData 纯托管解码,AssetBundleCreator 用 AssetsTools.NET),全仓库零处 using AssetStudio。Windows 原本通过 Libs 引用 AssetStudio/AssetStudioUtility 与复制 Texture2DDecoderNative.dll 已是死依赖。 - 删 AssetStudio/AssetStudioUtility 引用与对应 Libs/x64 二进制 - 删 Texture2DDecoderNative.dll 复制 - Mono.Cecil 两平台统一走 NuGet 0.11.6(原 Windows 用 Libs\Mono.Cecil.dll) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
这些都已无任何 csproj/代码引用:WinBlur 早已不用,K4os/Mono.Cecil.* 是 AssetStudio 与旧 Cecil 的捆绑依赖,现 Mono.Cecil 走 NuGet。一并清掉 csproj 里 已失效的 Libs\** Remove 指令。Libs 目录清空。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Linux 发布改为框架依赖(SelfContained=false + 关 ReadyToRun),依赖系统
aspnet-runtime,不打包运行时;保留 RID 以带上 Photino.Native.so 原生库。
- 关掉 StaticWebAssetsEnabled:发布用 UseFileServer 显式伺服 wwwroot,
不需要静态资产清单(也避免其 endpoints.json 干扰)。
- 把 Locale.zh-hans/zh-hant.resx 重命名为规范大小写 zh-Hans/zh-Hant:
原小写在大小写敏感的 Linux 上有两个问题——(1) 运行时 CultureInfo("zh-Hans")
探测规范大小写目录,找不到小写卫星目录,中文后端本地化失效;(2) 发布时与
System.CommandLine 的规范大小写卫星目录冲突导致 MSBuild 复制失败、publish 中断。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
从本地含 submodule 的工作树构建:前端 vite build → wwwroot,后端框架依赖 publish 到 /opt/maichartmanager,附 /usr/bin 启动器与 .desktop。 运行时依赖 aspnet-runtime + webkit2gtk-4.1 + ffmpeg;图标复用 Windows appx 的 Square 256x256 装进 hicolor 主题。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
之前 Linux 上 AppVersionController 返回的版本来自空的 AssemblyVersion,显示不对。 - AssemblyInfo.cs 的版本特性原本全在 #if WINDOWS 内,Linux 不发;改为两平台都发, 统一从 AppMain.Version 取(与 Windows 一致)。 - Linux 构建不再排除 AppMain.g.cs(其 Version 常量供 AssemblyInfo 使用)。 - AppVersionController(Linux) 改为读 AssemblyInformationalVersion,与 Windows 的 Application.ProductVersion 语义一致。 - PKGBUILD 像 Build.ps1 一样从 git describe --tags 重写 AppMain.g.cs 的 Version (Release 取 tag 基号,如 26.3)。 另:PKGBUILD 前端改用 pnpm(workspace + @munet/ui submodule),图标用 appx 的 Square 256x256。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MuConvert: - 之前随主项目发布产出 Windows PE 的 MuConvert.exe——根因是 Directory.Build.props 无条件给 MuConvert 设 win-x64+SelfContained(原为 Linux 上跑 Wine 测 Windows 版)。 改为仅 Windows 构建时才强制(保留 Windows 打包进自包含 exe 的需求),Linux 原生 按 linux-x64 框架依赖构建出 ELF。去掉无用的 EnableWindowsTargeting。 - 子模块修 Locale.zh-hant.resx 大小写(同主项目的 Linux 卫星目录冲突/本地化问题)。 MaiChartManager.CLI: - csproj 跨平台化(IsLinuxBuild:Windows=net*-windows 自包含 win-x64,Linux=net10.0 框架依赖 linux-x64),Program.cs/DebugCommand/AssemblyInfo 加 #if WINDOWS 守卫 (AppMain/IapManager/Program.Main/WebView2 STA 仅 Windows;Linux 走 LinuxProgram)。 - LinuxProgram.InitConfiguration/ConfigureFfmpeg 公开供 CLI 复用。 打包:PKGBUILD 改为发布 CLI 项目(超集,含 GUI/CLI/MuConvert/wwwroot), 装三个命令 maichartmanager(GUI) / mcm(CLI) / muconvert。 启动性能:重开 PublishReadyToRun(之前因卫星 crossgen 报错关掉,那其实是 zh-Hant 大小写冲突,已修)。CLI 冷启动从全量 JIT 降到 ~0.15s,GUI 启动同样受益。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
上一提交因 .gitignore 忽略而漏加,导致文件被误删。-f 强制加回。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebApplication.CreateBuilder 默认用 cwd 当 ContentRoot,从用户 HOME 启动时 host 会对 HOME 海量文件做文件监视/扫描,CreateBuilder 卡 25 秒+。显式指向 exeDir 解决(dotnet run 从项目目录启动所以不慢,掩盖了这个问题)。 wwwroot 伺服走独立 PhysicalFileProvider,不受影响。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @clansty, your pull request is larger than the review limit of 150000 diff characters
There was a problem hiding this comment.
Code Review
This pull request adds native Linux support to MaiChartManager by introducing cross-platform abstractions for UI shells, dialogs, and file operations, migrating the media conversion engine from Xabe.FFmpeg to FFMpegCore, and replacing AssetStudio with AssetsTools.NET. The code review identified several critical issues, including an argument injection vulnerability in the Linux shell service, potential resource leaks from undisposed ManualResetEventSlim and MemoryStream instances, and temporary file leaks during failed FFmpeg conversions. Additionally, feedback was provided to address potential infinite recursion when copying directories, fragile path segment matching, unhandled permission exceptions during directory enumeration, and premature cancellation triggers in the file picker.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private void XdgOpen(string arg) | ||
| { | ||
| try | ||
| { | ||
| Process.Start(new ProcessStartInfo("xdg-open", $"\"{arg}\"") { UseShellExecute = false }); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| logger.LogWarning(e, "Failed to xdg-open {Arg}", arg); | ||
| } | ||
| } |
There was a problem hiding this comment.
在 UseShellExecute = false 时,手动使用双引号包裹参数 $"\"{arg}\"" 容易导致参数注入(Argument Injection)漏洞,并且如果 URL 或路径本身包含双引号,也会导致解析失败。建议使用 ProcessStartInfo 的 ArgumentList 属性来安全地传递参数,这样 .NET 会自动处理所有转义和安全隔离。
private void XdgOpen(string arg)
{
try
{
Process.Start(new ProcessStartInfo("xdg-open")
{
ArgumentList = { arg },
UseShellExecute = false
});
}
catch (Exception e)
{
logger.LogWarning(e, "Failed to xdg-open {Arg}", arg);
}
}References
- Before flagging a shell injection vulnerability, confirm that a shell is actually being used to execute the command. If UseShellExecute is false, the command is not vulnerable to shell injection, although argument injection might still be possible if parameters are not escaped correctly.
| private static string ConvertMp4AudioToWavViaFfmpeg(string mp4Path) | ||
| { | ||
| Directory.CreateDirectory(StaticSettings.tempPath); | ||
| var outputPath = Path.Combine(StaticSettings.tempPath, $"ExtractMp4Audio_{Guid.NewGuid():N}.wav"); | ||
|
|
||
| // 用 FFMpegCore 把 mp4(或其它视频容器)里的音频流解码成 16bit PCM wav。 | ||
| // 等价命令行:ffmpeg -y -i <mp4> -vn -c:a pcm_s16le <output> | ||
| FFMpegArguments | ||
| .FromFileInput(mp4Path, verifyExists: false) | ||
| .OutputToFile(outputPath, overwrite: true, o => o.WithCustomArgument("-vn -c:a pcm_s16le")) | ||
| .ProcessSynchronously(); | ||
|
|
||
| if (!File.Exists(outputPath) || new FileInfo(outputPath).Length == 0) | ||
| { | ||
| File.Delete(outputPath); | ||
| throw new InvalidOperationException("ffmpeg produced empty wav file from mp4 input."); | ||
| } | ||
|
|
||
| return outputPath; | ||
| } |
There was a problem hiding this comment.
如果 FFMpegArguments...ProcessSynchronously() 执行失败抛出异常,临时文件 outputPath 将不会被删除,从而导致临时文件泄露。建议使用 try-catch 块包裹执行过程,在捕获到异常时确保清理已创建的临时文件。
private static string ConvertMp4AudioToWavViaFfmpeg(string mp4Path)
{
Directory.CreateDirectory(StaticSettings.tempPath);
var outputPath = Path.Combine(StaticSettings.tempPath, $"ExtractMp4Audio_{Guid.NewGuid():N}.wav");
try
{
// 用 FFMpegCore 把 mp4(或其它视频容器)里的音频流解码成 16bit PCM wav。
// 等价命令行:ffmpeg -y -i <mp4> -vn -c:a pcm_s16le <output>
FFMpegArguments
.FromFileInput(mp4Path, verifyExists: false)
.OutputToFile(outputPath, overwrite: true, o => o.WithCustomArgument("-vn -c:a pcm_s16le"))
.ProcessSynchronously();
if (!File.Exists(outputPath) || new FileInfo(outputPath).Length == 0)
{
throw new InvalidOperationException("ffmpeg produced empty wav file from mp4 input.");
}
return outputPath;
}
catch
{
if (File.Exists(outputPath))
{
try { File.Delete(outputPath); } catch { }
}
throw;
}
}References
- Avoid deleting shared cached files immediately after use, especially in concurrent environments. Instead, implement a global cache cleanup strategy or use non-cached temporary files to prevent race conditions and data corruption.
|
|
||
| // 启动进程内 Kestrel:loopback + 伺服 SPA(wwwroot)+ API 同源,但不开 LAN 端口。 | ||
| // Kestrel 在后台线程运行(StartApp 内部 Task.Run),主线程留给 Photino 开窗。 | ||
| var serverReady = new ManualResetEventSlim(false); |
| } | ||
|
|
||
| string[]? result = null; | ||
| var done = new ManualResetEventSlim(); |
| public static bool ContainsSegment(string? path, string segment) | ||
| => path is not null && | ||
| path.Replace('\\', '/').Contains($"/{segment}/", System.StringComparison.InvariantCultureIgnoreCase); |
There was a problem hiding this comment.
当前的 ContainsSegment 实现使用 Replace('\\', '/').Contains($"/{segment}/", ...),这在处理相对路径(如 A000\\foo.png,没有前导斜杠)或路径以该段结尾(如 C:\\foo\\A000,没有后续斜杠)时会匹配失败。建议使用更健壮的路径分割和匹配逻辑。
public static bool ContainsSegment(string? path, string segment)
{
if (path is null) return false;
var parts = path.Split(new[] { '/', '\\' }, System.StringSplitOptions.RemoveEmptyEntries);
return parts.Any(p => string.Equals(p, segment, System.StringComparison.OrdinalIgnoreCase));
}| let settled = false; | ||
| input.addEventListener('change', () => { settled = true; resolve(input.files?.[0] ?? null); input.remove(); }); | ||
| // 取消时多数内核不触发 change;用 window focus 兜底判定取消 | ||
| window.addEventListener('focus', () => setTimeout(() => { if (!settled) { resolve(null); input.remove(); } }, 500), { once: true }); |
There was a problem hiding this comment.
使用 window focus 兜底判定取消文件选择存在一个已知问题:如果用户在文件选择框打开时切换到其他应用,然后再切换回浏览器,窗口会重新获得焦点,从而提前触发取消逻辑,导致后续选择文件失效。现代浏览器 and WebKit 内核已原生支持 cancel 事件,建议结合使用 cancel 事件以提供更可靠的取消判定。
| let settled = false; | |
| input.addEventListener('change', () => { settled = true; resolve(input.files?.[0] ?? null); input.remove(); }); | |
| // 取消时多数内核不触发 change;用 window focus 兜底判定取消 | |
| window.addEventListener('focus', () => setTimeout(() => { if (!settled) { resolve(null); input.remove(); } }, 500), { once: true }); | |
| let settled = false; | |
| input.addEventListener('change', () => { settled = true; resolve(input.files?.[0] ?? null); input.remove(); }); | |
| input.addEventListener('cancel', () => { settled = true; resolve(null); input.remove(); }); | |
| // 取消时多数内核不触发 change;用 window focus 兜底判定取消 | |
| window.addEventListener('focus', () => setTimeout(() => { if (!settled) { resolve(null); input.remove(); } }, 500), { once: true }); |
| { | ||
| Directory.CreateDirectory(dest); | ||
| foreach (var file in Directory.EnumerateFiles(source)) | ||
| File.Copy(file, Path.Combine(dest, Path.GetFileName(file)), overwrite); | ||
| foreach (var dir in Directory.EnumerateDirectories(source)) | ||
| CopyDirectory(dir, Path.Combine(dest, Path.GetFileName(dir)), overwrite); | ||
| } | ||
| } |
There was a problem hiding this comment.
如果目标目录 dest 是源目录 source 的子目录(例如将 C:\\foo 复制到 C:\\foo\\bar),递归调用 CopyDirectory 会导致无限循环,最终导致磁盘写满或栈溢出崩溃。建议在复制前进行路径层级层级校验,防止无限递归。
public static void CopyDirectory(string source, string dest, bool overwrite = true)
{
var sourceReal = Path.GetFullPath(source);
var destReal = Path.GetFullPath(dest);
if (string.Equals(sourceReal, destReal, StringComparison.OrdinalIgnoreCase))
{
return; // 相同目录,无需复制
}
// 避免无限递归:如果目标目录是源目录的子目录
if (destReal.StartsWith(sourceReal + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
destReal.StartsWith(sourceReal + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
throw new IOException("Cannot copy a directory into a subdirectory of itself.");
}
Directory.CreateDirectory(dest);
foreach (var file in Directory.EnumerateFiles(source))
File.Copy(file, Path.Combine(dest, Path.GetFileName(file)), overwrite);
foreach (var dir in Directory.EnumerateDirectories(source))
CopyDirectory(dir, Path.Combine(dest, Path.GetFileName(dir)), overwrite);
}| MemoryStream ms; | ||
| if (s is MemoryStream existing) | ||
| { | ||
| ms = existing; | ||
| } | ||
| else | ||
| { | ||
| ms = new MemoryStream(); | ||
| s.CopyTo(ms); | ||
| ms.Position = 0; | ||
| } | ||
|
|
||
| byte[] track = ConvertStream.ConvertFile(options, ms, encodeType, convertToType); |
There was a problem hiding this comment.
当输入流 s 不是 MemoryStream 时,新创建的 MemoryStream ms 在方法结束时没有被释放。建议使用 C# 的 using 声明来优雅地管理临时 MemoryStream 的生命周期,确保其被正确释放。
using var tempMs = s is MemoryStream ? null : new MemoryStream();
if (tempMs != null)
{
s.CopyTo(tempMs);
tempMs.Position = 0;
}
MemoryStream ms = (s as MemoryStream) ?? tempMs!;
byte[] track = ConvertStream.ConvertFile(options, ms, encodeType, convertToType);| var result = new List<ImportDirEntry>(); | ||
| // 子目录 | ||
| foreach (var dir in Directory.EnumerateDirectories(path)) | ||
| { | ||
| result.Add(new ImportDirEntry(Path.GetFileName(dir), dir, true)); | ||
| } | ||
| // 子文件 | ||
| foreach (var file in Directory.EnumerateFiles(path)) | ||
| { | ||
| result.Add(new ImportDirEntry(Path.GetFileName(file), file, false)); | ||
| } | ||
| return Ok(result); |
There was a problem hiding this comment.
如果用户选择的导入目录中包含某些受系统保护或无权限访问的子目录/文件,Directory.EnumerateDirectories 或 EnumerateFiles 会抛出 UnauthorizedAccessException,导致整个 API 请求返回 500 错误。建议使用 try-catch 块包裹枚举过程,以便在遇到权限问题时能优雅降级并记录警告,而不是直接崩溃。
var result = new List<ImportDirEntry>();
try
{
// 子目录
foreach (var dir in Directory.EnumerateDirectories(path))
{
result.Add(new ImportDirEntry(Path.GetFileName(dir), dir, true));
}
// 子文件
foreach (var file in Directory.EnumerateFiles(path))
{
result.Add(new ImportDirEntry(Path.GetFileName(file), file, false));
}
}
catch (Exception e)
{
logger.LogWarning(e, "读取导入目录失败:{Path}", path);
}
return Ok(result);There was a problem hiding this comment.
22 issues found across 103 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="MaiChartManager/Controllers/Music/MovieConvertController.cs">
<violation number="1" location="MaiChartManager/Controllers/Music/MovieConvertController.cs:33">
P1: Unsanitized `assetDir` is used in file-system write paths, enabling path traversal outside `StreamingAssets`. Validate/canonicalize combined path and reject values that resolve outside the allowed root.</violation>
</file>
<file name="MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs">
<violation number="1" location="MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs:26">
P1: Security gate is incomplete: endpoints trust `Export` flag but do not enforce loopback caller. Add remote-IP loopback check to prevent accidental LAN exposure of filesystem APIs.</violation>
<violation number="2" location="MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs:46">
P2: Handle directory enumeration exceptions here; inaccessible entries currently bubble out and can turn this endpoint into a 500.</violation>
</file>
<file name="MaiChartManager/Utils/Audio.cs">
<violation number="1" location="MaiChartManager/Utils/Audio.cs:125">
P2: Wrap ffmpeg execution with failure cleanup so temporary WAV files are deleted when conversion throws.</violation>
</file>
<file name="MaiChartManager/LinuxProgram.cs">
<violation number="1" location="MaiChartManager/LinuxProgram.cs:20">
P3: Dispose `ManualResetEventSlim` after startup synchronization to release its wait handle.</violation>
</file>
<file name="MaiChartManager/Front/src/views/BatchAction/ChooseAction.tsx">
<violation number="1" location="MaiChartManager/Front/src/views/BatchAction/ChooseAction.tsx:83">
P2: Local-host maidata export drops ById/subdir semantics; ConvertToMaidataById and subdir selection are ignored in this new path. This causes inconsistent export structure versus the non-local path.</violation>
<violation number="2" location="MaiChartManager/Front/src/views/BatchAction/ChooseAction.tsx:87">
P2: Canceling native folder selection can still show “export success” and close the action step. Users get a false-success result for a no-op export.</violation>
</file>
<file name="MaiChartManager/Front/src/utils/webkitDirectoryAdapter.ts">
<violation number="1" location="MaiChartManager/Front/src/utils/webkitDirectoryAdapter.ts:59">
P3: 新增的 webkitDirectory 适配器当前未被任何代码路径使用,形成死代码并增加维护负担。应删除该实现或在实际目录选择流程中接入调用。</violation>
</file>
<file name="MaiChartManager/Front/src/utils/httpImportDirectory.ts">
<violation number="1" location="MaiChartManager/Front/src/utils/httpImportDirectory.ts:54">
P2: Directory listing HTTP errors are silently swallowed, returning an empty iterator. This can cause import logic to behave as if the folder is empty instead of surfacing a real failure.</violation>
</file>
<file name="MaiChartManager/Controllers/App/LocaleController.cs">
<violation number="1" location="MaiChartManager/Controllers/App/LocaleController.cs:22">
P2: Invalid locale input is handled by throwing an exception, producing error-path responses instead of a client validation response. Return `BadRequest`/`ValidationProblem` for unsupported locale values.</violation>
<violation number="2" location="MaiChartManager/Controllers/App/LocaleController.cs:25">
P2: Locale application logic is duplicated across controller/startup paths, increasing drift risk when locale rules change. Centralize this block in one shared method/service and call it here.</violation>
</file>
<file name="MaiChartManager/Front/src/views/Charts/MusicEdit/AcbAwb.tsx">
<violation number="1" location="MaiChartManager/Front/src/views/Charts/MusicEdit/AcbAwb.tsx:41">
P2: 500ms delay before overlays hide when user cancels file picker. `pickFile`'s cancellation detection uses a `focus` event listener with 500ms setTimeout, making the BottomOverlay linger after the user already dismissed the dialog. To remove the delay, set `tipShow.value = false` before awaiting `pickFile` (the native dialog itself is enough visual feedback).</violation>
</file>
<file name="MaiChartManager/Controllers/App/ShellController.cs">
<violation number="1" location="MaiChartManager/Controllers/App/ShellController.cs:30">
P2: API reports success even when URL opening fails on Linux. This breaks frontend error handling and hides real launch failures.</violation>
<violation number="2" location="MaiChartManager/Controllers/App/ShellController.cs:35">
P2: 500 response leaks raw exception message to callers. Return a generic error body and keep details only in logs.</violation>
</file>
<file name="MaiChartManager/Front/src/client/api.ts">
<violation number="1" location="MaiChartManager/Front/src/client/api.ts:48">
P2: Photino detection is too broad and misclassifies regular localhost browsers, causing preview open to call a non-existent host bridge.</violation>
</file>
<file name="MaiChartManager/Front/src/utils/pickFile.ts">
<violation number="1" location="MaiChartManager/Front/src/utils/pickFile.ts:13">
P3: Focus-based cancel fallback can leave dangling Promise and leaked DOM element. If `window.focus` doesn't fire after cancel, `input` stays in DOM and Promise never resolves.</violation>
</file>
<file name="MaiChartManager/Utils/PathUtils.cs">
<violation number="1" location="MaiChartManager/Utils/PathUtils.cs:8">
P1: This segment check misses path-boundary cases like `A000/foo` and `.../A000`; match by path segments instead of searching for `"/{segment}/"`.</violation>
</file>
<file name="MaiChartManager/Platform/Linux/LinuxShellService.cs">
<violation number="1" location="MaiChartManager/Platform/Linux/LinuxShellService.cs:25">
P1: Use `ProcessStartInfo.ArgumentList` instead of manually quoting `Arguments` so paths/URLs containing quotes or spaces are passed safely as a single argument.</violation>
</file>
<file name="MaiChartManager/Controllers/App/AppLicenseController.cs">
<violation number="1" location="MaiChartManager/Controllers/App/AppLicenseController.cs:41">
P2: Linux purchase response changes `status` to numeric, breaking existing string-enum API contract.</violation>
<violation number="2" location="MaiChartManager/Controllers/App/AppLicenseController.cs:54">
P1: `VerifyOfflineKey` unconditionally succeeds on Linux, allowing invalid offline keys to be accepted.</violation>
</file>
<file name="MaiChartManager/Platform/Linux/PhotinoDialogService.cs">
<violation number="1" location="MaiChartManager/Platform/Linux/PhotinoDialogService.cs:30">
P3: Dispose each `ManualResetEventSlim` created in dialog methods (for example with `using var`) to avoid accumulating wait handles over repeated dialog calls.</violation>
</file>
<file name="MaiChartManager/Platform/PlatformFile.cs">
<violation number="1" location="MaiChartManager/Platform/PlatformFile.cs:58">
P1: Add a guard that rejects destination paths inside the source tree; without it, recursive copy can loop indefinitely.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| if (Path.GetExtension(file.FileName).Equals(".dat", StringComparison.InvariantCultureIgnoreCase)) | ||
| { | ||
| var targetPath = Path.Combine(StaticSettings.StreamingAssets, assetDir, $@"MovieData\{id:000000}.dat"); | ||
| var targetPath = Path.Combine(StaticSettings.StreamingAssets, assetDir, "MovieData", $"{id:000000}.dat"); |
There was a problem hiding this comment.
P1: Unsanitized assetDir is used in file-system write paths, enabling path traversal outside StreamingAssets. Validate/canonicalize combined path and reject values that resolve outside the allowed root.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Controllers/Music/MovieConvertController.cs, line 33:
<comment>Unsanitized `assetDir` is used in file-system write paths, enabling path traversal outside `StreamingAssets`. Validate/canonicalize combined path and reject values that resolve outside the allowed root.</comment>
<file context>
@@ -30,15 +30,17 @@ public async Task SetMovie(int id, [FromForm] double padding, IFormFile file, st
if (Path.GetExtension(file.FileName).Equals(".dat", StringComparison.InvariantCultureIgnoreCase))
{
- var targetPath = Path.Combine(StaticSettings.StreamingAssets, assetDir, $@"MovieData\{id:000000}.dat");
+ var targetPath = Path.Combine(StaticSettings.StreamingAssets, assetDir, "MovieData", $"{id:000000}.dat");
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
await using var stream = System.IO.File.Open(targetPath, FileMode.Create);
</file context>
| public ActionResult<string?> PickImportFolder() | ||
| { | ||
| // 仅本地桌面场景,export / 远程模式下禁止 | ||
| if (StaticSettings.Config.Export) return Forbid(); |
There was a problem hiding this comment.
P1: Security gate is incomplete: endpoints trust Export flag but do not enforce loopback caller. Add remote-IP loopback check to prevent accidental LAN exposure of filesystem APIs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs, line 26:
<comment>Security gate is incomplete: endpoints trust `Export` flag but do not enforce loopback caller. Add remote-IP loopback check to prevent accidental LAN exposure of filesystem APIs.</comment>
<file context>
@@ -0,0 +1,75 @@
+ public ActionResult<string?> PickImportFolder()
+ {
+ // 仅本地桌面场景,export / 远程模式下禁止
+ if (StaticSettings.Config.Export) return Forbid();
+ var path = dialogService.PickFolder();
+ logger.LogInformation("PickImportFolder: {path}", path);
</file context>
| /// 判断路径是否包含某个目录段(跨平台,忽略分隔符差异,大小写不敏感) | ||
| public static bool ContainsSegment(string? path, string segment) | ||
| => path is not null && | ||
| path.Replace('\\', '/').Contains($"/{segment}/", System.StringComparison.InvariantCultureIgnoreCase); |
There was a problem hiding this comment.
P1: This segment check misses path-boundary cases like A000/foo and .../A000; match by path segments instead of searching for "/{segment}/".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Utils/PathUtils.cs, line 8:
<comment>This segment check misses path-boundary cases like `A000/foo` and `.../A000`; match by path segments instead of searching for `"/{segment}/"`.</comment>
<file context>
@@ -0,0 +1,40 @@
+ /// 判断路径是否包含某个目录段(跨平台,忽略分隔符差异,大小写不敏感)
+ public static bool ContainsSegment(string? path, string segment)
+ => path is not null &&
+ path.Replace('\\', '/').Contains($"/{segment}/", System.StringComparison.InvariantCultureIgnoreCase);
+
+ /// <summary>
</file context>
| { | ||
| try | ||
| { | ||
| Process.Start(new ProcessStartInfo("xdg-open", $"\"{arg}\"") { UseShellExecute = false }); |
There was a problem hiding this comment.
P1: Use ProcessStartInfo.ArgumentList instead of manually quoting Arguments so paths/URLs containing quotes or spaces are passed safely as a single argument.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Platform/Linux/LinuxShellService.cs, line 25:
<comment>Use `ProcessStartInfo.ArgumentList` instead of manually quoting `Arguments` so paths/URLs containing quotes or spaces are passed safely as a single argument.</comment>
<file context>
@@ -0,0 +1,32 @@
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo("xdg-open", $"\"{arg}\"") { UseShellExecute = false });
+ }
+ catch (Exception e)
</file context>
| { | ||
| // Linux 不做离线密钥验证;始终视为已授权 | ||
| return Task.FromResult(true); | ||
| } |
There was a problem hiding this comment.
P1: VerifyOfflineKey unconditionally succeeds on Linux, allowing invalid offline keys to be accepted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Controllers/App/AppLicenseController.cs, line 54:
<comment>`VerifyOfflineKey` unconditionally succeeds on Linux, allowing invalid offline keys to be accepted.</comment>
<file context>
@@ -32,4 +35,22 @@ public async Task<bool> VerifyOfflineKey([FromBody] string key)
+ {
+ // Linux 不做离线密钥验证;始终视为已授权
+ return Task.FromResult(true);
+ }
+#endif
+}
</file context>
|
|
||
| var result = new List<ImportDirEntry>(); | ||
| // 子目录 | ||
| foreach (var dir in Directory.EnumerateDirectories(path)) |
There was a problem hiding this comment.
P2: Handle directory enumeration exceptions here; inaccessible entries currently bubble out and can turn this endpoint into a 500.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs, line 46:
<comment>Handle directory enumeration exceptions here; inaccessible entries currently bubble out and can turn this endpoint into a 500.</comment>
<file context>
@@ -0,0 +1,75 @@
+
+ var result = new List<ImportDirEntry>();
+ // 子目录
+ foreach (var dir in Directory.EnumerateDirectories(path))
+ {
+ result.Add(new ImportDirEntry(Path.GetFileName(dir), dir, true));
</file context>
|
|
||
| // 启动进程内 Kestrel:loopback + 伺服 SPA(wwwroot)+ API 同源,但不开 LAN 端口。 | ||
| // Kestrel 在后台线程运行(StartApp 内部 Task.Run),主线程留给 Photino 开窗。 | ||
| var serverReady = new ManualResetEventSlim(false); |
There was a problem hiding this comment.
P3: Dispose ManualResetEventSlim after startup synchronization to release its wait handle.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/LinuxProgram.cs, line 20:
<comment>Dispose `ManualResetEventSlim` after startup synchronization to release its wait handle.</comment>
<file context>
@@ -0,0 +1,181 @@
+
+ // 启动进程内 Kestrel:loopback + 伺服 SPA(wwwroot)+ API 同源,但不开 LAN 端口。
+ // Kestrel 在后台线程运行(StartApp 内部 Task.Run),主线程留给 Photino 开窗。
+ var serverReady = new ManualResetEventSlim(false);
+ string? backendUrl = null;
+ ServerManager.StartApp(export: false, serveSpa: true, onStart: url =>
</file context>
|
|
||
| // 把扁平 FileList(带 webkitRelativePath)重建为目录树,返回根目录适配器。 | ||
| // 选目录时浏览器会把所选目录名作为 webkitRelativePath 的第一段,因此根节点名取该第一段。 | ||
| export function buildDirectoryFromFileList(files: FileList | File[]): ImportDirectory { |
There was a problem hiding this comment.
P3: 新增的 webkitDirectory 适配器当前未被任何代码路径使用,形成死代码并增加维护负担。应删除该实现或在实际目录选择流程中接入调用。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Front/src/utils/webkitDirectoryAdapter.ts, line 59:
<comment>新增的 webkitDirectory 适配器当前未被任何代码路径使用,形成死代码并增加维护负担。应删除该实现或在实际目录选择流程中接入调用。</comment>
<file context>
@@ -0,0 +1,93 @@
+
+// 把扁平 FileList(带 webkitRelativePath)重建为目录树,返回根目录适配器。
+// 选目录时浏览器会把所选目录名作为 webkitRelativePath 的第一段,因此根节点名取该第一段。
+export function buildDirectoryFromFileList(files: FileList | File[]): ImportDirectory {
+ const list = Array.from(files);
+ // 用一个虚拟根承载,最终若只有单一顶层目录则把它作为返回根
</file context>
| let settled = false; | ||
| input.addEventListener('change', () => { settled = true; resolve(input.files?.[0] ?? null); input.remove(); }); | ||
| // 取消时多数内核不触发 change;用 window focus 兜底判定取消 | ||
| window.addEventListener('focus', () => setTimeout(() => { if (!settled) { resolve(null); input.remove(); } }, 500), { once: true }); |
There was a problem hiding this comment.
P3: Focus-based cancel fallback can leave dangling Promise and leaked DOM element. If window.focus doesn't fire after cancel, input stays in DOM and Promise never resolves.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Front/src/utils/pickFile.ts, line 13:
<comment>Focus-based cancel fallback can leave dangling Promise and leaked DOM element. If `window.focus` doesn't fire after cancel, `input` stays in DOM and Promise never resolves.</comment>
<file context>
@@ -0,0 +1,16 @@
+ let settled = false;
+ input.addEventListener('change', () => { settled = true; resolve(input.files?.[0] ?? null); input.remove(); });
+ // 取消时多数内核不触发 change;用 window focus 兜底判定取消
+ window.addEventListener('focus', () => setTimeout(() => { if (!settled) { resolve(null); input.remove(); } }, 500), { once: true });
+ input.click();
+ });
</file context>
| @@ -0,0 +1,155 @@ | |||
| #if !WINDOWS | |||
There was a problem hiding this comment.
P3: Dispose each ManualResetEventSlim created in dialog methods (for example with using var) to avoid accumulating wait handles over repeated dialog calls.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Platform/Linux/PhotinoDialogService.cs, line 30:
<comment>Dispose each `ManualResetEventSlim` created in dialog methods (for example with `using var`) to avoid accumulating wait handles over repeated dialog calls.</comment>
<file context>
@@ -0,0 +1,155 @@
+ }
+
+ string[]? result = null;
+ var done = new ManualResetEventSlim();
+ // 上次选过的目录作为初始目录(没有则交给系统默认)
+ var startDir = StaticSettings.Config.LastDialogFolder is { Length: > 0 } d && Directory.Exists(d) ? d : "";
</file context>
概述
让 MaiChartManager 原生运行在 Linux 上(Windows 行为保持不变)。界面本就是 Vue SPA,无需"提取";真正要做的是替换 Windows 外壳、让 ASP.NET Core 后端在 Linux 构建运行,并把音视频等原生依赖跨平台化。
通过
Configuration驱动条件编译实现单项目双平台:LinuxDebug/LinuxRelease→net10.0,Windows 配置 →net10.0-windows+WINDOWS宏。主要改动
宿主与平台抽象
IDesktopDialogService/IShellService/ITaskbarProgress/IAppShell/IProgressController,Windows 用 WinForms 实现、Linux 用 Photino/无头实现。贴图/AssetBundle
AssetsTools.NET.Texture(纯托管),彻底移除 AssetStudio 依赖(含 nativeTexture2DDecoderNative与相关 Libs 二进制)。音频
视频 / FFmpeg
-hwaccel dxva2(Windows 专有)在 Linux 导致转换中断的问题。GlobalFFOptions,不再误用随 Windows 构建拷入的.exe。CLI / MuConvert
mcm(MaiChartManager CLI)与muconvert跨平台可用;maichartmanager启动 GUI。.exe(仅 Windows 打包时才自包含)。打包
aspnet-runtime+webkit2gtk-4.1+ffmpeg),装maichartmanager/mcm/muconvert三个命令、.desktop与图标。Build.ps1)。杂项修复
Locale.zh-hans/zh-hant.resx区域名大小写规范化(Linux 卫星目录冲突 + 中文本地化失效)。ContentRoot为 exeDir(默认用 cwd,从 HOME 启动会扫描海量文件)。Windows 影响
设计上 Windows 走原有路径、逐参数等价。需在 Windows 上编译 + 跑一遍回归验证(音视频转换、导入、打包)。
备注
fix/linux-resx-case(commitae6aeba),本 PR pin 到该 commit,可后续合入 MuConvert master。.omo/drafts/(未提交)。🤖 Generated with Claude Code