2019-12-22 02:59:18 +00:00
|
|
|
|
using System;
|
2020-01-07 13:50:11 +00:00
|
|
|
|
using System.Collections;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.Collections.Generic;
|
2019-10-31 03:40:33 +00:00
|
|
|
|
using System.Data.HashFunction.xxHash;
|
2019-08-04 22:08:03 +00:00
|
|
|
|
using System.Diagnostics;
|
2020-04-03 23:23:13 +00:00
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2019-07-23 04:27:26 +00:00
|
|
|
|
using System.Net.Http;
|
2020-08-11 15:37:17 +00:00
|
|
|
|
using System.Reactive.Concurrency;
|
2019-12-20 20:51:10 +00:00
|
|
|
|
using System.Reactive.Linq;
|
2019-11-05 04:14:31 +00:00
|
|
|
|
using System.Reactive.Subjects;
|
2019-10-03 04:35:29 +00:00
|
|
|
|
using System.Reflection;
|
2019-12-19 16:09:31 +00:00
|
|
|
|
using System.Runtime.InteropServices;
|
2020-04-04 14:33:58 +00:00
|
|
|
|
using System.Security;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
2019-08-02 23:04:04 +00:00
|
|
|
|
using System.Threading;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2019-11-21 00:04:01 +00:00
|
|
|
|
using Alphaleonis.Win32.Filesystem;
|
2019-09-14 04:35:42 +00:00
|
|
|
|
using ICSharpCode.SharpZipLib.BZip2;
|
|
|
|
|
using IniParser;
|
2020-03-28 02:54:14 +00:00
|
|
|
|
using IniParser.Model.Configuration;
|
|
|
|
|
using IniParser.Parser;
|
2020-06-15 18:03:50 +00:00
|
|
|
|
using Microsoft.Win32;
|
2019-09-14 04:35:42 +00:00
|
|
|
|
using Newtonsoft.Json;
|
2019-12-04 04:12:08 +00:00
|
|
|
|
using Wabbajack.Common.StatusFeed;
|
2019-12-05 05:07:44 +00:00
|
|
|
|
using Wabbajack.Common.StatusFeed.Errors;
|
2019-10-16 21:36:14 +00:00
|
|
|
|
using YamlDotNet.Serialization;
|
|
|
|
|
using YamlDotNet.Serialization.NamingConventions;
|
2019-11-21 00:04:01 +00:00
|
|
|
|
using Directory = System.IO.Directory;
|
2019-08-29 22:49:48 +00:00
|
|
|
|
using File = Alphaleonis.Win32.Filesystem.File;
|
|
|
|
|
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
|
|
|
|
|
using Path = Alphaleonis.Win32.Filesystem.Path;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
|
|
|
|
namespace Wabbajack.Common
|
|
|
|
|
{
|
2020-03-22 15:50:53 +00:00
|
|
|
|
public static partial class Utils
|
2019-07-21 04:40:54 +00:00
|
|
|
|
{
|
2019-11-01 10:55:50 +00:00
|
|
|
|
public static bool IsMO2Running(string mo2Path)
|
|
|
|
|
{
|
|
|
|
|
Process[] processList = Process.GetProcesses();
|
2019-11-01 11:06:12 +00:00
|
|
|
|
return processList.Where(process => process.ProcessName == "ModOrganizer").Any(process => Path.GetDirectoryName(process.MainModule?.FileName) == mo2Path);
|
2019-11-01 10:55:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-25 12:47:25 +00:00
|
|
|
|
public static AbsolutePath LogFile { get; }
|
|
|
|
|
public static AbsolutePath LogFolder { get; }
|
2019-12-20 20:51:10 +00:00
|
|
|
|
|
|
|
|
|
public enum FileEventType
|
|
|
|
|
{
|
|
|
|
|
Created,
|
|
|
|
|
Changed,
|
|
|
|
|
Deleted
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-03 04:35:29 +00:00
|
|
|
|
static Utils()
|
|
|
|
|
{
|
2020-03-28 02:54:14 +00:00
|
|
|
|
LogFolder = Consts.LogsFolder;
|
|
|
|
|
LogFile = Consts.LogFile;
|
2020-03-25 12:47:25 +00:00
|
|
|
|
Consts.LocalAppDataPath.CreateDirectory();
|
|
|
|
|
Consts.LogsFolder.CreateDirectory();
|
2019-12-20 22:47:33 +00:00
|
|
|
|
|
2019-10-03 04:35:29 +00:00
|
|
|
|
_startTime = DateTime.Now;
|
|
|
|
|
|
2020-03-25 12:47:25 +00:00
|
|
|
|
if (LogFile.Exists)
|
2020-02-12 05:31:42 +00:00
|
|
|
|
{
|
2020-03-25 12:47:25 +00:00
|
|
|
|
var newPath = Consts.LogsFolder.Combine(Consts.EntryPoint.FileNameWithoutExtension + LogFile.LastModified.ToString(" yyyy-MM-dd HH_mm_ss") + ".log");
|
2020-05-25 16:24:16 +00:00
|
|
|
|
LogFile.MoveToAsync(newPath, true).Wait();
|
2020-02-12 05:31:42 +00:00
|
|
|
|
}
|
2019-12-20 20:51:10 +00:00
|
|
|
|
|
2020-03-28 02:54:14 +00:00
|
|
|
|
var logFiles = LogFolder.EnumerateFiles(false).ToList();
|
2020-03-25 12:47:25 +00:00
|
|
|
|
if (logFiles.Count >= Consts.MaxOldLogs)
|
2020-02-20 11:03:49 +00:00
|
|
|
|
{
|
2020-03-25 12:47:25 +00:00
|
|
|
|
Log($"Maximum amount of old logs reached ({logFiles.Count} >= {Consts.MaxOldLogs})");
|
2020-02-20 11:03:49 +00:00
|
|
|
|
var filesToDelete = logFiles
|
2020-03-25 12:47:25 +00:00
|
|
|
|
.Where(f => f.IsFile)
|
|
|
|
|
.OrderBy(f => f.LastModified)
|
|
|
|
|
.Take(logFiles.Count - Consts.MaxOldLogs)
|
|
|
|
|
.ToList();
|
2020-02-20 11:03:49 +00:00
|
|
|
|
|
|
|
|
|
Log($"Found {filesToDelete.Count} old log files to delete");
|
|
|
|
|
|
|
|
|
|
var success = 0;
|
|
|
|
|
var failed = 0;
|
|
|
|
|
filesToDelete.Do(f =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2020-06-01 20:26:03 +00:00
|
|
|
|
f.Delete();
|
2020-02-20 11:03:49 +00:00
|
|
|
|
success++;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
failed++;
|
|
|
|
|
Log($"Could not delete log at {f}!\n{e}");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Log($"Deleted {success} log files, failed to delete {failed} logs");
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-25 12:47:25 +00:00
|
|
|
|
var watcher = new FileSystemWatcher((string)Consts.LocalAppDataPath);
|
2019-12-20 20:51:10 +00:00
|
|
|
|
AppLocalEvents = Observable.Merge(Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(h => watcher.Changed += h, h => watcher.Changed -= h).Select(e => (FileEventType.Changed, e.EventArgs)),
|
|
|
|
|
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(h => watcher.Created += h, h => watcher.Created -= h).Select(e => (FileEventType.Created, e.EventArgs)),
|
|
|
|
|
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(h => watcher.Deleted += h, h => watcher.Deleted -= h).Select(e => (FileEventType.Deleted, e.EventArgs)))
|
2020-08-11 15:37:17 +00:00
|
|
|
|
.ObserveOn(Scheduler.Default);
|
2019-12-20 20:51:10 +00:00
|
|
|
|
watcher.EnableRaisingEvents = true;
|
2019-10-03 04:35:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 04:12:08 +00:00
|
|
|
|
private static readonly Subject<IStatusMessage> LoggerSubj = new Subject<IStatusMessage>();
|
|
|
|
|
public static IObservable<IStatusMessage> LogMessages => LoggerSubj;
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-09-24 15:26:44 +00:00
|
|
|
|
private static readonly string[] Suffix = {"B", "KB", "MB", "GB", "TB", "PB", "EB"}; // Longs run out around EB
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-10-03 04:35:29 +00:00
|
|
|
|
private static object _lock = new object();
|
2019-11-05 04:14:31 +00:00
|
|
|
|
|
2019-10-03 04:35:29 +00:00
|
|
|
|
private static DateTime _startTime;
|
|
|
|
|
|
2019-12-04 04:12:08 +00:00
|
|
|
|
|
2019-08-19 22:08:34 +00:00
|
|
|
|
public static void Log(string msg)
|
2019-12-04 04:12:08 +00:00
|
|
|
|
{
|
|
|
|
|
Log(new GenericInfo(msg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static T Log<T>(T msg) where T : IStatusMessage
|
2019-08-19 22:08:34 +00:00
|
|
|
|
{
|
2020-04-16 03:02:39 +00:00
|
|
|
|
LogStraightToFile(string.IsNullOrWhiteSpace(msg.ExtendedDescription) ? msg.ShortDescription : msg.ExtendedDescription);
|
2019-11-21 15:49:14 +00:00
|
|
|
|
LoggerSubj.OnNext(msg);
|
2019-12-04 04:12:08 +00:00
|
|
|
|
return msg;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-10 04:55:57 +00:00
|
|
|
|
public static void Error(string errMessage)
|
|
|
|
|
{
|
|
|
|
|
Log(errMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-03 23:23:13 +00:00
|
|
|
|
public static void Error(Exception ex, string? extraMessage = null)
|
2019-12-05 05:07:44 +00:00
|
|
|
|
{
|
|
|
|
|
Log(new GenericException(ex, extraMessage));
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-03 23:23:13 +00:00
|
|
|
|
public static void ErrorThrow(Exception ex, string? extraMessage = null)
|
2019-12-05 05:07:44 +00:00
|
|
|
|
{
|
|
|
|
|
Error(ex, extraMessage);
|
|
|
|
|
throw ex;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-05 04:57:05 +00:00
|
|
|
|
public static void Error(IException err)
|
2019-12-04 04:12:08 +00:00
|
|
|
|
{
|
2020-01-20 01:36:09 +00:00
|
|
|
|
LogStraightToFile($"{err.ShortDescription}\n{err.Exception.StackTrace}");
|
2019-12-04 04:12:08 +00:00
|
|
|
|
LoggerSubj.OnNext(err);
|
2019-12-05 04:58:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void ErrorThrow(IException err)
|
|
|
|
|
{
|
|
|
|
|
Error(err);
|
2019-12-05 04:57:05 +00:00
|
|
|
|
throw err.Exception;
|
2019-08-19 22:08:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-20 01:36:09 +00:00
|
|
|
|
public static void LogStraightToFile(string msg)
|
2019-10-12 19:15:19 +00:00
|
|
|
|
{
|
2020-03-28 02:54:14 +00:00
|
|
|
|
if (LogFile == default) return;
|
2019-10-12 19:15:19 +00:00
|
|
|
|
lock (_lock)
|
|
|
|
|
{
|
2020-05-25 14:31:56 +00:00
|
|
|
|
File.AppendAllText(LogFile.ToString(), $"{(DateTime.Now - _startTime).TotalSeconds:0.##} - {msg}\r\n");
|
2019-10-12 19:15:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-08 04:35:08 +00:00
|
|
|
|
public static void Status(string msg, Percent progress, bool alsoLog = false)
|
2019-08-19 22:08:34 +00:00
|
|
|
|
{
|
2019-12-07 06:40:48 +00:00
|
|
|
|
WorkQueue.AsyncLocalCurrentQueue.Value?.Report(msg, progress);
|
2019-12-22 06:27:57 +00:00
|
|
|
|
if (alsoLog)
|
|
|
|
|
{
|
|
|
|
|
Utils.Log(msg);
|
|
|
|
|
}
|
2019-08-19 22:08:34 +00:00
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2020-02-08 04:35:08 +00:00
|
|
|
|
public static void Status(string msg, bool alsoLog = false)
|
|
|
|
|
{
|
|
|
|
|
Status(msg, Percent.Zero, alsoLog: alsoLog);
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-05 00:05:31 +00:00
|
|
|
|
public static void CatchAndLog(Action a)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
a();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Utils.Error(ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async Task CatchAndLog(Func<Task> f)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await f();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Utils.Error(ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 23:52:03 +00:00
|
|
|
|
public static void CopyToWithStatus(this Stream istream, long maxSize, Stream ostream, string status)
|
|
|
|
|
{
|
|
|
|
|
var buffer = new byte[1024 * 64];
|
|
|
|
|
if (maxSize == 0) maxSize = 1;
|
2019-11-21 15:49:14 +00:00
|
|
|
|
long totalRead = 0;
|
2019-08-25 23:52:03 +00:00
|
|
|
|
while (true)
|
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var read = istream.Read(buffer, 0, buffer.Length);
|
2019-08-25 23:52:03 +00:00
|
|
|
|
if (read == 0) break;
|
2019-11-21 15:49:14 +00:00
|
|
|
|
totalRead += read;
|
2019-08-25 23:52:03 +00:00
|
|
|
|
ostream.Write(buffer, 0, read);
|
2020-02-08 04:35:08 +00:00
|
|
|
|
Status(status, Percent.FactoryPutInRange(totalRead, maxSize));
|
2019-08-25 23:52:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-29 20:42:45 +00:00
|
|
|
|
|
|
|
|
|
public static async Task CopyToWithStatusAsync(this Stream istream, long maxSize, Stream ostream, string status)
|
|
|
|
|
{
|
|
|
|
|
var buffer = new byte[1024 * 64];
|
|
|
|
|
if (maxSize == 0) maxSize = 1;
|
|
|
|
|
long totalRead = 0;
|
2020-04-20 21:36:33 +00:00
|
|
|
|
long remain = maxSize;
|
2020-03-29 20:42:45 +00:00
|
|
|
|
while (true)
|
|
|
|
|
{
|
2020-04-20 21:36:33 +00:00
|
|
|
|
var toRead = Math.Min(buffer.Length, remain);
|
|
|
|
|
var read = await istream.ReadAsync(buffer, 0, (int)toRead);
|
|
|
|
|
remain -= read;
|
2020-03-29 20:42:45 +00:00
|
|
|
|
if (read == 0) break;
|
|
|
|
|
totalRead += read;
|
|
|
|
|
await ostream.WriteAsync(buffer, 0, read);
|
|
|
|
|
Status(status, Percent.FactoryPutInRange(totalRead, maxSize));
|
|
|
|
|
}
|
2020-04-20 21:36:33 +00:00
|
|
|
|
|
|
|
|
|
await ostream.FlushAsync();
|
2020-03-29 20:42:45 +00:00
|
|
|
|
}
|
2019-07-26 20:59:14 +00:00
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Returns a Base64 encoding of these bytes
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static string ToBase64(this byte[] data)
|
|
|
|
|
{
|
|
|
|
|
return Convert.ToBase64String(data);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 00:27:46 +00:00
|
|
|
|
public static string ToHex(this byte[] bytes)
|
2019-09-05 22:18:54 +00:00
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
for (var i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2"));
|
2019-09-05 22:18:54 +00:00
|
|
|
|
return builder.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 00:27:46 +00:00
|
|
|
|
public static byte[] FromHex(this string hex)
|
|
|
|
|
{
|
|
|
|
|
return Enumerable.Range(0, hex.Length)
|
|
|
|
|
.Where(x => x % 2 == 0)
|
|
|
|
|
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static DateTime AsUnixTime(this long timestamp)
|
|
|
|
|
{
|
2019-11-21 15:04:48 +00:00
|
|
|
|
DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
|
2019-11-22 00:41:01 +00:00
|
|
|
|
dtDateTime = dtDateTime.AddSeconds(timestamp);
|
2019-11-05 00:27:46 +00:00
|
|
|
|
return dtDateTime;
|
|
|
|
|
}
|
2020-02-16 02:50:02 +00:00
|
|
|
|
|
|
|
|
|
public static ulong AsUnixTime(this DateTime timestamp)
|
|
|
|
|
{
|
|
|
|
|
var diff = timestamp - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
|
return (ulong)diff.TotalSeconds;
|
|
|
|
|
}
|
2019-11-05 00:27:46 +00:00
|
|
|
|
|
2019-07-23 04:27:26 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Returns data from a base64 stream
|
2019-07-23 04:27:26 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static byte[] FromBase64(this string data)
|
|
|
|
|
{
|
|
|
|
|
return Convert.FromBase64String(data);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Executes the action for every item in coll
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
/// <param name="coll"></param>
|
|
|
|
|
/// <param name="f"></param>
|
|
|
|
|
public static void Do<T>(this IEnumerable<T> coll, Action<T> f)
|
|
|
|
|
{
|
|
|
|
|
foreach (var i in coll) f(i);
|
|
|
|
|
}
|
2020-05-25 19:41:16 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Executes the action for every item in coll
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
/// <param name="coll"></param>
|
|
|
|
|
/// <param name="f"></param>
|
|
|
|
|
public static async Task DoAsync<T>(this IEnumerable<T> coll, Func<T, Task> f)
|
|
|
|
|
{
|
|
|
|
|
foreach (var i in coll) await f(i);
|
|
|
|
|
}
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
2019-08-25 23:52:03 +00:00
|
|
|
|
public static void DoIndexed<T>(this IEnumerable<T> coll, Action<int, T> f)
|
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var idx = 0;
|
2019-08-25 23:52:03 +00:00
|
|
|
|
foreach (var i in coll)
|
|
|
|
|
{
|
|
|
|
|
f(idx, i);
|
|
|
|
|
idx += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-20 21:36:33 +00:00
|
|
|
|
|
|
|
|
|
public static async Task DoIndexed<T>(this IEnumerable<T> coll, Func<int, T, Task> f)
|
|
|
|
|
{
|
|
|
|
|
var idx = 0;
|
|
|
|
|
foreach (var i in coll)
|
|
|
|
|
{
|
|
|
|
|
await f(idx, i);
|
|
|
|
|
idx += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-25 23:52:03 +00:00
|
|
|
|
|
2019-12-23 23:28:47 +00:00
|
|
|
|
public static Task PDoIndexed<T>(this IEnumerable<T> coll, WorkQueue queue, Action<int, T> f)
|
|
|
|
|
{
|
|
|
|
|
return coll.Zip(Enumerable.Range(0, int.MaxValue), (v, idx) => (v, idx))
|
|
|
|
|
.PMap(queue, vs=> f(vs.idx, vs.v));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-03-28 02:54:14 +00:00
|
|
|
|
private static IniDataParser IniParser()
|
|
|
|
|
{
|
|
|
|
|
var config = new IniParserConfiguration {AllowDuplicateKeys = true, AllowDuplicateSections = true};
|
|
|
|
|
var parser = new IniDataParser(config);
|
|
|
|
|
return parser;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 23:52:03 +00:00
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Loads INI data from the given filename and returns a dynamic type that
|
|
|
|
|
/// can use . operators to navigate the INI.
|
2019-07-21 04:40:54 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="file"></param>
|
|
|
|
|
/// <returns></returns>
|
2020-03-25 12:47:25 +00:00
|
|
|
|
public static dynamic LoadIniFile(this AbsolutePath file)
|
2019-07-21 04:40:54 +00:00
|
|
|
|
{
|
2020-03-28 02:54:14 +00:00
|
|
|
|
return new DynamicIniData(new FileIniDataParser(IniParser()).ReadFile((string)file));
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-12 18:03:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Loads a INI from the given string
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="file"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static dynamic LoadIniString(this string file)
|
|
|
|
|
{
|
2020-03-28 02:54:14 +00:00
|
|
|
|
return new DynamicIniData(new FileIniDataParser(IniParser()).ReadData(new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(file)))));
|
2019-10-12 18:03:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 04:40:54 +00:00
|
|
|
|
|
|
|
|
|
public static bool FileExists(this string filename)
|
|
|
|
|
{
|
|
|
|
|
return File.Exists(filename);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string RelativeTo(this string file, string folder)
|
|
|
|
|
{
|
|
|
|
|
return file.Substring(folder.Length + 1);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Returns the string compressed via BZip2
|
2019-07-21 22:47:17 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static byte[] BZip2String(this string data)
|
|
|
|
|
{
|
|
|
|
|
using (var os = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
using (var bz = new BZip2OutputStream(os))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(bz))
|
2019-09-14 04:35:42 +00:00
|
|
|
|
{
|
2019-07-21 22:47:17 +00:00
|
|
|
|
bw.Write(data);
|
2019-09-14 04:35:42 +00:00
|
|
|
|
}
|
2019-07-21 22:47:17 +00:00
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
return os.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-21 04:48:59 +00:00
|
|
|
|
public static void BZip2ExtractToFile(this Stream src, string dest)
|
|
|
|
|
{
|
2020-01-18 20:52:09 +00:00
|
|
|
|
using (var os = File.Open(dest, System.IO.FileMode.Create))
|
2019-09-21 04:48:59 +00:00
|
|
|
|
{
|
|
|
|
|
os.SetLength(0);
|
|
|
|
|
using (var bz = new BZip2InputStream(src))
|
|
|
|
|
bz.CopyTo(os);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-21 22:47:17 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// Returns the string compressed via BZip2
|
2019-07-21 22:47:17 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static string BZip2String(this byte[] data)
|
|
|
|
|
{
|
|
|
|
|
using (var s = new MemoryStream(data))
|
|
|
|
|
{
|
|
|
|
|
using (var bz = new BZip2InputStream(s))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryReader(bz))
|
2019-09-14 04:35:42 +00:00
|
|
|
|
{
|
2019-07-21 22:47:17 +00:00
|
|
|
|
return bw.ReadString();
|
2019-09-14 04:35:42 +00:00
|
|
|
|
}
|
2019-07-21 22:47:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-21 21:32:58 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// A combination of .Select(func).Where(v => v != default). So select and filter default values.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="TIn"></typeparam>
|
|
|
|
|
/// <typeparam name="TOut"></typeparam>
|
|
|
|
|
/// <param name="coll"></param>
|
|
|
|
|
/// <param name="func"></param>
|
|
|
|
|
/// <returns></returns>
|
2020-06-26 17:19:07 +00:00
|
|
|
|
public static IEnumerable<TOut> Keep<TIn, TOut>(this IEnumerable<TIn> coll, Func<TIn, TOut> func)
|
|
|
|
|
where TOut : struct, IComparable<TOut>
|
2019-11-21 21:32:58 +00:00
|
|
|
|
{
|
|
|
|
|
return coll.Select(func).Where(v => v.CompareTo(default) != 0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-22 03:36:25 +00:00
|
|
|
|
public static byte[] ReadAll(this Stream ins)
|
|
|
|
|
{
|
|
|
|
|
using (var ms = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
ins.CopyTo(ms);
|
|
|
|
|
return ms.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 12:29:59 +00:00
|
|
|
|
public static async Task<byte[]> ReadAllAsync(this Stream ins)
|
|
|
|
|
{
|
|
|
|
|
await using var ms = new MemoryStream();
|
|
|
|
|
await ins.CopyToAsync(ms);
|
|
|
|
|
return ms.ToArray();
|
|
|
|
|
}
|
2020-04-17 03:52:19 +00:00
|
|
|
|
|
|
|
|
|
public static async Task<string> ReadAllTextAsync(this Stream ins)
|
|
|
|
|
{
|
|
|
|
|
await using var ms = new MemoryStream();
|
|
|
|
|
await ins.CopyToAsync(ms);
|
|
|
|
|
return Encoding.UTF8.GetString(ms.ToArray());
|
|
|
|
|
}
|
2020-02-13 12:29:59 +00:00
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
public static async Task<TR[]> PMap<TI, TR>(this IEnumerable<TI> coll, WorkQueue queue, StatusUpdateTracker updateTracker,
|
2019-11-17 04:16:42 +00:00
|
|
|
|
Func<TI, TR> f)
|
|
|
|
|
{
|
|
|
|
|
var cnt = 0;
|
|
|
|
|
var collist = coll.ToList();
|
2019-12-04 01:26:26 +00:00
|
|
|
|
return await collist.PMap(queue, itm =>
|
2019-11-17 04:16:42 +00:00
|
|
|
|
{
|
|
|
|
|
updateTracker.MakeUpdate(collist.Count, Interlocked.Increment(ref cnt));
|
|
|
|
|
return f(itm);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
public static async Task<TR[]> PMap<TI, TR>(this IEnumerable<TI> coll, WorkQueue queue, StatusUpdateTracker updateTracker,
|
|
|
|
|
Func<TI, Task<TR>> f)
|
|
|
|
|
{
|
|
|
|
|
var cnt = 0;
|
|
|
|
|
var collist = coll.ToList();
|
|
|
|
|
return await collist.PMap(queue, itm =>
|
|
|
|
|
{
|
|
|
|
|
updateTracker.MakeUpdate(collist.Count, Interlocked.Increment(ref cnt));
|
|
|
|
|
return f(itm);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-22 02:59:18 +00:00
|
|
|
|
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue, StatusUpdateTracker updateTracker,
|
|
|
|
|
Func<TI, Task> f)
|
|
|
|
|
{
|
|
|
|
|
var cnt = 0;
|
|
|
|
|
var collist = coll.ToList();
|
|
|
|
|
await collist.PMap(queue, async itm =>
|
|
|
|
|
{
|
|
|
|
|
updateTracker.MakeUpdate(collist.Count, Interlocked.Increment(ref cnt));
|
|
|
|
|
await f(itm);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue, StatusUpdateTracker updateTracker,
|
2019-11-24 23:03:36 +00:00
|
|
|
|
Action<TI> f)
|
|
|
|
|
{
|
|
|
|
|
var cnt = 0;
|
|
|
|
|
var collist = coll.ToList();
|
2019-12-04 01:26:26 +00:00
|
|
|
|
await collist.PMap(queue, itm =>
|
2019-11-24 23:03:36 +00:00
|
|
|
|
{
|
|
|
|
|
updateTracker.MakeUpdate(collist.Count, Interlocked.Increment(ref cnt));
|
|
|
|
|
f(itm);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
public static async Task<TR[]> PMap<TI, TR>(this IEnumerable<TI> coll, WorkQueue queue,
|
2019-11-17 04:16:42 +00:00
|
|
|
|
Func<TI, TR> f)
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
2019-08-02 23:04:04 +00:00
|
|
|
|
var colllst = coll.ToList();
|
|
|
|
|
|
2019-11-21 15:49:14 +00:00
|
|
|
|
var remainingTasks = colllst.Count;
|
2019-08-10 15:21:50 +00:00
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
var tasks = colllst.Select(i =>
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var tc = new TaskCompletionSource<TR>();
|
2019-12-04 01:26:26 +00:00
|
|
|
|
queue.QueueTask(async () =>
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
tc.SetResult(f(i));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
tc.SetException(ex);
|
|
|
|
|
}
|
2019-11-21 15:49:14 +00:00
|
|
|
|
Interlocked.Decrement(ref remainingTasks);
|
2019-07-22 22:17:46 +00:00
|
|
|
|
});
|
|
|
|
|
return tc.Task;
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
2019-08-10 15:21:50 +00:00
|
|
|
|
// To avoid thread starvation, we'll start to help out in the work queue
|
|
|
|
|
if (WorkQueue.WorkerThread)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2020-04-22 04:13:07 +00:00
|
|
|
|
while (true)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2020-03-29 03:29:27 +00:00
|
|
|
|
var (got, a) = await queue.Queue.TryTake(TimeSpan.FromMilliseconds(100), CancellationToken.None);
|
2020-03-30 21:38:01 +00:00
|
|
|
|
if (got)
|
2019-12-07 06:40:48 +00:00
|
|
|
|
{
|
2019-12-04 01:26:26 +00:00
|
|
|
|
await a();
|
2019-12-07 06:40:48 +00:00
|
|
|
|
}
|
2020-04-22 04:13:07 +00:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-12-23 04:42:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-10 15:21:50 +00:00
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
return await Task.WhenAll(tasks);
|
|
|
|
|
}
|
|
|
|
|
public static async Task<TR[]> PMap<TI, TR>(this IEnumerable<TI> coll, WorkQueue queue,
|
|
|
|
|
Func<TI, Task<TR>> f)
|
|
|
|
|
{
|
|
|
|
|
var colllst = coll.ToList();
|
|
|
|
|
|
|
|
|
|
var remainingTasks = colllst.Count;
|
|
|
|
|
|
|
|
|
|
var tasks = colllst.Select(i =>
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
2019-12-04 01:26:26 +00:00
|
|
|
|
var tc = new TaskCompletionSource<TR>();
|
|
|
|
|
queue.QueueTask(async () =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
tc.SetResult(await f(i));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
tc.SetException(ex);
|
|
|
|
|
}
|
|
|
|
|
Interlocked.Decrement(ref remainingTasks);
|
|
|
|
|
});
|
|
|
|
|
return tc.Task;
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
|
|
|
// To avoid thread starvation, we'll start to help out in the work queue
|
|
|
|
|
if (WorkQueue.WorkerThread)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2019-12-04 01:26:26 +00:00
|
|
|
|
while (remainingTasks > 0)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2020-03-30 21:38:01 +00:00
|
|
|
|
var (got, a) = await queue.Queue.TryTake(TimeSpan.FromMilliseconds(200), CancellationToken.None);
|
|
|
|
|
if (got)
|
2019-12-07 06:40:48 +00:00
|
|
|
|
{
|
2019-12-04 01:26:26 +00:00
|
|
|
|
await a();
|
2019-12-07 06:40:48 +00:00
|
|
|
|
}
|
2019-12-23 04:42:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-04 01:26:26 +00:00
|
|
|
|
|
|
|
|
|
return await Task.WhenAll(tasks);
|
2019-07-22 22:17:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-20 20:50:02 +00:00
|
|
|
|
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue,
|
|
|
|
|
Func<TI, Task> f)
|
|
|
|
|
{
|
|
|
|
|
var colllst = coll.ToList();
|
|
|
|
|
|
|
|
|
|
var remainingTasks = colllst.Count;
|
|
|
|
|
|
|
|
|
|
var tasks = colllst.Select(i =>
|
|
|
|
|
{
|
|
|
|
|
var tc = new TaskCompletionSource<bool>();
|
|
|
|
|
queue.QueueTask(async () =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await f(i);
|
|
|
|
|
tc.SetResult(true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
tc.SetException(ex);
|
|
|
|
|
}
|
|
|
|
|
Interlocked.Decrement(ref remainingTasks);
|
|
|
|
|
});
|
|
|
|
|
return tc.Task;
|
|
|
|
|
}).ToList();
|
2020-04-22 04:13:07 +00:00
|
|
|
|
|
2019-12-20 20:50:02 +00:00
|
|
|
|
// To avoid thread starvation, we'll start to help out in the work queue
|
|
|
|
|
if (WorkQueue.WorkerThread)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2019-12-20 20:50:02 +00:00
|
|
|
|
while (remainingTasks > 0)
|
2019-12-23 04:42:57 +00:00
|
|
|
|
{
|
2020-03-30 21:38:01 +00:00
|
|
|
|
var (got, a) = await queue.Queue.TryTake(TimeSpan.FromMilliseconds(200), CancellationToken.None);
|
|
|
|
|
if (got)
|
2019-12-20 20:50:02 +00:00
|
|
|
|
{
|
|
|
|
|
await a();
|
|
|
|
|
}
|
2019-12-23 04:42:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-20 20:50:02 +00:00
|
|
|
|
|
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 01:26:26 +00:00
|
|
|
|
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue, Action<TI> f)
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
2019-12-04 01:26:26 +00:00
|
|
|
|
await coll.PMap(queue, i =>
|
2019-07-22 22:17:46 +00:00
|
|
|
|
{
|
2019-08-10 15:21:50 +00:00
|
|
|
|
f(i);
|
|
|
|
|
return false;
|
|
|
|
|
});
|
2019-07-22 22:17:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-14 04:35:42 +00:00
|
|
|
|
public static void DoProgress<T>(this IEnumerable<T> coll, string msg, Action<T> f)
|
2019-08-29 22:49:48 +00:00
|
|
|
|
{
|
|
|
|
|
var lst = coll.ToList();
|
|
|
|
|
lst.DoIndexed((idx, i) =>
|
|
|
|
|
{
|
2020-02-08 04:35:08 +00:00
|
|
|
|
Status(msg, Percent.FactoryPutInRange(idx, lst.Count));
|
2019-08-29 22:49:48 +00:00
|
|
|
|
f(i);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-04-20 21:36:33 +00:00
|
|
|
|
|
|
|
|
|
public static async Task DoProgress<T>(this IEnumerable<T> coll, string msg, Func<T, Task> f)
|
|
|
|
|
{
|
|
|
|
|
var lst = coll.ToList();
|
|
|
|
|
await lst.DoIndexed(async (idx, i) =>
|
|
|
|
|
{
|
|
|
|
|
Status(msg, Percent.FactoryPutInRange(idx, lst.Count));
|
|
|
|
|
await f(i);
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-08-29 22:49:48 +00:00
|
|
|
|
|
|
|
|
|
public static void OnQueue(Action f)
|
|
|
|
|
{
|
|
|
|
|
new List<bool>().Do(_ => f());
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-06 05:59:57 +00:00
|
|
|
|
public static async Task<Stream> PostStream(this HttpClient client, string url, HttpContent content)
|
2019-07-23 04:27:26 +00:00
|
|
|
|
{
|
2019-12-06 05:59:57 +00:00
|
|
|
|
var result = await client.PostAsync(url, content);
|
|
|
|
|
return await result.Content.ReadAsStreamAsync();
|
2019-08-26 23:02:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-15 04:30:37 +00:00
|
|
|
|
public static IEnumerable<T> DistinctBy<T, V>(this IEnumerable<T> vs, Func<T, V> select)
|
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var set = new HashSet<V>();
|
|
|
|
|
foreach (var v in vs)
|
|
|
|
|
{
|
2019-08-15 04:30:37 +00:00
|
|
|
|
var key = select(v);
|
|
|
|
|
if (set.Contains(key)) continue;
|
2020-01-09 00:04:57 +00:00
|
|
|
|
set.Add(key);
|
2019-08-15 04:30:37 +00:00
|
|
|
|
yield return v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static T Last<T>(this T[] a)
|
|
|
|
|
{
|
|
|
|
|
if (a == null || a.Length == 0)
|
|
|
|
|
throw new InvalidDataException("null or empty array");
|
|
|
|
|
return a[a.Length - 1];
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-03 23:23:13 +00:00
|
|
|
|
[return: MaybeNull]
|
2019-08-07 23:06:38 +00:00
|
|
|
|
public static V GetOrDefault<K, V>(this IDictionary<K, V> dict, K key)
|
2020-04-03 23:23:13 +00:00
|
|
|
|
where K : notnull
|
2019-08-07 23:06:38 +00:00
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
if (dict.TryGetValue(key, out var v)) return v;
|
|
|
|
|
return default;
|
2019-08-07 23:06:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-10 13:02:25 +00:00
|
|
|
|
public static string ToFileSizeString(this long byteCount)
|
|
|
|
|
{
|
|
|
|
|
if (byteCount == 0)
|
|
|
|
|
return "0" + Suffix[0];
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var bytes = Math.Abs(byteCount);
|
|
|
|
|
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
|
|
|
|
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
|
|
|
|
|
return Math.Sign(byteCount) * num + Suffix[place];
|
2019-09-10 13:02:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-20 22:49:32 +00:00
|
|
|
|
public static string ToFileSizeString(this int byteCount)
|
|
|
|
|
{
|
|
|
|
|
return ToFileSizeString((long)byteCount);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-01 22:39:25 +00:00
|
|
|
|
public static IEnumerable<T> ButLast<T>(this IEnumerable<T> coll)
|
|
|
|
|
{
|
|
|
|
|
var lst = coll.ToList();
|
|
|
|
|
return lst.Take(lst.Count() - 1);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-23 21:37:10 +00:00
|
|
|
|
public static byte[] ConcatArrays(this IEnumerable<byte[]> arrays)
|
|
|
|
|
{
|
|
|
|
|
var outarr = new byte[arrays.Sum(a => a.Length)];
|
|
|
|
|
int offset = 0;
|
|
|
|
|
foreach (var arr in arrays)
|
|
|
|
|
{
|
|
|
|
|
Array.Copy(arr, 0, outarr, offset, arr.Length);
|
|
|
|
|
offset += arr.Length;
|
|
|
|
|
}
|
|
|
|
|
return outarr;
|
|
|
|
|
}
|
2019-10-12 18:03:45 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
2020-01-11 04:15:53 +00:00
|
|
|
|
/// Roundtrips the value through the JSON routines
|
2019-10-12 18:03:45 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="TV"></typeparam>
|
|
|
|
|
/// <typeparam name="TR"></typeparam>
|
|
|
|
|
/// <param name="tv"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static T ViaJSON<T>(this T tv)
|
|
|
|
|
{
|
2020-04-06 20:48:54 +00:00
|
|
|
|
var json = tv.ToJson();
|
|
|
|
|
return json.FromJsonString<T>();
|
2019-10-12 18:03:45 +00:00
|
|
|
|
}
|
2019-10-12 22:15:20 +00:00
|
|
|
|
|
2019-12-04 04:12:08 +00:00
|
|
|
|
/*
|
2019-10-12 22:15:20 +00:00
|
|
|
|
public static void Error(string msg)
|
|
|
|
|
{
|
|
|
|
|
Log(msg);
|
|
|
|
|
throw new Exception(msg);
|
2019-12-04 04:12:08 +00:00
|
|
|
|
}*/
|
2019-10-16 03:17:27 +00:00
|
|
|
|
|
2019-11-29 23:56:56 +00:00
|
|
|
|
public static Stream GetEmbeddedResourceStream(string name)
|
2019-10-16 03:17:27 +00:00
|
|
|
|
{
|
|
|
|
|
return (from assembly in AppDomain.CurrentDomain.GetAssemblies()
|
|
|
|
|
where !assembly.IsDynamic
|
|
|
|
|
from rname in assembly.GetManifestResourceNames()
|
|
|
|
|
where rname == name
|
|
|
|
|
select assembly.GetManifestResourceStream(name)).First();
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-16 21:36:14 +00:00
|
|
|
|
public static T FromYaml<T>(this Stream s)
|
|
|
|
|
{
|
|
|
|
|
var d = new DeserializerBuilder()
|
|
|
|
|
.WithNamingConvention(PascalCaseNamingConvention.Instance)
|
|
|
|
|
.Build();
|
|
|
|
|
return d.Deserialize<T>(new StreamReader(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static T FromYaml<T>(this string s)
|
|
|
|
|
{
|
|
|
|
|
var d = new DeserializerBuilder()
|
|
|
|
|
.WithNamingConvention(PascalCaseNamingConvention.Instance)
|
|
|
|
|
.Build();
|
2019-10-16 21:49:20 +00:00
|
|
|
|
return d.Deserialize<T>(new StringReader(s));
|
2019-10-16 21:36:14 +00:00
|
|
|
|
}
|
2019-11-02 15:38:03 +00:00
|
|
|
|
public static void LogStatus(string s)
|
|
|
|
|
{
|
|
|
|
|
Status(s);
|
|
|
|
|
Log(s);
|
|
|
|
|
}
|
2019-11-03 03:23:59 +00:00
|
|
|
|
|
2020-04-22 20:58:50 +00:00
|
|
|
|
private static async Task<long> TestDiskSpeedInner(WorkQueue queue, AbsolutePath path)
|
2019-11-20 23:39:03 +00:00
|
|
|
|
{
|
2020-05-30 13:32:00 +00:00
|
|
|
|
var seconds = 10;
|
|
|
|
|
var runTime = new TimeSpan(0, 0, seconds);
|
|
|
|
|
Log($"Running disk benchmark, this will take {seconds} seconds");
|
|
|
|
|
var results = Enumerable.Range(0, queue.DesiredNumWorkers)
|
2020-05-25 16:24:16 +00:00
|
|
|
|
.PMap(queue, async idx =>
|
2019-11-20 23:39:03 +00:00
|
|
|
|
{
|
2020-05-30 13:32:00 +00:00
|
|
|
|
var startTime = DateTime.Now;
|
2019-11-20 23:39:03 +00:00
|
|
|
|
var random = new Random();
|
|
|
|
|
|
2020-04-22 20:58:50 +00:00
|
|
|
|
var file = path.Combine($"size_test{idx}.bin");
|
2019-11-20 23:39:03 +00:00
|
|
|
|
long size = 0;
|
|
|
|
|
byte[] buffer = new byte[1024 * 8];
|
|
|
|
|
random.NextBytes(buffer);
|
2020-05-25 16:24:16 +00:00
|
|
|
|
await using (var fs = await file.Create())
|
2019-11-20 23:39:03 +00:00
|
|
|
|
{
|
2020-05-30 13:32:00 +00:00
|
|
|
|
while (DateTime.Now - startTime < runTime)
|
2019-11-20 23:39:03 +00:00
|
|
|
|
{
|
|
|
|
|
fs.Write(buffer, 0, buffer.Length);
|
|
|
|
|
// Flush to make sure large buffers don't cause the rate to be higher than it should
|
|
|
|
|
fs.Flush();
|
|
|
|
|
size += buffer.Length;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-26 11:31:11 +00:00
|
|
|
|
await file.DeleteAsync();
|
2019-11-20 23:39:03 +00:00
|
|
|
|
return size;
|
2019-12-04 01:26:26 +00:00
|
|
|
|
});
|
2020-05-30 13:32:00 +00:00
|
|
|
|
|
|
|
|
|
for (int x = 0; x < seconds; x++)
|
|
|
|
|
{
|
|
|
|
|
Log($"Running Disk benchmark {Percent.FactoryPutInRange(x, seconds)} complete");
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (await results).Sum() / seconds;
|
2019-11-20 23:39:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-22 20:58:50 +00:00
|
|
|
|
public static async Task<long> TestDiskSpeed(WorkQueue queue, AbsolutePath path)
|
2019-11-21 00:04:01 +00:00
|
|
|
|
{
|
2020-04-22 20:58:50 +00:00
|
|
|
|
var benchmarkFile = path.Combine("disk_benchmark.bin");
|
|
|
|
|
if (benchmarkFile.Exists)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return benchmarkFile.FromJson<long>();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var speed = await TestDiskSpeedInner(queue, path);
|
2020-05-25 16:24:16 +00:00
|
|
|
|
await speed.ToJsonAsync(benchmarkFile);
|
2020-04-22 20:58:50 +00:00
|
|
|
|
|
2019-11-21 00:04:01 +00:00
|
|
|
|
return speed;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-03 03:23:59 +00:00
|
|
|
|
/// https://stackoverflow.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists
|
|
|
|
|
public static IErrorResponse IsFilePathValid(string path)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
|
|
|
{
|
2019-11-30 21:45:39 +00:00
|
|
|
|
return ErrorResponse.Fail("Path is empty.");
|
2019-11-03 03:23:59 +00:00
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var fi = new System.IO.FileInfo(path);
|
|
|
|
|
}
|
|
|
|
|
catch (ArgumentException ex)
|
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
2019-11-21 15:04:48 +00:00
|
|
|
|
catch (PathTooLongException ex)
|
2019-11-03 03:23:59 +00:00
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
catch (NotSupportedException ex)
|
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
return ErrorResponse.Success;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 20:04:22 +00:00
|
|
|
|
public static IErrorResponse IsDirectoryPathValid(AbsolutePath path)
|
2019-11-03 03:23:59 +00:00
|
|
|
|
{
|
2020-03-28 20:04:22 +00:00
|
|
|
|
if (path == default)
|
2019-11-03 03:23:59 +00:00
|
|
|
|
{
|
2019-11-30 21:45:39 +00:00
|
|
|
|
return ErrorResponse.Fail("Path is empty");
|
2019-11-03 03:23:59 +00:00
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
2020-03-28 20:04:22 +00:00
|
|
|
|
var fi = new System.IO.DirectoryInfo((string)path);
|
2019-11-03 03:23:59 +00:00
|
|
|
|
}
|
|
|
|
|
catch (ArgumentException ex)
|
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
2019-11-21 15:04:48 +00:00
|
|
|
|
catch (PathTooLongException ex)
|
2019-11-03 03:23:59 +00:00
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
catch (NotSupportedException ex)
|
|
|
|
|
{
|
|
|
|
|
return ErrorResponse.Fail(ex.Message);
|
|
|
|
|
}
|
|
|
|
|
return ErrorResponse.Success;
|
|
|
|
|
}
|
2019-11-23 17:37:24 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Both AlphaFS and C#'s Directory.Delete sometimes fail when certain files are read-only
|
|
|
|
|
/// or have other weird attributes. This is the only 100% reliable way I've found to completely
|
|
|
|
|
/// delete a folder. If you don't like this code, it's unlikely to change without a ton of testing.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path"></param>
|
2020-03-28 04:33:26 +00:00
|
|
|
|
public static async Task DeleteDirectory(AbsolutePath path)
|
2019-11-23 17:37:24 +00:00
|
|
|
|
{
|
2020-04-15 18:07:52 +00:00
|
|
|
|
if (!path.Exists)
|
|
|
|
|
return;
|
|
|
|
|
|
2020-03-19 12:13:57 +00:00
|
|
|
|
var process = new ProcessHelper
|
2019-11-23 17:37:24 +00:00
|
|
|
|
{
|
2020-04-10 12:58:55 +00:00
|
|
|
|
Path = ((RelativePath)"cmd.exe").RelativeToSystemDirectory(),
|
2020-03-23 12:57:18 +00:00
|
|
|
|
Arguments = new object[] {"/c", "del", "/f", "/q", "/s", $"\"{(string)path}\"", "&&", "rmdir", "/q", "/s", $"\"{(string)path}\""},
|
2019-11-23 17:37:24 +00:00
|
|
|
|
};
|
2020-03-19 12:13:57 +00:00
|
|
|
|
var result = process.Output.Where(d => d.Type == ProcessHelper.StreamType.Output)
|
|
|
|
|
.ForEachAsync(p =>
|
|
|
|
|
{
|
|
|
|
|
Status($"Deleting: {p.Line}");
|
|
|
|
|
});
|
2019-11-23 17:37:24 +00:00
|
|
|
|
|
2020-04-22 04:13:07 +00:00
|
|
|
|
var exitCode = await process.Start();
|
2020-03-19 12:13:57 +00:00
|
|
|
|
await result;
|
2019-11-23 17:37:24 +00:00
|
|
|
|
}
|
2019-12-07 00:13:16 +00:00
|
|
|
|
|
2019-12-22 01:30:01 +00:00
|
|
|
|
public static bool IsUnderneathDirectory(string path, string dirPath)
|
|
|
|
|
{
|
|
|
|
|
return path.StartsWith(dirPath, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-07 00:13:16 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Writes a file to JSON but in an encrypted format in the user's app local directory.
|
|
|
|
|
/// The data will be encrypted so that it can only be read by this machine and this user.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
/// <param name="key"></param>
|
|
|
|
|
/// <param name="data"></param>
|
2020-05-25 16:24:16 +00:00
|
|
|
|
public static async ValueTask ToEcryptedJson<T>(this T data, string key)
|
2019-12-07 00:13:16 +00:00
|
|
|
|
{
|
2020-06-20 22:51:47 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var bytes = Encoding.UTF8.GetBytes(data.ToJson());
|
|
|
|
|
await bytes.ToEcryptedData(key);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log($"Error encrypting data {key} {ex}");
|
|
|
|
|
throw;
|
|
|
|
|
}
|
2020-01-31 22:38:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-25 14:31:56 +00:00
|
|
|
|
public static async Task<T> FromEncryptedJson<T>(string key)
|
2020-01-31 22:38:56 +00:00
|
|
|
|
{
|
2020-05-25 14:31:56 +00:00
|
|
|
|
var decoded = await FromEncryptedData(key);
|
2020-04-06 20:48:54 +00:00
|
|
|
|
return Encoding.UTF8.GetString(decoded).FromJsonString<T>();
|
2020-01-31 22:38:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-05-25 16:24:16 +00:00
|
|
|
|
public static async ValueTask ToEcryptedData(this byte[] bytes, string key)
|
2020-01-31 22:38:56 +00:00
|
|
|
|
{
|
2019-12-07 00:13:16 +00:00
|
|
|
|
var encoded = ProtectedData.Protect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine);
|
2020-03-25 12:47:25 +00:00
|
|
|
|
Consts.LocalAppDataPath.CreateDirectory();
|
2019-12-12 13:41:06 +00:00
|
|
|
|
|
2020-05-25 16:24:16 +00:00
|
|
|
|
await Consts.LocalAppDataPath.Combine(key).WriteAllBytesAsync(encoded);
|
2019-12-07 00:13:16 +00:00
|
|
|
|
}
|
2020-05-25 14:31:56 +00:00
|
|
|
|
public static async Task<byte[]> FromEncryptedData(string key)
|
2019-12-07 00:13:16 +00:00
|
|
|
|
{
|
2020-05-25 14:31:56 +00:00
|
|
|
|
var bytes = await Consts.LocalAppDataPath.Combine(key).ReadAllBytesAsync();
|
2020-01-31 22:38:56 +00:00
|
|
|
|
return ProtectedData.Unprotect(bytes, Encoding.UTF8.GetBytes(key), DataProtectionScope.LocalMachine);
|
2019-12-07 00:13:16 +00:00
|
|
|
|
}
|
2019-12-12 22:52:24 +00:00
|
|
|
|
|
2019-12-20 20:51:10 +00:00
|
|
|
|
public static bool HaveEncryptedJson(string key)
|
|
|
|
|
{
|
2020-03-25 12:47:25 +00:00
|
|
|
|
return Consts.LocalAppDataPath.Combine(key).IsFile;
|
2019-12-20 20:51:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-15 18:03:50 +00:00
|
|
|
|
public static bool HaveRegKey()
|
|
|
|
|
{
|
|
|
|
|
return Registry.CurrentUser.OpenSubKey(@"Software\Wabbajack") != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool HaveRegKeyMetricsKey()
|
|
|
|
|
{
|
|
|
|
|
if (HaveRegKey())
|
|
|
|
|
{
|
|
|
|
|
return Registry.CurrentUser.OpenSubKey(@"Software\Wabbajack")!.GetValueNames().Contains(Consts.MetricsKeyHeader);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-12-20 20:51:10 +00:00
|
|
|
|
public static IObservable<(FileEventType, FileSystemEventArgs)> AppLocalEvents { get; }
|
|
|
|
|
|
|
|
|
|
public static IObservable<bool> HaveEncryptedJsonObservable(string key)
|
|
|
|
|
{
|
2020-03-25 12:47:25 +00:00
|
|
|
|
var path = Consts.LocalAppDataPath.Combine(key);
|
|
|
|
|
return AppLocalEvents.Where(t => (AbsolutePath)t.Item2.FullPath.ToLower() == path)
|
|
|
|
|
.Select(_ => path.Exists)
|
|
|
|
|
.StartWith(path.Exists)
|
2019-12-20 20:51:10 +00:00
|
|
|
|
.DistinctUntilChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 11:31:11 +00:00
|
|
|
|
public static async ValueTask DeleteEncryptedJson(string key)
|
2019-12-20 20:51:10 +00:00
|
|
|
|
{
|
2020-05-26 11:31:11 +00:00
|
|
|
|
await Consts.LocalAppDataPath.Combine(key).DeleteAsync();
|
2019-12-20 20:51:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-26 03:21:41 +00:00
|
|
|
|
public static void StartProcessFromFile(string file)
|
|
|
|
|
{
|
|
|
|
|
Process.Start(new ProcessStartInfo("cmd.exe", $"/c {file}")
|
|
|
|
|
{
|
|
|
|
|
CreateNoWindow = true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-22 21:55:31 +00:00
|
|
|
|
public static void OpenWebsite(Uri url)
|
2020-01-26 03:21:41 +00:00
|
|
|
|
{
|
|
|
|
|
Process.Start(new ProcessStartInfo("cmd.exe", $"/c start {url}")
|
|
|
|
|
{
|
|
|
|
|
CreateNoWindow = true,
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-12-20 20:51:10 +00:00
|
|
|
|
|
2019-12-12 22:52:24 +00:00
|
|
|
|
public static bool IsInPath(this string path, string parent)
|
|
|
|
|
{
|
|
|
|
|
return path.ToLower().TrimEnd('\\').StartsWith(parent.ToLower().TrimEnd('\\') + "\\");
|
|
|
|
|
}
|
2020-01-29 04:17:24 +00:00
|
|
|
|
|
|
|
|
|
public static async Task CopyToLimitAsync(this Stream frm, Stream tw, long limit)
|
|
|
|
|
{
|
|
|
|
|
var buff = new byte[1024];
|
|
|
|
|
while (limit > 0)
|
|
|
|
|
{
|
|
|
|
|
var to_read = Math.Min(buff.Length, limit);
|
|
|
|
|
var read = await frm.ReadAsync(buff, 0, (int)to_read);
|
|
|
|
|
await tw.WriteAsync(buff, 0, read);
|
|
|
|
|
limit -= read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tw.Flush();
|
|
|
|
|
}
|
2019-12-12 22:52:24 +00:00
|
|
|
|
|
2019-12-16 13:00:22 +00:00
|
|
|
|
public class NexusErrorResponse
|
|
|
|
|
{
|
|
|
|
|
public int code;
|
2020-04-03 23:23:13 +00:00
|
|
|
|
public string message = string.Empty;
|
2019-12-16 13:00:22 +00:00
|
|
|
|
}
|
2019-12-19 16:09:31 +00:00
|
|
|
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
|
|
|
|
public class MEMORYSTATUSEX
|
|
|
|
|
{
|
|
|
|
|
public uint dwLength;
|
|
|
|
|
public uint dwMemoryLoad;
|
|
|
|
|
public ulong ullTotalPhys;
|
|
|
|
|
public ulong ullAvailPhys;
|
|
|
|
|
public ulong ullTotalPageFile;
|
|
|
|
|
public ulong ullAvailPageFile;
|
|
|
|
|
public ulong ullTotalVirtual;
|
|
|
|
|
public ulong ullAvailVirtual;
|
|
|
|
|
public ulong ullAvailExtendedVirtual;
|
|
|
|
|
public MEMORYSTATUSEX()
|
|
|
|
|
{
|
|
|
|
|
dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
|
|
|
static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer);
|
|
|
|
|
|
|
|
|
|
public static MEMORYSTATUSEX GetMemoryStatus()
|
|
|
|
|
{
|
|
|
|
|
var mstat = new MEMORYSTATUSEX();
|
|
|
|
|
GlobalMemoryStatusEx(mstat);
|
|
|
|
|
return mstat;
|
|
|
|
|
}
|
2020-01-04 02:49:08 +00:00
|
|
|
|
|
|
|
|
|
public static string MakeRandomKey()
|
|
|
|
|
{
|
|
|
|
|
var random = new Random();
|
|
|
|
|
byte[] bytes = new byte[32];
|
|
|
|
|
random.NextBytes(bytes);
|
|
|
|
|
return bytes.ToHex();
|
|
|
|
|
}
|
2020-03-30 20:38:46 +00:00
|
|
|
|
|
|
|
|
|
public static byte[] RandomData(int size)
|
|
|
|
|
{
|
|
|
|
|
var random = new Random();
|
|
|
|
|
byte[] bytes = new byte[size];
|
|
|
|
|
random.NextBytes(bytes);
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
2020-02-05 05:17:12 +00:00
|
|
|
|
|
2020-04-04 14:33:58 +00:00
|
|
|
|
public static string ToNormalString(this SecureString value)
|
|
|
|
|
{
|
|
|
|
|
var valuePtr = IntPtr.Zero;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
|
2020-04-04 22:15:55 +00:00
|
|
|
|
return Marshal.PtrToStringUni(valuePtr) ?? "";
|
2020-04-04 14:33:58 +00:00
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-05 20:59:47 +00:00
|
|
|
|
|
|
|
|
|
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> coll, int size)
|
|
|
|
|
{
|
|
|
|
|
var lst = new List<T>();
|
|
|
|
|
foreach (var itm in coll)
|
|
|
|
|
{
|
|
|
|
|
lst.Add(itm);
|
|
|
|
|
if (lst.Count != size) continue;
|
|
|
|
|
|
|
|
|
|
yield return lst;
|
|
|
|
|
lst = new List<T>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lst.Count > 0 && lst.Count != size)
|
|
|
|
|
yield return lst;
|
|
|
|
|
}
|
2020-06-20 22:51:47 +00:00
|
|
|
|
|
2020-05-21 22:12:51 +00:00
|
|
|
|
private static Random _random = new Random();
|
|
|
|
|
public static int NextRandom(int min, int max)
|
|
|
|
|
{
|
|
|
|
|
return _random.Next(min, max);
|
|
|
|
|
}
|
2019-07-21 04:40:54 +00:00
|
|
|
|
}
|
2019-11-20 00:15:46 +00:00
|
|
|
|
}
|