-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
1439 lines (1364 loc) · 141 KB
/
Copy pathProgram.cs
File metadata and controls
1439 lines (1364 loc) · 141 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace ArcConfGUI;
internal static class Program
{
[STAThread]
static void Main(string[] args)
{
ApplicationConfiguration.Initialize();
var arcconf = ArcConfLocator.Find();
if (args.Length == 2 && args[0] == "--self-test")
{
var s = Snapshot.Parse(File.ReadAllText(args[1]));
File.WriteAllText(System.IO.Path.ChangeExtension(args[1], ".selftest.txt"),
$"OK Controller={s.ControllerName}; Mode={s.ControllerMode}; RAW={s.RawExposure}; HostBoot={s.HostBootMode}; Connectors={s.ConnectorModes}; Drives={s.Drives.Count}; Arrays={s.Arrays.Count}; LogicalDrives={s.LogicalDrives.Count}");
return;
}
if(args.Length==3&&args[0]=="--snapshot-ids")
{
var s=Snapshot.Parse(File.ReadAllText(args[1]));var d=s.Drives.FirstOrDefault(x=>x.Channel==0&&x.Id==5);
File.WriteAllLines(args[2],[
$"DRIVE_0_5={(d==null?"MISSING":$"{d.State}|{d.Config}")}",
$"LD_IDS={string.Join(',',s.LogicalDrives.Select(x=>x.Id))}",
$"ARRAY_IDS={string.Join(',',s.Arrays.Select(x=>x.Id))}"]);
return;
}
if(args.Length==3&&args[0]=="--health-plan")
{
var s=Snapshot.Parse(File.ReadAllText(args[1]));var targets=DiskScanPlanner.Build(s,s.Drives);
File.WriteAllLines(args[2],s.Drives.Select(d=>$"DRIVE {d.Key} {d.HealthLevel} {d.HealthReason}").Concat(targets.Select(t=>$"TARGET {t.Id} | {t.Strategy} | {(t.Available?t.DevicePath:"UNAVAILABLE")} | {t.Length} | {string.Join(',',t.PhysicalKeys)} | {t.Notes}")));
return;
}
if(args.Length==3&&args[0]=="--render-health-setup")
{
var s=Snapshot.Parse(File.ReadAllText(args[1]));Render(new HealthScanSetupForm(s,s.Drives.Select(x=>x.Key)),args[2]);return;
}
if(args.Length==3&&args[0]=="--render-health-scan")
{
var s=Snapshot.Parse(File.ReadAllText(args[1]));Render(new DiskHealthScanForm(DiskScanPlanner.Build(s,s.Drives),2,false),args[2]);return;
}
if(args.Length==4&&args[0]=="--scan-read-test")
{
DiskScanUpdate? last=null;var target=new DiskScanTarget("TEST","只读访问测试",args[1],long.Parse(args[2],CultureInfo.InvariantCulture),"测试","仅读取指定长度",[]);
DiskSequentialScanner.ScanAsync(target,new InlineProgress<DiskScanUpdate>(x=>last=x),CancellationToken.None).GetAwaiter().GetResult();
File.WriteAllText(args[3],last==null?"NO RESULT":$"{last.State}|processed={last.Processed}|read={last.BytesRead}|errors={last.Errors}|retries={last.Retries}|first={last.FirstError}|{last.Message}");return;
}
if(args.Length==2&&args[0]=="--argument-normalization-test")
{
File.WriteAllText(args[1],string.Join(' ',ArcConfClient.NormalizeArguments(["CREATE","1","RAIDZEROARRAY","0","5","noprompt","nologs"]))+Environment.NewLine+string.Join(' ',ArcConfClient.NormalizeArguments(["SETCACHE","1","LOGICALDRIVE","1","coff","noprompt","NOLOGS"])));
return;
}
if (args.Length >= 1 && args[0] == "--dialog-test")
{
Application.Run(new OperationInfoForm(OperationCatalog.Explain("设置为全局热备盘", ["SETSTATE","1","DEVICE","0","5","HSP","noprompt","nologs"], false), "arcconf SETSTATE 1 DEVICE 0 5 HSP noprompt nologs"));
return;
}
if (args.Length >= 1 && args[0] == "--drive-detail-test")
{
Application.Run(new DriveDetailForm(new PhysicalDrive(0,5,6,"Ready","Unassigned","ATA WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,
"【身份与位置】\r\n通道 / 设备 ID : 0 / 5\r\n槽位 : 6\r\n型号 : WDC WD5000AAKX-0\r\n序列号 : WD-WMAYUM262366\r\n固件 : 15.01H15\r\n\r\n【容量与连接】\r\n逻辑块大小 : 512\r\n物理块大小 : 512\r\n协商速率 : SATA 6.0 Gb/s\r\n\r\n【健康、温度与寿命】\r\n支持 SMART : True\r\n当前温度 : 42\r\n最高温度 : 42\r\n阈值温度 : 65\r\n\r\n【错误计数】\r\nmediaFailures : 0\r\nhardReadErrors : 0\r\nhardWriteErrors : 0",
"{\"controllerID\":1,\"channelID\":0,\"deviceID\":5,\"model\":\"WDC WD5000AAKX-0\"}")));
return;
}
if (args.Length == 2 && args[0] == "--render-dialog")
{
Render(new OperationInfoForm(OperationCatalog.Explain("设置为全局热备盘", ["SETSTATE","1","DEVICE","0","5","HSP","noprompt","nologs"], false), "arcconf SETSTATE 1 DEVICE 0 5 HSP noprompt nologs"), args[1]);
return;
}
if (args.Length == 2 && args[0] == "--render-drive")
{
Render(new DriveDetailForm(new PhysicalDrive(0,5,6,"Ready","Unassigned","ATA WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,
"【身份与位置】\r\n通道 / 设备 ID : 0 / 5\r\n槽位 : 6\r\n型号 : WDC WD5000AAKX-0\r\n序列号 : WD-WMAYUM262366\r\n固件 : 15.01H15\r\n\r\n【容量与连接】\r\n逻辑块大小 : 512\r\n物理块大小 : 512\r\n协商速率 : SATA 6.0 Gb/s\r\n\r\n【健康、温度与寿命】\r\n支持 SMART : True\r\n当前温度 : 42\r\n最高温度 : 42\r\n阈值温度 : 65\r\n\r\n【错误计数】\r\nmediaFailures : 0\r\nhardReadErrors : 0\r\nhardWriteErrors : 0",
"{\"controllerID\":1,\"channelID\":0,\"deviceID\":5,\"model\":\"WDC WD5000AAKX-0\"}")), args[1]);
return;
}
if (args.Length == 2 && args[0] == "--render-raid")
{
var sampleDrives=new List<PhysicalDrive>{
new(0,4,5,"Online","Data","IBM HUSMM8020ASS20","2KVHGBGA","SAS","186.33 GiB","38 °C","0",false,"Sample","{}",true),
new(0,5,6,"Ready","Unassigned","WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,"Sample","{}"),
new(0,6,7,"Ready","Unassigned","SAMSUNG MZ7L3480HCHQ","S6ABCDEF","SATA","447.13 GiB","35 °C","",true,"Sample","{}",true)};
Render(new RaidDialog("在线 RAID 迁移 / 容量扩展",sampleDrives,new LogicalDrive(0,0,"Logical Drive 1","RAID 0","Optimal","186.3 GiB",190747,256,"禁用","E:\\")),args[1]);
return;
}
if (args.Length == 2 && args[0] == "--render-raid-members")
{
var sampleDrives=new List<PhysicalDrive>{new(0,4,5,"Online","Data","IBM HUSMM8020ASS20","2KVHGBGA","SAS","186.33 GiB","38 °C","0",false,"Sample","{}",true),new(0,5,6,"Ready","Unassigned","WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,"Sample","{}"),new(0,6,7,"Ready","Unassigned","SAMSUNG MZ7L3480HCHQ","S6ABCDEF","SATA","447.13 GiB","35 °C","",true,"Sample","{}",true)};
var dialog=new RaidDialog("在线 RAID 迁移 / 容量扩展",sampleDrives,new LogicalDrive(0,0,"Logical Drive 1","RAID 0","Optimal","186.3 GiB",190747,256,"禁用","E:\\"));dialog.ShowMembersTabForTest();Render(dialog,args[1]);
return;
}
if (args.Length == 2 && args[0] == "--render-create")
{
var sampleDrives=new List<PhysicalDrive>{new(0,5,6,"Ready","Unassigned","WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,"Sample","{}"),new(0,6,7,"Ready","Unassigned","SAMSUNG MZ7L3480HCHQ","S6ABCDEF","SATA","447.13 GiB","35 °C","",true,"Sample","{}",true)};
var dialog=new RaidDialog("创建 RAID",sampleDrives,null);dialog.SelectDriveForTest(0);Render(dialog,args[1]);
return;
}
if(args.Length==2&&args[0]=="--create-options-test")
{
using var hdd=new RaidDialog("test",[new(0,5,6,"Ready","Unassigned","WDC HDD","HDD1","SATA","465 GiB","40 °C","",true,"Sample","{}")],null);hdd.SelectDriveForTest(0);hdd.SetCacheForTest(false);
using var ssd=new RaidDialog("test",[new(0,6,7,"Ready","Unassigned","SAS SSD","SSD1","SAS","186 GiB","35 °C","",true,"Sample","{}",true)],null);ssd.SelectDriveForTest(0);ssd.SetBypassForTest(true);
File.WriteAllText(args[1],$"HDD={string.Join(' ',hdd.StorageArguments)}\r\nSSD={string.Join(' ',ssd.StorageArguments)}");return;
}
if (args.Length == 2 && args[0] == "--render-picker")
{
var sampleDrives=new List<PhysicalDrive>{new(0,5,6,"Ready","Unassigned","WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,"Sample","{}"),new(0,6,7,"Ready","Unassigned","SAMSUNG MZ7L3480HCHQ","S6ABCDEF","SATA","447.13 GiB","35 °C","",true,"Sample","{}")};
Render(new DrivePicker("选择要加入阵列的 Ready 物理盘",sampleDrives),args[1]);
return;
}
if (args.Length == 2 && args[0] == "--render-prompt")
{
Render(new TextPromptForm("危险操作确认","在线迁移会在后台重新布局数据。\r\n请输入 MIGRATE 继续:",""),args[1]);
return;
}
if (args.Length == 4 && args[0] == "--render-structured")
{
var form=new MainForm(new ArcConfClient(arcconf),false);form.LoadStructuredForTest(args[1],File.ReadAllText(args[2]),args[1].Equals("SMART",StringComparison.OrdinalIgnoreCase));Render(form,args[3]);
return;
}
if (args.Length == 3 && args[0] == "--render-main-tab")
{
var form=new MainForm(new ArcConfClient(arcconf),false,int.Parse(args[1],CultureInfo.InvariantCulture));
if(args[1]=="1") form.LoadDrivesForTest();
if(args[1]=="2") form.LoadStorageForTest();
Render(form,args[2]);
return;
}
if (!File.Exists(arcconf))
{
MessageBox.Show($"未找到 ARCCONF:\n{arcconf}", "ARCCONF GUI", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (args.Length >= 1 && args[0] == "--layout-test")
{
Application.Run(new MainForm(new ArcConfClient(arcconf), false, args.Length > 1 && int.TryParse(args[1], out var tab) ? tab : 0));
return;
}
Application.Run(new MainForm(new ArcConfClient(arcconf)));
}
static void Render(Form form,string path)
{
form.Show();Application.DoEvents();
using var bitmap=new Bitmap(form.Width,form.Height);
form.DrawToBitmap(bitmap,new Rectangle(0,0,bitmap.Width,bitmap.Height));
bitmap.Save(path,System.Drawing.Imaging.ImageFormat.Png);form.Close();
}
}
internal static class ArcConfLocator
{
const string AppKey = @"SOFTWARE\ArcConfGUI";
public static string Find()
{
var candidates = new List<string>();
try
{
using var key = Registry.LocalMachine.OpenSubKey(AppKey);
if (key?.GetValue("ArcConfPath") is string configured)
candidates.Add(configured);
}
catch { }
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
candidates.AddRange(path.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(directory => System.IO.Path.Combine(directory.Trim('"'), "arcconf.exe")));
candidates.AddRange([
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microchip", "ARCCONF", "arcconf.exe"),
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Adaptec", "ARCCONF", "arcconf.exe"),
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microchip", "ARCCONF", "arcconf.exe"),
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Adaptec", "ARCCONF", "arcconf.exe")
]);
return candidates.FirstOrDefault(File.Exists)
?? System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microchip", "ARCCONF", "arcconf.exe");
}
}
internal static class AppAssets
{
public static Icon? Icon(string name,int size)
{
using var stream=Assembly.GetExecutingAssembly().GetManifestResourceStream(name);if(stream==null)return null;
using var source=new Icon(stream,size,size);return (Icon)source.Clone();
}
public static Image? Image(string name)
{
using var stream=Assembly.GetExecutingAssembly().GetManifestResourceStream(name);if(stream==null)return null;
using var source=System.Drawing.Image.FromStream(stream);return new Bitmap(source);
}
}
internal sealed record CommandResult(int ExitCode, string Output, string Command)
{
public bool NoChange => Output.Contains("already set to",StringComparison.OrdinalIgnoreCase);
public bool Success => (ExitCode == 0 || NoChange) && !Output.Contains("Invalid arguments", StringComparison.OrdinalIgnoreCase)
&& !Output.Contains("Command failed", StringComparison.OrdinalIgnoreCase);
}
internal sealed class ArcConfClient(string path)
{
public string Path { get; } = path;
// ARCCONF 7.27 on this SmartRAID 3154-8i advertises NOLOGS as optional,
// but rejects otherwise valid CREATE and SETCACHE commands when it is
// present. The GUI keeps its own complete log, so remove it centrally.
public static string[] NormalizeArguments(IEnumerable<string> args)=>args.Where(x=>!x.Equals("nologs",StringComparison.OrdinalIgnoreCase)).ToArray();
public async Task<CommandResult> RunAsync(params string[] args)
{
args=NormalizeArguments(args);
var psi = new ProcessStartInfo(Path) {
UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8
};
foreach (var arg in args) psi.ArgumentList.Add(arg);
using var process = new Process { StartInfo = psi };
process.Start();
var stdout = process.StandardOutput.ReadToEndAsync();
var stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var text = (await stdout) + (await stderr);
return new(process.ExitCode, text.Trim(), "arcconf " + string.Join(" ", args.Select(Quote)));
}
static string Quote(string s) => s.Any(char.IsWhiteSpace) ? $"\"{s.Replace("\"", "\\\"")}\"" : s;
}
internal sealed record PhysicalDrive(int Channel, int Id, int Slot, string State, string Config, string Model,
string Serial, string Interface, string Size, string Temperature, string Arrays, bool IsReady,
string DetailText, string RawJson, bool IsSsd=false, string HealthLevel="Green", string HealthReason="状态正常",
bool ExposedToOs=false, string OsPath="", long SizeBytes=0)
{
public string Key => $"{Channel}:{Id}";
public string HealthGlyph => "■";
public override string ToString() => $"{Channel}:{Id} 槽位 {Slot} {Model} {Size} [{State}/{Config}]";
}
internal sealed record LogicalDrive(int Id, int ArrayId, string Name, string Raid, string State, string Size,
long SizeMb, int StripeKb, string Cache, string MountPoint, string OsPath="", long SizeBytes=0);
internal sealed record ArrayView(int Id, string Name, string Status, string Interface, string Size, string Free,
string Members, string Bypass);
internal sealed record OverviewRow(string Category,string Item,string Value,string Details="");
internal sealed class Snapshot
{
public int ControllerId { get; init; }
public string ControllerName { get; init; } = "";
public string Serial { get; init; } = "";
public string Firmware { get; init; } = "";
public string Driver { get; init; } = "";
public string Temperature { get; init; } = "";
public string PowerMode { get; init; } = "";
public int FunctionalMode { get; init; }
public string ControllerMode { get; init; } = "";
public string RawExposure { get; init; } = "";
public bool RaidFunctionsAvailable => FunctionalMode != 2;
public bool ModeChangeSupported { get; init; }
public string ConnectorModes { get; init; } = "";
public string HostBootMode { get; init; } = "";
public string ControllerBios { get; init; } = "";
public bool RuntimeBiosEnabled { get; init; }
public bool BootController { get; init; }
public List<PhysicalDrive> Drives { get; } = [];
public List<LogicalDrive> LogicalDrives { get; } = [];
public List<ArrayView> Arrays { get; } = [];
public static Snapshot Parse(string json)
{
var root = JsonNode.Parse(json) ?? throw new InvalidDataException("ARCCONF 返回了空 JSON");
var c = root["Controller"] ?? throw new InvalidDataException("JSON 中没有 Controller");
var s = new Snapshot {
ControllerId = I(c, "controllerID"), ControllerName = S(c, "deviceName"), Serial = S(c, "serialNumber"),
Firmware = S(c, "firmwareVersion"), Driver = S(c, "driverVersion"),
Temperature = S(c, "heatSensorTemperature") + " °C", PowerMode = Power(I(c, "powerModeOperational")),
FunctionalMode=I(c,"functionalMode"),ControllerMode=ControllerModeName(I(c,"functionalMode")),
RawExposure=I(c,"functionalMode") is 2 or 5?"暴露给操作系统":"隐藏,不暴露给操作系统",
ModeChangeSupported=B(c,"controllerModeChangeOperationSupport"),
ConnectorModes=string.Join(";",Items(c["SASConnector"]).Select(x=>$"{S(x,"connectorName")}: {ConnectorModeName(I(x,"functionalMode"))}")),
HostBootMode=SystemFirmware.Detect(),ControllerBios=S(c,"biosVersion") is "" or "0"?"未加载或未报告":S(c,"biosVersion"),
RuntimeBiosEnabled=B(c,"runTimeBIOSEnabled"),BootController=B(c,"bootController")
};
foreach (var channel in Items(c["Channel"]))
foreach (var d in Items(channel?["HardDrive"]))
{
var ch = I(d, "channelID"); var id = I(d, "deviceID"); var cfg = I(d, "driveConfigType");
var arrays = Items(d?["Chunk"]).Select(x => I(x, "consumerArrayID", -1)).Where(x => x >= 0 && x < 65535).Distinct();
var driveBytes=UL(d, "size") * (ulong)Math.Max(1, I(d, "blockSize", 512));
var health=AssessHealth(d,I(d,"state"));
s.Drives.Add(new(ch, id, I(d, "slotID", -1), DriveState(I(d, "state")), ConfigType(cfg),
$"{S(d, "vendor")} {S(d, "model")}".Trim(), S(d, "serialNumber"), Interface(I(d, "interfaceType")),
Bytes(driveBytes), S(d, "currentTemperature") + " °C",
string.Join(",", arrays), cfg == 2 || I(d, "state") == 0, DriveDetails(d),
d?.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented=true }) ?? "",B(d,"nonSpinning"),
health.Level,health.Reason,B(d,"isDriveExposedToOS"),S(d,"physicalDriveName"),ToLong(driveBytes)));
}
foreach (var ld in Items(c["LogicalDrive"]))
{
var chunks = Items(ld?["Chunk"]); var arrayId = chunks.Select(x => I(x, "consumerArrayID", -1)).FirstOrDefault(x => x >= 0, -1);
var sectors = UL(ld, "dataSpace"); var block = Math.Max(1, I(ld, "BlockSize", 512));
var logicalBytes=sectors*(ulong)block;
s.LogicalDrives.Add(new(I(ld, "logicalDriveID"), arrayId, S(ld, "name"), Raid(I(ld, "raidLevel")),
LogicalState(I(ld, "state")), Bytes(sectors * (ulong)block), (long)(sectors * (ulong)block / 1024 / 1024),
I(ld, "stripeSize"), B(ld, "caching") ? "启用" : "禁用", S(ld, "mountPoints"),
ExtractPhysicalDrivePath(S(ld,"physicalDriveName")),ToLong(logicalBytes)));
}
foreach (var a in Items(c["Array"]))
{
var members = Items(a?["Chunk"]).Where(x => x?["deviceID"] != null).Select(x => $"{I(x,"channelID")}:{I(x,"deviceID")}");
s.Arrays.Add(new(I(a, "arrayID"), S(a, "arrayName"), S(a, "status"), S(a, "interfaceType"),
Bytes(UL(a, "totalSize") * (ulong)Math.Max(1, I(a, "blockSize", 512))),
Bytes(UL(a, "unUsedSpace") * (ulong)Math.Max(1, I(a, "blockSize", 512))), string.Join(", ", members),
B(a, "ssdIOBypass") ? "启用" : "禁用"));
}
return s;
}
static IEnumerable<JsonNode?> Items(JsonNode? node) => node is JsonArray a ? a : node == null ? [] : [node];
static string S(JsonNode? n, string p) => n?[p]?.ToString() ?? "";
static int I(JsonNode? n, string p, int fallback = 0) => int.TryParse(S(n,p), out var v) ? v : fallback;
static ulong UL(JsonNode? n, string p) => ulong.TryParse(S(n,p), out var v) ? v : 0;
static bool B(JsonNode? n, string p) => bool.TryParse(S(n,p), out var v) && v;
static long ToLong(ulong value)=>value>(ulong)long.MaxValue?long.MaxValue:(long)value;
static string ExtractPhysicalDrivePath(string value)=>Regex.Match(value,@"\\\\\.\\PhysicalDrive\d+",RegexOptions.IgnoreCase).Value;
static (string Level,string Reason) AssessHealth(JsonNode? d,int state)
{
static long N(JsonNode? node,string name)=>long.TryParse(node?[name]?.ToString(),out var value)?value:0;
var reasons=new List<string>();
var last=S(d,"lastFailureReason");
var hardCounters=new[]{"mediaFailures","hardReadErrors","hardWriteErrors","hardwareErrors","failedReadRecovers","failedWriteRecovers","markedBadBlocks"};
foreach(var name in hardCounters){var value=N(d,name);if(value>0)reasons.Add($"{name}={value}");}
if(state==3||B(d,"pfaError")||reasons.Count>0||(!string.IsNullOrWhiteSpace(last)&&last!="No Failure"&&last!="Not Applicable"))
{
if(state==3)reasons.Insert(0,"控制器已判定 Failed");
if(B(d,"pfaError"))reasons.Insert(0,"SMART/PFA 故障");
if(!string.IsNullOrWhiteSpace(last)&&last!="No Failure"&&last!="Not Applicable")reasons.Add(last);
return ("Red",string.Join(";",reasons.Distinct()));
}
var warnings=new List<string>();
foreach(var name in new[]{"predictiveFailures","smartWarningCount","mediumErrorCount","timeOutErrors","retryRecoveredReadErrors","retryRecoveredWriteErrors"})
{var value=N(d,name);if(value>0)warnings.Add($"{name}={value}");}
if(B(d,"ssdSmartTripWearout")||B(d,"hasSSDWearOut"))warnings.Add("SSD 寿命告警");
var reported56DayWarning=B(d,"ssdHas56DayWarning");
var estimatedLifeDays=N(d,"estimatedLifeRemainingBasedOnWorkloadToDate");
var corroborated56DayWarning=reported56DayWarning&&estimatedLifeDays is >0 and <=56;
if(corroborated56DayWarning)warnings.Add($"SSD 预计寿命约 {estimatedLifeDays} 天");
// Some SmartRAID firmware reports pfaEnabled=false even while
// GETSMARTSTATS returns complete, healthy SMART data. Treat only
// actual PFA/SMART errors and warning counters as health signals.
var temperature=N(d,"currentTemperature");var threshold=N(d,"thresholdTemperature");
if(threshold>0&&temperature>=threshold)return("Red",$"温度 {temperature} °C 已达到阈值 {threshold} °C");
if(threshold>0&&temperature>=threshold-8)warnings.Add($"温度接近阈值:{temperature}/{threshold} °C");
if(warnings.Count>0)return("Orange",string.Join(";",warnings.Distinct()));
return ("Green",reported56DayWarning&&!corroborated56DayWarning
?"SMART/PFA、控制器状态和累计错误计数未见异常;未被有效寿命数据佐证的 56 天标志已按固件误报忽略"
:"SMART/PFA、控制器状态和累计错误计数未见异常");
}
static string Bytes(ulong b) { string[] u=["B","KiB","MiB","GiB","TiB"]; double n=b; var i=0; while(n>=1024&&i<u.Length-1){n/=1024;i++;} return $"{n:0.##} {u[i]}"; }
static string Power(int v) => v switch {1=>"最低功耗",2=>"平衡",3=>"最高性能",_=>$"未知 ({v})"};
static string ControllerModeName(int v)=>v switch{2=>"HBA(纯直通)",3=>"RAID(隐藏 RAW)",5=>"Mixed(RAID + RAW)",_=>$"未知 ({v})"};
static string ConnectorModeName(int v)=>v switch{1=>"HBA",2=>"RAID(隐藏 RAW)",3=>"Mixed",_=>$"未知 ({v})"};
static string Raid(int v) => v switch { 0=>"RAID 0",1=>"RAID 1",5=>"RAID 5",6=>"RAID 6",10=>"RAID 10",50=>"RAID 50",60=>"RAID 60",_=>$"RAID {v}"};
static string DriveState(int v) => v switch {0=>"Ready",1=>"Online",2=>"Hot Spare",3=>"Failed",_=>$"状态 {v}"};
static string LogicalState(int v) => v switch {2=>"Optimal",1=>"Degraded",0=>"Offline",_=>$"状态 {v}"};
static string ConfigType(int v) => v switch {0=>"Data",1=>"Spare",2=>"Unassigned",_=>$"类型 {v}"};
static string Interface(int v) => v switch {1=>"SATA",7=>"SAS",8=>"NVMe",_=>$"接口 {v}"};
static string DriveDetails(JsonNode? d)
{
var sb=new StringBuilder();
void Section(string title){sb.AppendLine();sb.AppendLine($"【{title}】");}
void Row(string label,string prop){var value=S(d,prop);if(!string.IsNullOrWhiteSpace(value)&&value!="2147483647"&&value!="Not Applicable")sb.AppendLine($"{label,-22}: {value}");}
Section("身份与位置"); Row("控制器", "controllerID"); Row("通道", "channelID"); Row("设备 ID", "deviceID"); Row("槽位", "slotID");
Row("连接器", "connectorName"); Row("厂商", "vendor"); Row("型号", "model"); Row("序列号", "serialNumber"); Row("固件", "firmwareLevel"); Row("WWN", "wwn"); Row("唯一 ID", "hardDriveUniqueID");
Section("容量与连接"); Row("块数量", "size"); Row("逻辑块大小", "blockSize"); Row("物理块大小", "physicalBlockSize"); Row("接口类型代码", "interfaceType");
Row("协商速率代码", "negotiatedSpeed"); Row("PHY 数量", "phyCount"); Row("转速", "rotationalSpeed"); Row("SSD 磨损告警", "ssdSmartTripWearout");
Section("状态与配置"); Row("状态代码", "state"); Row("配置类型代码", "driveConfigType"); Row("暴露给系统", "isDriveExposedToOS"); Row("挂载点", "hardDriveMountPoint");
Row("启动类型", "bootType"); Row("正在执行任务", "taskInProgress"); Row("预测故障", "pfaError"); Row("最后故障原因", "lastFailureReason"); Row("残留 RIS 配置", "hasRISConfig");
Section("健康、温度与寿命"); Row("支持 SMART", "pfaSupported"); Row("当前温度", "currentTemperature"); Row("最高温度", "maximumTemperature");
Row("温度阈值", "thresholdTemperature"); Row("通电小时", "powerOnHours"); Row("剩余寿命", "estimatedLifeRemainingBasedOnWorkloadToDate"); Row("剩余使用量", "usageRemaining"); Row("56 天警告", "ssdHas56DayWarning");
Section("缓存与功能"); Row("写缓存状态代码", "writeCacheEnable"); Row("写缓存可调", "writeCacheEnableSupported"); Row("支持 NCQ", "bNCQSupported"); Row("NCQ 已启用", "sataNCQEnabled");
Row("支持安全擦除", "SanitizeEraseSupport"); Row("安全擦除方法", "sanitizeMethods"); Row("操作系统分区", "isOSPartitionPresent");
Section("错误计数");
foreach(var p in new[]{"abortedCommands","badTargetErrors","eccRecoveredReadErrors","failedReadRecovers","failedWriteRecovers","formatErrors","hardwareErrors","hardReadErrors","hardWriteErrors","hotPlugCount","mediaFailures","notReadyErrors","timeOutErrors","predictiveFailures","retryRecoveredReadErrors","retryRecoveredWriteErrors","scsiBusFaults","sectorsRead","sectorsWritten","serviceHours","markedBadBlocks"})Row(p,p);
Section("PHY");
foreach(var phy in Items(d?["SASPhy"]))sb.AppendLine($"PHY {S(phy,"phyID")}: negotiated={S(phy,"negPhyLinkRate")}, programmed-max={S(phy,"progMaxPhyLinkRate")}, hardware-max={S(phy,"hwMaxPhyLinkRate")}, changes={S(phy,"phyChangeCount")}");
return sb.ToString().Trim();
}
}
internal static class SystemFirmware
{
[DllImport("kernel32.dll",SetLastError=true)]static extern bool GetFirmwareType(out uint firmwareType);
public static string Detect()
{
try{return GetFirmwareType(out var type)?type switch{1=>"Legacy BIOS",2=>"UEFI",_=>$"未知 ({type})"}:"无法检测";}
catch{return "无法检测";}
}
}
internal sealed class MainForm : Form
{
readonly ArcConfClient client;
Snapshot? snapshot;
readonly Label summary = new() { Dock=DockStyle.Fill, AutoSize=false, Font=new("Segoe UI", 11), Padding=new(12) };
readonly Label overviewTitle = new() { Dock=DockStyle.Fill, AutoSize=true, Font=new("Segoe UI Semibold",11), Padding=new(10,6,10,6), Text="控制器关键摘要" };
readonly DataGridView overviewGrid = Grid();
readonly DataGridView drives = Grid();
readonly DataGridView logical = Grid();
readonly DataGridView arrays = Grid();
readonly Label logicalCacheState = StateLabel("逻辑盘缓存:请选择逻辑盘");
readonly Label bypassState = StateLabel("SSD I/O Bypass:请选择阵列");
readonly Label controllerModeState = StateLabel("控制器模式:等待刷新");
Button? cacheEnableButton,cacheDisableButton,bypassEnableButton,bypassDisableButton;
Button? hbaModeButton,raidModeButton,mixedModeButton;
readonly List<Control> raidOnlyControls=[];
readonly Dictionary<string,(string Level,string Reason)> scanHealthOverrides=[];
readonly TextBox output = new() { Dock=DockStyle.Fill, Multiline=true, ReadOnly=true, ScrollBars=ScrollBars.Both,
Font=new("Consolas", 10), BackColor=Color.FromArgb(24,26,31), ForeColor=Color.Gainsboro, WordWrap=false };
readonly ToolStripStatusLabel status = new("正在初始化...");
readonly TabControl tabs = new() { Dock=DockStyle.Fill };
readonly System.Windows.Forms.Timer refreshTimer=new(){Interval=5*60*1000};
readonly Icon? brandWindowIcon=AppAssets.Icon("ArcConfGUI.Assets.MicrochipBrand.ico",32);
readonly Icon? taskbarUserIcon=AppAssets.Icon("ArcConfGUI.Assets.WindowsUserAvatar.ico",256);
readonly Image? brandLogo=AppAssets.Image("ArcConfGUI.Assets.MicrochipLogo.png");
readonly string appLogPath=System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"ARCCONF GUI","ARCCONF GUI.log");
TabPage? outputPage;
bool busy;
const int WM_SETICON=0x0080;static readonly IntPtr ICON_SMALL=IntPtr.Zero;static readonly IntPtr ICON_BIG=new(1);
[DllImport("user32.dll",CharSet=CharSet.Unicode)]static extern IntPtr SendMessage(IntPtr hWnd,int msg,IntPtr wParam,IntPtr lParam);
public MainForm(ArcConfClient client, bool autoRefresh=true, int initialTab=0)
{
this.client = client;
Text = "ARCCONF GUI — Microchip Adaptec RAID 管理器"; Width=1320; Height=860; MinimumSize=new(1080,700);
if(brandWindowIcon!=null)Icon=brandWindowIcon;
StartPosition=FormStartPosition.CenterScreen; Font=new("Segoe UI", 9.5f); AutoScaleMode=AutoScaleMode.Dpi;
var strip = new ToolStrip { GripStyle=ToolStripGripStyle.Hidden, Padding=new(6), ImageScalingSize=new(24,24) };
strip.Items.Add(Button("↻ 刷新全部", async (_,_) => await RefreshAll()));
strip.Items.Add(Button("任务状态", async (_,_) => await ShowRead("后台任务", "GETSTATUS", Controller(), "nologs")));
strip.Items.Add(Button("事件日志", async (_,_) => await ShowRead("事件日志", "GETLOGS", Controller(), "EVENT", "tabular", "nologs")));
strip.Items.Add(new ToolStripSeparator());
strip.Items.Add(new ToolStripLabel("所有写操作均会先显示命令并要求确认"));
Controls.Add(tabs); Controls.Add(strip); strip.Dock=DockStyle.Top;
var sb = new StatusStrip(); sb.Items.Add(status); Controls.Add(sb);
tabs.TabPages.Add(DashboardTab()); tabs.TabPages.Add(DrivesTab()); tabs.TabPages.Add(LogicalTab());
tabs.TabPages.Add(SettingsTab()); tabs.TabPages.Add(ConsoleTab()); tabs.TabPages.Add(HelpTab());tabs.TabPages.Add(AboutTab());
ConfigureDriveGrid();
ConfigureStorageGrids();
drives.MultiSelect=true;
drives.CellDoubleClick += (_,e) => { if(e.RowIndex>=0 && drives.Rows[e.RowIndex].DataBoundItem is PhysicalDrive d) new DriveDetailForm(d).ShowDialog(this); };
drives.CellFormatting+=DriveCellFormatting;
drives.CellToolTipTextNeeded+=DriveCellToolTipTextNeeded;
drives.CellMouseDown+=(s,e)=>{if(e.Button==MouseButtons.Right&&e.RowIndex>=0&&!drives.Rows[e.RowIndex].Selected){drives.ClearSelection();drives.Rows[e.RowIndex].Selected=true;drives.CurrentCell=drives.Rows[e.RowIndex].Cells[Math.Max(0,e.ColumnIndex)];}};
var driveMenu=new ContextMenuStrip();driveMenu.Items.Add("可视化磁盘健康检测(只读)",null,async(_,_)=>await OpenHealthScan());drives.ContextMenuStrip=driveMenu;
logical.SelectionChanged+=(_,_)=>UpdateStorageActionStates();arrays.SelectionChanged+=(_,_)=>UpdateStorageActionStates();
tabs.SelectedIndex=Math.Clamp(initialTab,0,tabs.TabPages.Count-1);
refreshTimer.Tick+=async(_,_)=>await RefreshAll(true);
if(autoRefresh) Shown+=async(_,_)=>{await RefreshAll();refreshTimer.Start();};else summary.Text="布局测试模式:未访问 RAID 控制器。";
FormClosed+=(_,_)=>{refreshTimer.Stop();refreshTimer.Dispose();brandWindowIcon?.Dispose();taskbarUserIcon?.Dispose();brandLogo?.Dispose();};
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if(brandWindowIcon!=null)SendMessage(Handle,WM_SETICON,ICON_SMALL,brandWindowIcon.Handle);
if(taskbarUserIcon!=null)SendMessage(Handle,WM_SETICON,ICON_BIG,taskbarUserIcon.Handle);
}
TabPage DashboardTab()
{
var p=new TabPage("概览"); var buttons=Flow();buttons.AutoSize=true;
buttons.Controls.Add(B("↻ 刷新摘要",async(_,_)=>await RefreshAll()));
buttons.Controls.Add(new Label{Text="自动刷新:每 5 分钟",AutoSize=true,Padding=new(8,9,12,0),ForeColor=Color.DimGray});
buttons.Controls.Add(B("控制器摘要", (_,_)=>{ShowSnapshotOverview();return Task.CompletedTask;}));
buttons.Controls.Add(B("完整配置", async (_,_)=>await ShowStructured("完整配置","GETCONFIG",Controller(),"nologs")));
buttons.Controls.Add(B("版本", async (_,_)=>await ShowStructured("版本信息","GETVERSION")));
buttons.Controls.Add(B("SMART", async (_,_)=>await ShowStructured("SMART 属性","GETSMARTSTATS",Controller(),"tabular","nologs")));
buttons.Controls.Add(B("重新扫描", async (_,_)=>await Write("重新扫描设备", false, null, "RESCAN",Controller(),"nologs")));
ConfigureOverviewGrid();
var root=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=4};root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.Percent,100));
buttons.Dock=DockStyle.Fill;summary.AutoSize=true;root.Controls.Add(buttons,0,0);root.Controls.Add(summary,0,1);root.Controls.Add(overviewTitle,0,2);root.Controls.Add(overviewGrid,0,3);p.Controls.Add(root);return p;
}
TabPage DrivesTab()
{
var p=new TabPage("物理盘");
var buttons=Flow(wrap:true);
buttons.Controls.Add(new Label{Text="双击任意硬盘可查看完整详情",AutoSize=true,Padding=new(8,9,12,0),ForeColor=Color.DimGray});
buttons.Controls.Add(B("定位灯 30 秒", async(_,_)=>await Identify(true)));
buttons.Controls.Add(B("停止定位灯", async(_,_)=>await Identify(false)));
var globalSpare=B("设为全局热备", async(_,_)=>await Spare(false));var dedicatedSpare=B("设为阵列专用热备", async(_,_)=>await Spare(true));var cancelSpare=B("取消热备", async(_,_)=>await SetDriveState("RDY","取消热备盘"));
buttons.Controls.Add(globalSpare);buttons.Controls.Add(dedicatedSpare);buttons.Controls.Add(cancelSpare);raidOnlyControls.AddRange([globalSpare,dedicatedSpare,cancelSpare]);
buttons.Controls.Add(B("安全擦除", async(_,_)=>await SecureErase(), danger:true));
buttons.Controls.Add(B("健康检测(全盘读)", async(_,_)=>await OpenHealthScan()));
var root=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=2,Padding=new(0)};
root.ColumnStyles.Add(new(SizeType.Percent,100));
root.RowStyles.Add(new(SizeType.AutoSize));
root.RowStyles.Add(new(SizeType.Percent,100));
buttons.Dock=DockStyle.Fill;
root.Controls.Add(buttons,0,0);
root.Controls.Add(drives,0,1);
p.Controls.Add(root); return p;
}
TabPage LogicalTab()
{
var p=new TabPage("阵列与逻辑盘"); var split=new SplitContainer {Dock=DockStyle.Fill,Orientation=Orientation.Horizontal,SplitterDistance=240};
split.SizeChanged+=(_,_)=>{var available=split.ClientSize.Height-split.SplitterWidth;if(available<360)return;var target=(int)(available*.55);if(Math.Abs(split.SplitterDistance-target)>2)split.SplitterDistance=target;};
var topButtons=Flow(wrap:true);topButtons.Controls.Add(logicalCacheState);var createButton=B("创建 RAID", async(_,_)=>await CreateRaid());var migrateButton=B("在线迁移/扩容", async(_,_)=>await MigrateLogical());topButtons.Controls.Add(createButton);topButtons.Controls.Add(migrateButton);raidOnlyControls.AddRange([createButton,migrateButton]);
cacheEnableButton=B("启用所选逻辑盘缓存", async(_,_)=>await SetLogicalCache(true));topButtons.Controls.Add(cacheEnableButton);
cacheDisableButton=B("禁用所选逻辑盘缓存", async(_,_)=>await SetLogicalCache(false));topButtons.Controls.Add(cacheDisableButton);
var renameButton=B("修改控制器内名称", async(_,_)=>await RenameLogical());var deleteLdButton=B("删除逻辑盘", async(_,_)=>await DeleteLogical(), danger:true);topButtons.Controls.Add(renameButton);topButtons.Controls.Add(deleteLdButton);raidOnlyControls.AddRange([cacheEnableButton,cacheDisableButton,renameButton,deleteLdButton]);
var top=ActionTable(topButtons,logical);
var arrButtons=Flow(wrap:true);arrButtons.Controls.Add(bypassState);var expandButton=B("向阵列增加硬盘", async(_,_)=>await ExpandArray());arrButtons.Controls.Add(expandButton);
bypassEnableButton=B("启用 SSD I/O Bypass", async(_,_)=>await SetBypass(true));arrButtons.Controls.Add(bypassEnableButton);
bypassDisableButton=B("禁用 SSD I/O Bypass", async(_,_)=>await SetBypass(false));arrButtons.Controls.Add(bypassDisableButton);
var deleteArrayButton=B("删除阵列", async(_,_)=>await DeleteArray(), danger:true);arrButtons.Controls.Add(deleteArrayButton);raidOnlyControls.AddRange([expandButton,bypassEnableButton,bypassDisableButton,deleteArrayButton]);
var bottom=ActionTable(arrButtons,arrays);
split.Panel1.Controls.Add(top); split.Panel2.Controls.Add(bottom); p.Controls.Add(split); return p;
}
static TableLayoutPanel ActionTable(FlowLayoutPanel actions,Control content)
{
var table=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=2};
table.ColumnStyles.Add(new(SizeType.Percent,100));table.RowStyles.Add(new(SizeType.AutoSize));table.RowStyles.Add(new(SizeType.Percent,100));
actions.Dock=DockStyle.Fill;table.Controls.Add(actions,0,0);table.Controls.Add(content,0,1);return table;
}
TabPage SettingsTab()
{
var p=new TabPage("控制器设置"); var f=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,AutoScroll=true,Padding=new(12)};f.ColumnStyles.Add(new(SizeType.Percent,100));
f.Controls.Add(ControllerModeGroup());
f.Controls.Add(Group("电源模式", ("最低功耗", async()=>await SimpleWrite("设置最低功耗模式","SETPOWER",Controller(),"POWERMODE","1","nologs")),
("平衡", async()=>await SimpleWrite("设置平衡模式","SETPOWER",Controller(),"POWERMODE","2","nologs")),
("最高性能", async()=>await SimpleWrite("设置最高性能模式","SETPOWER",Controller(),"POWERMODE","3","nologs"))));
var priorityGroup=Group("后台任务优先级", ("重建:低", async()=>await SimpleWrite("设置重建优先级","SETPRIORITY",Controller(),"REBUILD","LOW","nologs")),
("重建:高", async()=>await SimpleWrite("设置重建优先级","SETPRIORITY",Controller(),"REBUILD","HIGH","nologs")),
("扩容:低", async()=>await SimpleWrite("设置扩容优先级","SETPRIORITY",Controller(),"EXPAND","LOW","nologs")),
("扩容:高", async()=>await SimpleWrite("设置扩容优先级","SETPRIORITY",Controller(),"EXPAND","HIGH","nologs")));
var consistencyGroup=Group("一致性检查", ("开启(空闲 3 秒)", async()=>await SimpleWrite("开启后台一致性检查","CONSISTENCYCHECK",Controller(),"ON","3","nologs")),
("关闭", async()=>await SimpleWrite("关闭后台一致性检查","CONSISTENCYCHECK",Controller(),"OFF","nologs")));
var cacheGroup=Group("无电池写缓存(控制器全局,影响全部逻辑盘;断电可能丢数据)", ("全局启用", async()=>await Write("全局启用无电池写缓存",true,"ENABLE","SETCACHE",Controller(),"NOBATTERYWRITECACHE","enable","noprompt")),
("全局禁用", async()=>await Write("全局禁用无电池写缓存",false,null,"SETCACHE",Controller(),"NOBATTERYWRITECACHE","disable","noprompt")));
f.Controls.Add(priorityGroup);f.Controls.Add(consistencyGroup);f.Controls.Add(cacheGroup);raidOnlyControls.AddRange([priorityGroup,consistencyGroup,cacheGroup]);
f.Controls.Add(Group("维护", ("保存支持包", SaveSupport), ("重新扫描", async()=>await SimpleWrite("重新扫描设备","RESCAN",Controller(),"nologs"))));
f.RowCount=f.Controls.Count;
for(var i=0;i<f.RowCount;i++)f.RowStyles.Add(new(SizeType.AutoSize));
foreach(Control c in f.Controls)c.Dock=DockStyle.Top;
p.Controls.Add(f); return p;
}
GroupBox ControllerModeGroup()
{
var g=new GroupBox{Text="控制器工作模式(修改后必须重启)",AutoSize=true,AutoSizeMode=AutoSizeMode.GrowAndShrink,MinimumSize=new(0,120),Padding=new(8),Margin=new(4)};
var root=new TableLayoutPanel{Dock=DockStyle.Top,AutoSize=true,AutoSizeMode=AutoSizeMode.GrowAndShrink,ColumnCount=1,RowCount=2};root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));
controllerModeState.Dock=DockStyle.Fill;controllerModeState.AutoSize=true;controllerModeState.Padding=new(6,4,6,4);
var flow=Flow();flow.Dock=DockStyle.Top;flow.AutoSize=true;hbaModeButton=B("纯 HBA(全部 RAW)",async(_,_)=>await SetControllerMode(2,"HBA"));raidModeButton=B("RAID:隐藏 RAW",async(_,_)=>await SetControllerMode(3,"RAID_HIDE_RAW"));mixedModeButton=B("Mixed:RAID + RAW",async(_,_)=>await SetControllerMode(5,"MIXED"));
flow.Controls.AddRange([hbaModeButton,raidModeButton,mixedModeButton]);root.Controls.Add(controllerModeState,0,0);root.Controls.Add(flow,0,1);g.Controls.Add(root);return g;
}
TabPage ConsoleTab()
{
var p=new TabPage("输出与高级命令");outputPage=p;var top=new TableLayoutPanel{Dock=DockStyle.Fill,AutoSize=false,MinimumSize=new(0,60),ColumnCount=3,Padding=new(5)};
top.ColumnStyles.Add(new(SizeType.Percent,100));top.ColumnStyles.Add(new(SizeType.AutoSize));top.ColumnStyles.Add(new(SizeType.AutoSize));
var input=new TextBox{Dock=DockStyle.Fill,PlaceholderText="输入 arcconf 后面的参数;也可点击上方常用原始命令"};
var run=B("执行",async(_,_)=>{ var a=SplitArgs(input.Text); if(a.Length>0) await Write("执行高级命令",true,"RUN",a); });
var clear=B("清空输出",(_,_)=>{output.Clear();return Task.CompletedTask;});
top.Controls.Add(input,0,0);top.Controls.Add(run,1,0);top.Controls.Add(clear,2,0);
var suggestions=Flow();suggestions.AutoSize=true;suggestions.WrapContents=true;suggestions.FlowDirection=FlowDirection.LeftToRight;suggestions.Dock=DockStyle.Fill;suggestions.Controls.Add(new Label{Text="常用原始命令:",AutoSize=true,Padding=new(6,10,4,0),ForeColor=Color.DimGray});
suggestions.Controls.Add(B("GETCONFIG 1",async(_,_)=>await ShowRead("完整配置原文","GETCONFIG",Controller(),"nologs")));
suggestions.Controls.Add(B("GETVERSION",async(_,_)=>await ShowRead("版本原文","GETVERSION")));
suggestions.Controls.Add(B("GETSMARTSTATS 1 tabular",async(_,_)=>await ShowRead("SMART 原文","GETSMARTSTATS",Controller(),"tabular","nologs")));
suggestions.Controls.Add(B("GETSTATUS 1",async(_,_)=>await ShowRead("任务状态原文","GETSTATUS",Controller(),"nologs")));
suggestions.Controls.Add(B("GETLOGS 1 EVENT tabular",async(_,_)=>await ShowRead("事件日志原文","GETLOGS",Controller(),"EVENT","tabular","nologs")));
suggestions.Controls.Add(B("打开应用日志目录",(_,_)=>{OpenLogFolder();return Task.CompletedTask;}));
var root=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=3};root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.Percent,100));root.Controls.Add(suggestions,0,0);root.Controls.Add(top,0,1);root.Controls.Add(output,0,2);p.Controls.Add(root);return p;
}
TabPage HelpTab()
{
var p=new TabPage("操作说明"); var t=new TextBox{Dock=DockStyle.Fill,Multiline=true,ReadOnly=true,ScrollBars=ScrollBars.Vertical,Font=new("Segoe UI",10),Text=
@"常用流程
1. 创建 RAID:在“阵列与逻辑盘”中点击“创建 RAID”,勾选 Ready/Unassigned 的物理盘,选择 RAID、容量、条带和初始化方式。
2. 在线 RAID 迁移/扩容:选择逻辑盘,点击“在线迁移/扩容”,设置目标 RAID 和最终成员盘。该操作可能持续数小时,期间不要关机或拔盘。
3. 增加阵列成员:选择阵列,点击“向阵列增加硬盘”。扩容后若还需扩大逻辑盘容量,再运行在线迁移/扩容。
4. 热备盘:选择 Ready 物理盘,可设为全局或指定阵列的专用热备。
5. 删除和安全擦除不可恢复,必须输入确认词。
安全提示
• 在线迁移前仍应备份重要数据。
• 不要在重建、迁移或一致性检查时强制关机。
• “逻辑盘缓存”只控制选中的逻辑盘;“无电池写缓存”是控制器级全局许可,影响全部逻辑盘。
• 全局允许“无电池写缓存”后,意外断电可能造成文件系统或阵列数据损坏。
• SSD I/O Bypass 是纯 SSD 阵列的阵列级设置,会影响该阵列中的全部逻辑盘;它不是普通缓存开关。
• GUI 会显示并记录执行的完整 ARCCONF 命令。"}; p.Controls.Add(t); return p;
}
TabPage AboutTab()
{
const string url="https://github.com/soxmonitor";
var p=new TabPage("关于与更新");
var root=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=4,Padding=new(22),AutoScroll=true};
root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.Percent,100));root.RowStyles.Add(new(SizeType.AutoSize));
var header=new TableLayoutPanel{Dock=DockStyle.Fill,AutoSize=true,ColumnCount=2,RowCount=1};header.ColumnStyles.Add(new(SizeType.Absolute,360));header.ColumnStyles.Add(new(SizeType.Percent,100));
var logo=new PictureBox{Image=brandLogo,SizeMode=PictureBoxSizeMode.Zoom,Dock=DockStyle.Fill,Height=76,Margin=new(0,0,18,8)};
var version=Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)??"1.3.7";
var heading=new Label{Text=$"ARCCONF GUI\r\n版本 {version}",Dock=DockStyle.Fill,AutoSize=true,Font=new("Segoe UI Semibold",16),ForeColor=Color.FromArgb(35,65,95),Padding=new(8,4,4,4)};
header.Controls.Add(logo,0,0);header.Controls.Add(heading,1,0);
var author=new Label{Text="由 飯野龙马 制作",Dock=DockStyle.Fill,AutoSize=true,Font=new("Segoe UI Semibold",12),Padding=new(4,12,4,8)};
var notes=new RichTextBox{Dock=DockStyle.Fill,ReadOnly=true,BackColor=Color.White,BorderStyle=BorderStyle.FixedSingle,Font=new("Segoe UI",10.5f),Text=
$@"版本 {version} 更新说明(2026-08-07)
• 概览页增加手动刷新按钮,程序每 5 分钟自动刷新控制器信息。
• 阵列与逻辑盘页实时显示缓存及 SSD I/O Bypass 状态,并自动禁用重复操作。
• 完善 RAID 创建、迁移、缓存、热备、电源和维护操作的说明与风险提示。
• 改进物理盘详情、SMART/版本/完整配置的结构化展示和 DPI 自适应布局。
• 修复 CREATE 同时发送 LDCACHE 与 SSDIOBYPASS 导致命令中止的问题。
• 创建阵列时检测 HDD/SSD 和 SAS/SATA;包含 HDD 时禁用 Bypass,混合介质仅提示而不阻止。
• 新增持久化应用日志,记录命令、退出码与完整输出。
• 针对本机 ARCCONF 7.27 固件兼容问题,在唯一命令执行入口统一移除可导致有效命令被拒绝的 NOLOGS 参数。
• 创建后先读取实际名称、缓存和 Bypass 状态;目标状态已经满足时不再重复设置或误报失败。
• 物理盘表格新增 HP LED 健康指示:绿色健康、橙色警告、红色故障,并提供悬停原因。
• 新增 256 区块可视化全盘顺序读取检测,支持多个并行 Job、取消、慢速/重试/错误定位和 CSV 报告。
• RAW 暴露盘直接只读;阵列成员自动改读对应逻辑盘;隐藏且未组阵列的盘不会擅自改模式或创建临时 RAID。
• SSD“56 天寿命”字段必须得到有效寿命天数或其他 SMART/PFA 告警佐证,过滤部分 SAS SSD 的固件默认值误报。
• 不再把固件 JSON 中不可靠的 pfaEnabled=false 当作 SMART 未启用;健康状态以实际 SMART/PFA 错误及错误计数为准。
应用日志:%LOCALAPPDATA%\ARCCONF GUI\ARCCONF GUI.log
项目说明
这是一个调用本机 Microchip ARCCONF 的独立第三方图形前端。它并非 Microchip Technology Inc. 官方产品,也未获得其赞助、认可或背书。
Microchip、Adaptec 及其标志是 Microchip Technology Inc. 或其关联公司的商标或注册商标。"};
var link=new LinkLabel{Text="访问 github.com/soxmonitor,查看飯野龙马的其他作品",AutoSize=true,Font=new("Segoe UI Semibold",11),Padding=new(4,12,4,4),LinkColor=Color.FromArgb(20,90,170)};
link.LinkClicked+=(_,_)=>{try{Process.Start(new ProcessStartInfo(url){UseShellExecute=true});}catch(Exception ex){Error(ex.Message);}};
root.Controls.Add(header,0,0);root.Controls.Add(author,0,1);root.Controls.Add(notes,0,2);root.Controls.Add(link,0,3);p.Controls.Add(root);return p;
}
async Task RefreshAll(bool automatic=false)
{
if (busy) return; Busy(true,"正在读取控制器配置...");
var temp=System.IO.Path.Combine(System.IO.Path.GetTempPath(),$"arcconf-{Guid.NewGuid():N}.json");
try {
var r=await client.RunAsync("GETCONFIGJSON","1",temp,"nologs"); Log(r);
if(!File.Exists(temp)) throw new InvalidDataException(r.Output);
snapshot=Snapshot.Parse(await File.ReadAllTextAsync(temp));
summary.Text=$"控制器:{snapshot.ControllerName}\r\n模式:{snapshot.ControllerMode} RAW:{snapshot.RawExposure}\r\n主机启动:{snapshot.HostBootMode} 连接器:{snapshot.ConnectorModes}\r\n固件 / 驱动:{snapshot.Firmware} / {snapshot.Driver}\r\n温度:{snapshot.Temperature} 电源模式:{snapshot.PowerMode}\r\n\r\n物理盘:{snapshot.Drives.Count} 阵列:{snapshot.Arrays.Count} 逻辑盘:{snapshot.LogicalDrives.Count}";
drives.DataSource=snapshot.Drives; logical.DataSource=snapshot.LogicalDrives; arrays.DataSource=snapshot.Arrays;
UpdateStorageActionStates();
ShowSnapshotOverview();
status.Text=$"{(automatic?"已自动刷新":"已刷新")}:{DateTime.Now:G} 控制器 {snapshot.ControllerId} {snapshot.ControllerName}";
} catch(Exception ex){if(automatic){status.Text=$"自动刷新失败:{ex.Message};将在 5 分钟后重试";AppendLog($"自动刷新失败\r\n{ex}");}else Error(ex.Message);} finally { try{File.Delete(temp);}catch{} Busy(false); }
}
async Task ShowRead(string title, params string[] args){if(outputPage!=null){tabs.SelectedTab=outputPage;output.Focus();}Busy(true,$"正在读取{title}...");try{var r=await client.RunAsync(args);Log(r);}catch(Exception e){Error(e.Message);}finally{Busy(false);} }
async Task ShowStructured(string title,params string[] args)
{
Busy(true,$"正在读取并整理{title}...");
try{var r=await client.RunAsync(args);Log(r);overviewTitle.Text=title;overviewGrid.DataSource=args[0].Equals("GETSMARTSTATS",StringComparison.OrdinalIgnoreCase)?ParseSmart(r.Output):ParseKeyValues(r.Output,title);}
catch(Exception e){Error(e.Message);}finally{Busy(false);}
}
public void LoadStructuredForTest(string title,string text,bool smart){overviewTitle.Text=title;overviewGrid.DataSource=smart?ParseSmart(text):ParseKeyValues(text,title);tabs.SelectedIndex=0;summary.Text="结构化解析预览";}
public void LoadDrivesForTest()
{
drives.DataSource=new List<PhysicalDrive>{
new(0,4,5,"Online","Data","IBM HUSMM8020ASS20","2KVHGBGA","SAS","186.33 GiB","38 °C","0",false,"Sample","{}",true,"Green","SMART/PFA 与错误计数正常"),
new(0,5,6,"Ready","Unassigned","WDC WD5000AAKX-0","WD-WMAYUM262366","SATA","465.76 GiB","42 °C","",true,"Sample","{}",false,"Green","SMART/PFA、控制器状态和累计错误计数未见异常"),
new(0,6,7,"Failed","Data","SAMPLE FAILED DRIVE","FAILED001","SAS","1.82 TiB","55 °C","1",false,"Sample","{}",false,"Red","控制器已判定 Failed")};
}
public void LoadStorageForTest()
{
logical.DataSource=new List<LogicalDrive>{new(0,0,"Hitachi 200G SSD SAS","RAID 0","Optimal","186.3 GiB",190747,256,"禁用","E:\\")};
arrays.DataSource=new List<ArrayView>{new(0,"A","Optimal","SAS SSD","186.3 GiB","2.24 MiB","0:4","启用")};
UpdateStorageActionStates();
}
async Task SimpleWrite(string description, params string[] args)=>await Write(description,false,null,args);
async Task Write(string description,bool critical,string? word,params string[] args)
{
args=ArcConfClient.NormalizeArguments(args);
var command="arcconf "+string.Join(" ",args);
var info=OperationCatalog.Explain(description,args,critical);
using(var dialog=new OperationInfoForm(info,command))if(dialog.ShowDialog(this)!=DialogResult.OK)return;
if(critical && !ConfirmWord(word??"CONFIRM",description))return;
Busy(true,$"正在执行:{description}");
try{var r=await client.RunAsync(args);Log(r);var resultText=r.Success?r.Output:$"执行命令:\r\n{r.Command}\r\n\r\n返回内容:\r\n{r.Output}";MessageBox.Show(resultText.Length>1800?resultText[..1800]+"…":resultText, r.Success?"操作完成":"操作返回错误",MessageBoxButtons.OK,r.Success?MessageBoxIcon.Information:MessageBoxIcon.Error);Busy(false);await RefreshAll();}
catch(Exception e){Error(e.Message);}finally{Busy(false);}
}
async Task Identify(bool start){var d=SelectedDrive();if(d==null)return;if(start)await SimpleWrite("启动物理盘定位灯","IDENTIFY",Controller(),"DEVICE",d.Channel.ToString(),d.Id.ToString(),"TIME","30","nologs");else await SimpleWrite("停止物理盘定位灯","IDENTIFY",Controller(),"DEVICE",d.Channel.ToString(),d.Id.ToString(),"STOP","nologs");}
async Task Spare(bool dedicated){var d=SelectedDrive();if(d==null)return;var a=dedicated?Prompt("阵列 ID","输入要绑定的阵列 ID:",snapshot?.Arrays.FirstOrDefault()?.Id.ToString()??"0"):null;if(dedicated&&string.IsNullOrWhiteSpace(a))return;var args=new List<string>{"SETSTATE",Controller(),"DEVICE",d.Channel.ToString(),d.Id.ToString(),"HSP"};if(dedicated)args.AddRange(["ARRAY",a!,"SPARETYPE","1"]);args.AddRange(["noprompt","nologs"]);await Write(dedicated?"设置专用热备盘":"设置全局热备盘",false,null,args.ToArray());}
async Task SetDriveState(string state,string desc){var d=SelectedDrive();if(d!=null)await Write(desc,false,null,"SETSTATE",Controller(),"DEVICE",d.Channel.ToString(),d.Id.ToString(),state,"noprompt","nologs");}
async Task SecureErase(){var d=SelectedDrive();if(d!=null)await Write($"安全擦除物理盘 {d.Key}(数据不可恢复)",true,"ERASE","TASK","START",Controller(),"DEVICE",d.Channel.ToString(),d.Id.ToString(),"secureerase","PATTERN","1","noprompt","nologs");}
async Task SetControllerMode(int mode,string confirmWord)=>await Write($"切换控制器模式为 {(mode==2?"纯 HBA":mode==3?"RAID(隐藏 RAW)":"Mixed(RAID + RAW)")};需要手动重启后生效",true,confirmWord,"SETCONTROLLERMODE",Controller(),mode.ToString(),"nologs");
async Task CreateRaid()
{
if(snapshot==null)return; using var dlg=new RaidDialog("创建 RAID",snapshot.Drives.Where(x=>x.IsReady).ToList(),null);
if(dlg.ShowDialog(this)!=DialogResult.OK)return;
var beforeLd=snapshot.LogicalDrives.Select(x=>x.Id).ToHashSet();var beforeArrays=snapshot.Arrays.Select(x=>x.Id).ToHashSet();
// Keep CREATE itself minimal. Several 3100-series firmware builds reject
// otherwise documented NAME/LDCACHE combinations during CREATE. Apply
// those settings to the newly created objects in separate commands.
var singleDiskRaid0=dlg.Raid=="0"&&dlg.SelectedDrives.Count==1;
List<string> a;
if(singleDiskRaid0){var d=dlg.SelectedDrives[0];a=["CREATE",Controller(),"RAIDZEROARRAY",d.Channel.ToString(),d.Id.ToString(),"noprompt"];}
else{
a=["CREATE",Controller(),"LOGICALDRIVE"];
if(dlg.Stripe!="256")a.AddRange(["stripesize",dlg.Stripe]);
a.AddRange(dlg.MethodArguments);a.AddRange([dlg.VolumeSize,dlg.Raid]);
foreach(var d in dlg.SelectedDrives)a.AddRange([d.Channel.ToString(),d.Id.ToString()]);a.Add("noprompt");
}
var createCommand="arcconf "+string.Join(" ",a.Select(x=>x.Any(char.IsWhiteSpace)?$"\"{x}\"":x));
var plan=new StringBuilder(createCommand);
if(!string.IsNullOrWhiteSpace(dlg.VolumeName))plan.Append($"\r\n成功后:arcconf SETNAME {Controller()} LOGICALDRIVE <新逻辑盘> \"{dlg.VolumeName}\"");
plan.Append($"\r\n成功后:arcconf SETCACHE {Controller()} LOGICALDRIVE <新逻辑盘> {(dlg.UseSsdBypass?"coff":dlg.Cache=="LON"?"con":"coff")}");
if(dlg.AllSelectedSsd)plan.Append($"\r\n成功后:arcconf SETARRAYPARAM {Controller()} <新阵列> SSDIOBYPASS {(dlg.UseSsdBypass?"enable":"disable")}");
var info=OperationCatalog.Explain("创建 RAID 逻辑盘",a.ToArray(),true);
using(var confirm=new OperationInfoForm(info,plan.ToString()))if(confirm.ShowDialog(this)!=DialogResult.OK)return;
if(!ConfirmWord("CREATE","创建 RAID 逻辑盘"))return;
Busy(true,"正在创建 RAID...");
try{
var results=new List<CommandResult>();var created=await client.RunAsync(a.ToArray());results.Add(created);Log(created);
if(!created.Success){ShowCommandResults(results,"创建失败");return;}
Busy(false);await RefreshAll();
var newLd=snapshot?.LogicalDrives.FirstOrDefault(x=>!beforeLd.Contains(x.Id));var newArray=snapshot?.Arrays.FirstOrDefault(x=>!beforeArrays.Contains(x.Id));
if(newLd==null){MessageBox.Show("控制器已报告创建成功,但刷新后没有识别到新的逻辑盘,因此没有自动应用名称和缓存设置。请刷新后在逻辑盘列表中手动设置。","创建成功,后续设置未应用",MessageBoxButtons.OK,MessageBoxIcon.Warning);return;}
Busy(true,"正在应用新逻辑盘设置...");
if(!string.IsNullOrWhiteSpace(dlg.VolumeName)){var r=await client.RunAsync("SETNAME",Controller(),"LOGICALDRIVE",newLd.Id.ToString(),dlg.VolumeName,"nologs");results.Add(r);Log(r);}
var desiredCacheOn=!dlg.UseSsdBypass&&dlg.Cache=="LON";var cacheMode=desiredCacheOn?"con":"coff";
if((newLd.Cache=="启用")!=desiredCacheOn){var cacheResult=await client.RunAsync("SETCACHE",Controller(),"LOGICALDRIVE",newLd.Id.ToString(),cacheMode,"noprompt");results.Add(cacheResult);Log(cacheResult);}
if(dlg.AllSelectedSsd&&newArray!=null){var r=await client.RunAsync("SETARRAYPARAM",Controller(),newArray.Id.ToString(),"SSDIOBYPASS",dlg.UseSsdBypass?"enable":"disable","nologs");results.Add(r);Log(r);}
ShowCommandResults(results,results.All(x=>x.Success)?"创建及设置完成":"阵列已创建,但部分后续设置失败");
}catch(Exception e){Error(e.Message);}finally{Busy(false);await RefreshAll();}
}
void ShowCommandResults(IEnumerable<CommandResult> results,string title)
{
var list=results.ToList();var ok=list.All(x=>x.Success);var text=string.Join("\r\n\r\n",list.Select(x=>$"> {x.Command}\r\n退出码:{x.ExitCode}\r\n{x.Output}"));
MessageBox.Show(text.Length>2600?text[..2600]+"…":text,title,MessageBoxButtons.OK,ok?MessageBoxIcon.Information:MessageBoxIcon.Error);
}
async Task MigrateLogical()
{
var ld=SelectedLogical();if(ld==null||snapshot==null)return;using var dlg=new RaidDialog("在线 RAID 迁移 / 容量扩展",snapshot.Drives,ld);
if(dlg.ShowDialog(this)!=DialogResult.OK)return;var a=new List<string>{"MODIFY",Controller(),"FROM",ld.Id.ToString(),"TO","stripesize",dlg.Stripe,dlg.VolumeSize,dlg.Raid};
foreach(var d in dlg.SelectedDrives)a.AddRange([d.Channel.ToString(),d.Id.ToString()]);a.AddRange(["noprompt","nologs"]);
await Write($"在线迁移逻辑盘 {ld.Id}:{ld.Raid} → RAID {dlg.Raid}",true,"MIGRATE",a.ToArray());
}
async Task ExpandArray(){var ar=SelectedArray();if(ar==null||snapshot==null)return;using var d=new DrivePicker("选择要加入阵列的 Ready 物理盘",snapshot.Drives.Where(x=>x.IsReady).ToList());if(d.ShowDialog(this)!=DialogResult.OK)return;var a=new List<string>{"MODIFY",Controller(),"ARRAY",ar.Id.ToString(),"EXPAND"};foreach(var x in d.Selected)a.AddRange([x.Channel.ToString(),x.Id.ToString()]);a.Add("nologs");await Write($"扩展阵列 {ar.Id}",true,"EXPAND",a.ToArray());}
async Task SetLogicalCache(bool enable){var ld=SelectedLogical();if(ld==null)return;if((ld.Cache=="启用")==enable){MessageBox.Show("当前逻辑盘已经处于所选缓存状态。","无需重复设置");return;}await Write($"{(enable?"启用":"禁用")}逻辑盘 {ld.Id} 的控制器缓存",false,null,"SETCACHE",Controller(),"LOGICALDRIVE",ld.Id.ToString(),enable?"con":"coff","noprompt");}
async Task SetBypass(bool enable){var a=SelectedArray();if(a==null)return;if((a.Bypass=="启用")==enable){MessageBox.Show("当前阵列已经处于所选 SSD I/O Bypass 状态。","无需重复设置");return;}await Write($"{(enable?"启用":"禁用")}阵列 {a.Id} 的 SSD I/O Bypass",false,null,"SETARRAYPARAM",Controller(),a.Id.ToString(),"SSDIOBYPASS",enable?"enable":"disable","nologs");}
async Task RenameLogical()
{
var ld=SelectedLogical();if(ld==null)return;
var n=Prompt("修改控制器内逻辑盘名称",
"此名称保存在 RAID 控制器中,不会修改 Windows 资源管理器里的卷标。\r\n\r\n请输入 1–64 个 ASCII 字符(不能使用中文):",ld.Name);
if(n==null)return;n=n.Trim();
if(n.Length is < 1 or > 64){MessageBox.Show("名称长度必须为 1–64 个字符。","名称无效",MessageBoxButtons.OK,MessageBoxIcon.Warning);return;}
if(n.Any(c=>c<32||c>126)){MessageBox.Show("ARCCONF 只接受 ASCII 名称,不能包含中文、全角标点或控制字符。\r\n可使用英文字母、数字、空格和半角符号。","名称无效",MessageBoxButtons.OK,MessageBoxIcon.Warning);return;}
if(string.Equals(n,ld.Name,StringComparison.Ordinal)){MessageBox.Show("新名称与当前控制器名称相同,没有需要修改的内容。","名称未变化",MessageBoxButtons.OK,MessageBoxIcon.Information);return;}
await Write($"修改逻辑盘 {ld.Id} 的控制器内名称",false,null,"SETNAME",Controller(),"LOGICALDRIVE",ld.Id.ToString(),n,"nologs");
}
async Task DeleteLogical(){var ld=SelectedLogical();if(ld!=null)await Write($"删除逻辑盘 {ld.Id}(数据不可恢复)",true,"DELETE","DELETE",Controller(),"LOGICALDRIVE",ld.Id.ToString(),"noprompt","nologs");}
async Task DeleteArray(){var a=SelectedArray();if(a!=null)await Write($"删除阵列 {a.Id}(数据不可恢复)",true,"DELETE","DELETE",Controller(),"ARRAY",a.Id.ToString(),"noprompt","nologs");}
async Task SaveSupport(){using var f=new FolderBrowserDialog{Description="选择支持包保存目录"};if(f.ShowDialog(this)==DialogResult.OK)await SimpleWrite("保存支持包","SAVESUPPORTARCHIVE",f.SelectedPath,"Arcconf","nologs");}
async Task OpenHealthScan()
{
if(snapshot==null){MessageBox.Show("请先刷新控制器信息。","磁盘健康检测",MessageBoxButtons.OK,MessageBoxIcon.Information);return;}
var selected=drives.SelectedRows.Cast<DataGridViewRow>().Select(x=>x.DataBoundItem as PhysicalDrive).Where(x=>x!=null).Cast<PhysicalDrive>().ToList();
if(selected.Count==0&&drives.CurrentRow?.DataBoundItem is PhysicalDrive current)selected.Add(current);
using var setup=new HealthScanSetupForm(snapshot,selected.Select(x=>x.Key));
if(setup.ShowDialog(this)!=DialogResult.OK)return;
using var scan=new DiskHealthScanForm(setup.Targets,setup.MaxParallelJobs);
scan.ShowDialog(this);
foreach(var job in scan.Jobs)
foreach(var key in job.Target.PhysicalKeys)
{
if(job.ReadErrors>0)scanHealthOverrides[key]=("Red",$"全盘读取发现 {job.ReadErrors} 个读取错误;首个位置 {DiskScanFormatting.Offset(job.FirstErrorOffset)}");
else if(job.State==DiskScanState.Completed&&job.SlowRegions>0)scanHealthOverrides[key]=("Orange",$"全盘读取完成,但发现 {job.SlowRegions} 个明显慢速区域");
}
await RefreshAll();drives.Invalidate();
}
void DriveCellFormatting(object? sender,DataGridViewCellFormattingEventArgs e)
{
if(e.RowIndex<0||drives.Columns[e.ColumnIndex].Name!="HealthLed"||drives.Rows[e.RowIndex].DataBoundItem is not PhysicalDrive drive)return;
var health=scanHealthOverrides.TryGetValue(drive.Key,out var scanned)?scanned:(Level:drive.HealthLevel,Reason:drive.HealthReason);
e.Value="■";e.CellStyle.ForeColor=health.Level switch{"Red"=>Color.Firebrick,"Orange"=>Color.DarkOrange,_=>Color.ForestGreen};
e.CellStyle.SelectionForeColor=e.CellStyle.ForeColor;e.FormattingApplied=true;
}
void DriveCellToolTipTextNeeded(object? sender,DataGridViewCellToolTipTextNeededEventArgs e)
{
if(e.RowIndex<0||e.ColumnIndex<0||drives.Columns[e.ColumnIndex].Name!="HealthLed"||drives.Rows[e.RowIndex].DataBoundItem is not PhysicalDrive drive)return;
var health=scanHealthOverrides.TryGetValue(drive.Key,out var scanned)?scanned:(Level:drive.HealthLevel,Reason:drive.HealthReason);
e.ToolTipText=$"{(health.Level=="Green"?"健康":health.Level=="Orange"?"警告":"故障")}:{health.Reason}";
}
PhysicalDrive? SelectedDrive()=>Selected<PhysicalDrive>(drives,"请先选择一个物理盘。");
LogicalDrive? SelectedLogical()=>Selected<LogicalDrive>(logical,"请先选择一个逻辑盘。");
ArrayView? SelectedArray()=>Selected<ArrayView>(arrays,"请先选择一个阵列。");
T? Selected<T>(DataGridView g,string message) where T:class {if(g.CurrentRow?.DataBoundItem is T t)return t;MessageBox.Show(message,"需要选择",MessageBoxButtons.OK,MessageBoxIcon.Information);return null;}
string Controller()=>(snapshot?.ControllerId??1).ToString(CultureInfo.InvariantCulture);
void Log(CommandResult r){var entry=$"> {r.Command}\r\n退出码:{r.ExitCode}\r\n{r.Output}";output.AppendText($"\r\n[{DateTime.Now:G}] {entry}\r\n");AppendLog(entry);status.Text=$"退出码 {r.ExitCode}:{r.Command}";}
void Busy(bool value,string? text=null){busy=value;UseWaitCursor=value;if(text!=null)status.Text=text;}
void Error(string message){AppendLog("GUI 错误\r\n"+message);MessageBox.Show(message,"ARCCONF GUI 错误",MessageBoxButtons.OK,MessageBoxIcon.Error);status.Text="错误:"+message;}
void AppendLog(string text){try{var dir=System.IO.Path.GetDirectoryName(appLogPath)!;Directory.CreateDirectory(dir);File.AppendAllText(appLogPath,$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {text}\r\n\r\n",Encoding.UTF8);}catch{}}
void OpenLogFolder(){try{var dir=System.IO.Path.GetDirectoryName(appLogPath)!;Directory.CreateDirectory(dir);var psi=new ProcessStartInfo("explorer.exe"){UseShellExecute=true};psi.ArgumentList.Add(dir);Process.Start(psi);}catch(Exception ex){Error(ex.Message);}}
void ConfigureDriveGrid()
{
drives.AutoGenerateColumns=false;
drives.Columns.Clear();
void AddFill(string title,string property,float fillWeight,int minimumWidth)=>drives.Columns.Add(new DataGridViewTextBoxColumn{
HeaderText=title,DataPropertyName=property,MinimumWidth=minimumWidth,FillWeight=fillWeight,
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill,SortMode=DataGridViewColumnSortMode.NotSortable});
drives.Columns.Add(new DataGridViewTextBoxColumn{Name="HealthLed",HeaderText="HP LED",DataPropertyName=nameof(PhysicalDrive.HealthGlyph),MinimumWidth=130,Width=130,
AutoSizeMode=DataGridViewAutoSizeColumnMode.None,SortMode=DataGridViewColumnSortMode.NotSortable,
DefaultCellStyle=new DataGridViewCellStyle{Alignment=DataGridViewContentAlignment.MiddleCenter,Font=new Font("Segoe UI Symbol",13,FontStyle.Bold)}});
drives.Columns[0].HeaderCell.ToolTipText="Health/Predictive LED:绿色=健康,橙色=警告或 SMART 不可确认,红色=不可用或已确认故障";
AddFill("通道",nameof(PhysicalDrive.Channel),62,64);AddFill("设备",nameof(PhysicalDrive.Id),62,64);AddFill("槽位",nameof(PhysicalDrive.Slot),62,64);
AddFill("状态",nameof(PhysicalDrive.State),70,66);AddFill("用途",nameof(PhysicalDrive.Config),72,68);AddFill("型号",nameof(PhysicalDrive.Model),145,120);
AddFill("容量",nameof(PhysicalDrive.Size),82,75);AddFill("接口",nameof(PhysicalDrive.Interface),62,60);AddFill("温度",nameof(PhysicalDrive.Temperature),65,62);
AddFill("所属阵列",nameof(PhysicalDrive.Arrays),112,120);AddFill("序列号",nameof(PhysicalDrive.Serial),115,100);
drives.AutoSizeColumnsMode=DataGridViewAutoSizeColumnsMode.None;
drives.RowHeadersVisible=false;
drives.ColumnHeadersHeightSizeMode=DataGridViewColumnHeadersHeightSizeMode.AutoSize;
drives.ColumnHeadersDefaultCellStyle.WrapMode=DataGridViewTriState.False;
}
void ConfigureStorageGrids()
{
static void Add(DataGridView grid,string title,string property,float weight,int minimum=70)=>grid.Columns.Add(new DataGridViewTextBoxColumn{
HeaderText=title,DataPropertyName=property,FillWeight=weight,MinimumWidth=minimum,
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill,SortMode=DataGridViewColumnSortMode.NotSortable});
logical.AutoGenerateColumns=false;logical.Columns.Clear();logical.RowHeadersVisible=false;
Add(logical,"逻辑盘",nameof(LogicalDrive.Id),55,65);Add(logical,"阵列",nameof(LogicalDrive.ArrayId),55,65);Add(logical,"控制器内名称",nameof(LogicalDrive.Name),160,150);
Add(logical,"RAID",nameof(LogicalDrive.Raid),75);Add(logical,"状态",nameof(LogicalDrive.State),80);Add(logical,"容量",nameof(LogicalDrive.Size),90);
Add(logical,"条带 KB",nameof(LogicalDrive.StripeKb),75);Add(logical,"控制器缓存",nameof(LogicalDrive.Cache),100,95);Add(logical,"Windows 挂载点",nameof(LogicalDrive.MountPoint),125,120);
arrays.AutoGenerateColumns=false;arrays.Columns.Clear();arrays.RowHeadersVisible=false;
Add(arrays,"阵列",nameof(ArrayView.Id),55,65);Add(arrays,"名称",nameof(ArrayView.Name),70);Add(arrays,"状态",nameof(ArrayView.Status),85);
Add(arrays,"介质/接口",nameof(ArrayView.Interface),100,95);Add(arrays,"容量",nameof(ArrayView.Size),95);Add(arrays,"空闲",nameof(ArrayView.Free),85);
Add(arrays,"成员盘",nameof(ArrayView.Members),110,100);Add(arrays,"SSD I/O Bypass",nameof(ArrayView.Bypass),130,125);
}
void UpdateStorageActionStates()
{
var raidAvailable=snapshot?.RaidFunctionsAvailable!=false;
foreach(var control in raidOnlyControls)control.Enabled=raidAvailable;
if(snapshot!=null){
var change=snapshot.ModeChangeSupported;
controllerModeState.Text=$"当前:{snapshot.ControllerMode};RAW:{snapshot.RawExposure}。"+(change?"可提交模式变更,重启后生效。":"当前配置不允许切换(通常需先删除/迁出全部 RAID 逻辑盘与阵列)。");
controllerModeState.ForeColor=change?Color.DarkGreen:Color.DarkOrange;
if(hbaModeButton!=null)hbaModeButton.Enabled=change&&snapshot.FunctionalMode!=2;
if(raidModeButton!=null)raidModeButton.Enabled=change&&snapshot.FunctionalMode!=3;
if(mixedModeButton!=null)mixedModeButton.Enabled=change&&snapshot.FunctionalMode!=5;
}
var ld=logical.CurrentRow?.DataBoundItem as LogicalDrive;var cacheOn=ld?.Cache=="启用";
logicalCacheState.Text=ld==null?"逻辑盘缓存:请选择逻辑盘":$"逻辑盘 {ld.Id} 缓存:{ld.Cache}";
logicalCacheState.ForeColor=ld==null?Color.DimGray:cacheOn?Color.DarkGreen:Color.DarkOrange;
if(cacheEnableButton!=null)cacheEnableButton.Enabled=raidAvailable&&ld!=null&&!cacheOn;
if(cacheDisableButton!=null)cacheDisableButton.Enabled=raidAvailable&&ld!=null&&cacheOn;
var array=arrays.CurrentRow?.DataBoundItem as ArrayView;var bypassOn=array?.Bypass=="启用";
var isSsd=array?.Interface.Contains("SSD",StringComparison.OrdinalIgnoreCase)==true;
bypassState.Text=array==null?"SSD I/O Bypass:请选择阵列":!isSsd?$"阵列 {array.Id}:不适用(非纯 SSD 阵列)":$"阵列 {array.Id} SSD I/O Bypass:{array.Bypass}";
bypassState.ForeColor=array==null||!isSsd?Color.DimGray:bypassOn?Color.DarkGreen:Color.DarkOrange;
if(bypassEnableButton!=null)bypassEnableButton.Enabled=raidAvailable&&array!=null&&isSsd&&!bypassOn;
if(bypassDisableButton!=null)bypassDisableButton.Enabled=raidAvailable&&array!=null&&bypassOn;
}
void ConfigureOverviewGrid()
{
overviewGrid.AutoGenerateColumns=false;overviewGrid.Columns.Clear();
overviewGrid.Columns.Add(new DataGridViewTextBoxColumn{HeaderText="分类",DataPropertyName=nameof(OverviewRow.Category),Width=190,MinimumWidth=100});
overviewGrid.Columns.Add(new DataGridViewTextBoxColumn{HeaderText="项目",DataPropertyName=nameof(OverviewRow.Item),Width=300,MinimumWidth=140});
overviewGrid.Columns.Add(new DataGridViewTextBoxColumn{HeaderText="值",DataPropertyName=nameof(OverviewRow.Value),Width=260,MinimumWidth=100});
overviewGrid.Columns.Add(new DataGridViewTextBoxColumn{HeaderText="补充信息",DataPropertyName=nameof(OverviewRow.Details),AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill,MinimumWidth=180});
overviewGrid.AutoSizeColumnsMode=DataGridViewAutoSizeColumnsMode.None;overviewGrid.AutoSizeRowsMode=DataGridViewAutoSizeRowsMode.AllCells;overviewGrid.DefaultCellStyle.WrapMode=DataGridViewTriState.True;
}
void ShowSnapshotOverview()
{
if(snapshot==null)return;
var rows=new List<OverviewRow>{
new("控制器","型号",snapshot.ControllerName),new("控制器","序列号",snapshot.Serial),new("版本","固件",snapshot.Firmware),new("版本","驱动",snapshot.Driver),
new("模式","控制器工作模式",snapshot.ControllerMode,snapshot.RaidFunctionsAvailable?"硬件 RAID 功能可用":"硬件 RAID 功能已禁用"),new("模式","RAW 物理盘",snapshot.RawExposure),new("模式","连接器",snapshot.ConnectorModes),
new("启动","Windows 主机固件模式",snapshot.HostBootMode,"这是主机本次启动方式,不等同于控制器工作模式"),new("启动","控制器 Legacy Option ROM",snapshot.ControllerBios,$"运行时 BIOS:{(snapshot.RuntimeBiosEnabled?"启用":"禁用")};启动控制器:{(snapshot.BootController?"是":"否")}"),
new("状态","模式切换许可",snapshot.ModeChangeSupported?"允许":"当前不允许",snapshot.ModeChangeSupported?"提交后仍需重启":"已有 RAID 配置时通常会被固件锁定"),new("状态","控制器温度",snapshot.Temperature),new("状态","电源模式",snapshot.PowerMode),new("数量","物理盘",snapshot.Drives.Count.ToString()),new("数量","阵列",snapshot.Arrays.Count.ToString()),new("数量","逻辑盘",snapshot.LogicalDrives.Count.ToString())};
rows.AddRange(snapshot.Arrays.Select(a=>new OverviewRow($"阵列 {a.Id}",$"{a.Name} / {a.Status}",a.Size,$"成员 {a.Members};SSD I/O Bypass {a.Bypass}")));
rows.AddRange(snapshot.LogicalDrives.Select(ld=>new OverviewRow($"逻辑盘 {ld.Id}",$"{ld.Name} / {ld.Raid}",ld.Size,$"状态 {ld.State};缓存 {ld.Cache};挂载 {ld.MountPoint}")));
rows.AddRange(snapshot.Drives.Select(d=>new OverviewRow($"物理盘 {d.Key}",d.Model,d.Size,$"槽位 {d.Slot};{d.State}/{d.Config};{d.Temperature}")));
overviewTitle.Text="控制器关键摘要";overviewGrid.DataSource=rows;
}
static List<OverviewRow> ParseKeyValues(string text,string defaultCategory)
{
var rows=new List<OverviewRow>();var category=defaultCategory;var channel="";var device="";
foreach(var raw in text.Replace("\r","").Split('\n'))
{
var line=raw.Trim();if(string.IsNullOrWhiteSpace(line)||line.All(c=>c is '-' or '='))continue;
if(line.StartsWith("Controllers found",StringComparison.OrdinalIgnoreCase)||line.StartsWith("Command completed",StringComparison.OrdinalIgnoreCase))continue;
if(line.StartsWith("Controller information",StringComparison.OrdinalIgnoreCase)){category="控制器";continue;}
if(line.Equals("Power Settings",StringComparison.OrdinalIgnoreCase)){category="控制器 / 电源";continue;}
if(line.Equals("Cache Properties",StringComparison.OrdinalIgnoreCase)){category="控制器 / 缓存";continue;}
if(line.Equals("RAID Properties",StringComparison.OrdinalIgnoreCase)){category="控制器 / RAID";continue;}
if(line.Contains("Controller Version Information",StringComparison.OrdinalIgnoreCase)){category="控制器 / 版本";continue;}
if(line.Contains("Temperature Sensors Information",StringComparison.OrdinalIgnoreCase)){category="控制器 / 温度传感器";continue;}
if(line.Contains("Connector information",StringComparison.OrdinalIgnoreCase)){category="控制器 / 连接器";continue;}
if(line.Equals("Array Information",StringComparison.OrdinalIgnoreCase)){category="阵列";continue;}
if(Regex.IsMatch(line,@"^Array Number \d+",RegexOptions.IgnoreCase)){category="阵列 "+Regex.Match(line,@"\d+").Value;continue;}
if(line.Contains("Logical device information",StringComparison.OrdinalIgnoreCase)){category="逻辑盘";continue;}
if(Regex.IsMatch(line,@"^Logical Device number \d+",RegexOptions.IgnoreCase)){category="逻辑盘 "+Regex.Match(line,@"\d+").Value;continue;}
if(line.Equals("Physical Device information",StringComparison.OrdinalIgnoreCase)){category="物理盘";continue;}
if(Regex.IsMatch(line,@"^Channel #\d+",RegexOptions.IgnoreCase)){channel=Regex.Match(line,@"\d+").Value;category=$"物理盘 / 通道 {channel}";continue;}
if(Regex.IsMatch(line,@"^Device #\d+",RegexOptions.IgnoreCase)){device=Regex.Match(line,@"\d+").Value;category=$"物理盘 {channel}:{device}";continue;}
if(Regex.IsMatch(line,@"^Controller #\d+",RegexOptions.IgnoreCase)){category=line;continue;}
var match=Regex.Match(line,@"^(?<key>[^:]+?)\s*:\s*(?<value>.*)$");
if(match.Success){var key=match.Groups["key"].Value.Trim();var value=match.Groups["value"].Value.Trim();if(key.Length>0)rows.Add(new(category,key,value));}
}
return rows.Count>0?rows:[new(defaultCategory,"解析结果","未发现可列举的键值字段",text)];
}
static List<OverviewRow> ParseSmart(string text)
{
var rows=new List<OverviewRow>();var protocol="SMART";var channel="?";var id="?";var inAttribute=false;var attribute=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
void Flush()
{
if(attribute.Count==0)return;attribute.TryGetValue("name",out var name);attribute.TryGetValue("id",out var attrId);attribute.TryGetValue("Value",out var direct);attribute.TryGetValue("rawValue",out var raw);
var item=string.Join(" ",new[]{attrId,name}.Where(x=>!string.IsNullOrWhiteSpace(x)));var value=!string.IsNullOrWhiteSpace(direct)?direct:!string.IsNullOrWhiteSpace(raw)?raw:attribute.GetValueOrDefault("normalizedCurrent","");
var details=new List<string>();if(attribute.TryGetValue("normalizedCurrent",out var current))details.Add("当前 "+current);if(attribute.TryGetValue("normalizedWorst",out var worst))details.Add("最差 "+worst);if(attribute.TryGetValue("thresholdValue",out var threshold))details.Add("阈值 "+threshold);if(attribute.TryGetValue("Status",out var statusValue))details.Add("状态 "+statusValue);
rows.Add(new($"{protocol} 物理盘 {channel}:{id}",string.IsNullOrWhiteSpace(item)?"属性":item,value??"",string.Join(";",details)));attribute.Clear();
}
foreach(var rawLine in text.Replace("\r","").Split('\n'))
{
var line=rawLine.Trim();if(line.StartsWith("SMART STATS FOR SATA",StringComparison.OrdinalIgnoreCase)){Flush();protocol="SATA SMART";inAttribute=false;continue;}if(line.StartsWith("SMART STATS FOR SAS",StringComparison.OrdinalIgnoreCase)){Flush();protocol="SAS SMART";inAttribute=false;continue;}
if(line.Equals("PhysicalDriveSmartStats",StringComparison.OrdinalIgnoreCase)){Flush();inAttribute=false;continue;}if(line.Equals("Attribute",StringComparison.OrdinalIgnoreCase)){Flush();inAttribute=true;continue;}
var match=Regex.Match(line,@"^(?<key>.+?)\s*\.{3,}\s*(?<value>.*)$");if(!match.Success)continue;var key=match.Groups["key"].Value.Trim();var value=match.Groups["value"].Value.Trim();
if(inAttribute)attribute[key]=value;else if(key.Equals("channel",StringComparison.OrdinalIgnoreCase))channel=value;else if(key.Equals("id",StringComparison.OrdinalIgnoreCase))id=value;
}
Flush();return rows.Count>0?rows:ParseKeyValues(text,"SMART");
}
static DataGridView Grid()=>new(){Dock=DockStyle.Fill,ReadOnly=true,AllowUserToAddRows=false,AllowUserToDeleteRows=false,AutoGenerateColumns=true,AutoSizeColumnsMode=DataGridViewAutoSizeColumnsMode.DisplayedCells,SelectionMode=DataGridViewSelectionMode.FullRowSelect,MultiSelect=false,BackgroundColor=Color.White};
static Label StateLabel(string text)=>new(){Text=text,AutoSize=true,Padding=new(8,9,12,0),Margin=new(4),ForeColor=Color.DimGray,Font=new("Segoe UI Semibold",9.5f)};
static FlowLayoutPanel Flow(bool wrap=false)=>new(){
Height=48,Padding=new(5),WrapContents=wrap,AutoScroll=!wrap,
AutoSize=wrap,AutoSizeMode=wrap?AutoSizeMode.GrowAndShrink:AutoSizeMode.GrowOnly,
FlowDirection=FlowDirection.LeftToRight};
static ToolStripButton Button(string text,EventHandler click){var b=new ToolStripButton(text);b.Click+=click;return b;}
static Button B(string text,Func<object?,EventArgs,Task> click,bool danger=false){var b=new Button{Text=text,AutoSize=true,Height=32,Margin=new(4),BackColor=danger?Color.MistyRose:SystemColors.Control};b.Click+=async(s,e)=>await click(s,e);return b;}
static GroupBox Group(string title,params (string,Func<Task>)[] actions){var g=new GroupBox{Text=title,AutoSize=true,AutoSizeMode=AutoSizeMode.GrowAndShrink,MinimumSize=new(0,82),Padding=new(8),Margin=new(4)};var f=Flow();f.Dock=DockStyle.Top;f.AutoSize=true;foreach(var (name,act) in actions)f.Controls.Add(B(name,async(_,_)=>await act()));g.Controls.Add(f);return g;}
static bool ConfirmWord(string word,string desc){var v=Prompt("危险操作确认",$"{desc}\r\n\r\n请输入 {word} 继续:","");return string.Equals(v,word,StringComparison.Ordinal);}
static string? Prompt(string title,string text,string value){using var f=new TextPromptForm(title,text,value);return f.ShowDialog()==DialogResult.OK?f.Value:null;}
static string[] SplitArgs(string s){var list=new List<string>();foreach(System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(s,@"[^\s""]+|""([^""]*)"""))list.Add(m.Value.Trim('"'));return list.ToArray();}
}
internal sealed record OperationInfo(string Title,string Meaning,string Effect,string Requirements,string Risk,bool Critical);
internal static class UiSizing
{
public static void FitToScreen(Form form)
{
var area=Screen.FromControl(form).WorkingArea;var margin=32;
form.Width=Math.Min(form.Width,Math.Max(form.MinimumSize.Width,area.Width-margin));
form.Height=Math.Min(form.Height,Math.Max(form.MinimumSize.Height,area.Height-margin));
var anchor=form.Owner?.Bounds??area;
form.Left=Math.Max(area.Left,Math.Min(area.Right-form.Width,anchor.Left+(anchor.Width-form.Width)/2));
form.Top=Math.Max(area.Top,Math.Min(area.Bottom-form.Height,anchor.Top+(anchor.Height-form.Height)/2));
}
}
internal sealed class TextPromptForm : Form
{
readonly TextBox input;
public string Value=>input.Text;
public TextPromptForm(string title,string text,string value)
{
Text=title;Width=580;Height=360;MinimumSize=new(440,280);StartPosition=FormStartPosition.CenterParent;FormBorderStyle=FormBorderStyle.Sizable;AutoScaleMode=AutoScaleMode.Dpi;
var root=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=1,RowCount=3,Padding=new(14)};root.RowStyles.Add(new(SizeType.Percent,100));root.RowStyles.Add(new(SizeType.AutoSize));root.RowStyles.Add(new(SizeType.AutoSize));
var l=new RichTextBox{Dock=DockStyle.Fill,ReadOnly=true,BorderStyle=BorderStyle.None,BackColor=SystemColors.Control,ScrollBars=RichTextBoxScrollBars.Vertical,Text=text};input=new TextBox{Dock=DockStyle.Top,Text=value,Margin=new(4,10,4,10)};
var buttons=new FlowLayoutPanel{Dock=DockStyle.Fill,AutoSize=true,FlowDirection=FlowDirection.RightToLeft,Padding=new(4)};var ok=new Button{Text="确定",AutoSize=true,Padding=new(16,6,16,6),DialogResult=DialogResult.OK};var cancel=new Button{Text="取消",AutoSize=true,Padding=new(16,6,16,6),DialogResult=DialogResult.Cancel};buttons.Controls.AddRange([ok,cancel]);
root.Controls.Add(l,0,0);root.Controls.Add(input,0,1);root.Controls.Add(buttons,0,2);Controls.Add(root);AcceptButton=ok;CancelButton=cancel;Shown+=(_,_)=>UiSizing.FitToScreen(this);
}
}
internal static class OperationCatalog
{