分享时间 | 2022-08-27 12:44 |
最后更新 | 2023-06-22 15:15 |
修订版本 | 12 |
用户许可 | -未设置- |
Quicker版本 | 1.38.22 |
动作大小 | 46.5 KB |
快速使用:
1. 运行动作会提示输入需要导入的文件夹路径
2. 关闭窗口后, 再次运行动作就会导入到 Eagle 了.
右键菜单选项:
1. 导入路径设置: 可以重新打开上面的路径设置窗口.
2. 移入回收站: 可以把设置的路径内所有文件都移入回收站. (按住 Ctrl 不松运行动作)
3. 点击关闭\开启自动移入回收站: 导入完毕后自动移入回收站.
动作制作记录:
2022-08-27 12:58:28: 因为暂时无法检测到 Eagle 是否导入完成, 怕误删文件, 所以自动删除就做不了, 就做了右键菜单中手动的移入回收站.
Eagle 有自动导入选项, 需要把文件移动到 Eagle 中设置的文件夹内, 然后 Eagle 自己再导入.
一个是比较消耗固态硬盘寿命, 一个是导入的时间会翻倍, 做成动作要用户设置自动导入的路径也比较麻烦, 不适合写到动作步骤中.
2022-09-28 21:47:56: 通过监视 Eagle Log 文件实现了自动删除功能.
2022-10-02 01:03:17: 导入 1000 张图测试, 在第 620 张图的时候触发了移入回收站, 原因是目前检测的 log 词条出现了. 目前只在这么大数量的导入上出现过这个问题.
如果您出现了文件被移入了回收站导致导入失败一部分, 请不要关闭 Eagle 的导入失败界面. 先去回收站把文件还原, 再点击全部重试就行. 之后在动作右键菜单中手动删除.
以后我再看看有没有其他办法能够稳定的实现这个功能, 导入大量文件也能用.
2022-10-02 16:13:00: 已修复导入大量文件时自动移入回收站提早触发的问题, 判断改为日志一秒未发生变化, 出现了指定的字符.
2022-10-09 17:06:10: 尝试修复导入 Eagle 在后台时不会触发自动删除的问题, 一共有3条日志出现时机极高概率是导入完成的时机. 为了避免出现了这些日志但还处于导入状态误删的问题. 加了判断, 后面是否还有 Add 日志的出现, 有就判断还未导入完成.
2023-06-22 15:17:16: 尝试修复日志格式不符后导入自动删除报错的问题
检测导入成功 C# 代码:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace EagleAPI
{
public static class Eagle
{
public static void Main()
{
Console.WriteLine(WatchEagleLogFile(DateTimeOffset.Now.ToUnixTimeMilliseconds()) ? "导入完成" : "超时");
}
/// <summary>
/// 通过监视 Eagle 的日志文件, 等待 Eagle 导入完成.
/// </summary>
/// <param name="startUnixTime">获取当前时间戳, 后面获取的 log 行只获取这个时间之后的.</param>
/// <param name="refreshInterval">间隔时间 毫秒</param>
/// <param name="timeAllow">检测生效间隔 毫秒 (即使检测到了, 日志必须有多长时间没变化才算数)</param>
/// <param name="timeout">超时时间 毫秒 (日志有多长时间没有变化)</param>
/// <param name="readLine">读取行数 (这个是最终监测行数)</param>
/// <returns>Eagle 导入完成, 返回 true; 超时, 返回 false</returns>
private static bool WatchEagleLogFile(long startUnixTime, int refreshInterval = 1000, int timeAllow = 1000, int timeout = 30000, int readLine = 100)
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Eagle\\Log.log";
// 读取行数 (这个是用来监测日志是否变化的行数, 为了节约性能这样设计的)
var readLineTemp = 2;
// 在日志文件中, 出现此字符串说明没在导入状态.
// (2022-10-2 14:46:42 导入600张的样子就失效了, 需要加间隔判断才能稳定)
const string infoIpcUpdateCollectModal = "[ipc] update-collect-modal";
const string infoBgNoChangesIgnoreUpdateCache = "[info] [bg] No Changes, ignore update cache.";
const string infoBgUpdateLibraryCacheSuccessfully = "[info] [bg] Update library cache successfully";
const string infoBgAdd = " [info] [bg] Add [";
string[] waitStrings =
{
infoIpcUpdateCollectModal,
infoBgNoChangesIgnoreUpdateCache,
infoBgUpdateLibraryCacheSuccessfully
};
while (true)
{
Thread.Sleep(refreshInterval);
// 读取日志最后指定行数的字符串, 去掉前后空白/行
var backwardRead = BackwardRead(path, readLineTemp).Trim();
// 拆分成数组
var strArr = backwardRead.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries);
// 过滤日志
strArr = LogFilter(startUnixTime, strArr);
// 为空检测
if (strArr.Length < 1) return false;
// 获取日志未变化的时间
var noChangeInTime = ElapsedMilliseconds(strArr[0]);
// Console.WriteLine(strArr[0]);
// Console.WriteLine(noChangeInTime);
// Console.WriteLine(noChangeInTime > timeAllow);
// 开始检测时间, 如果超过了指定时间没有变化, 就读取指定行数来查找字符串是否出现.
if (noChangeInTime > timeAllow)
{
// 时间到了, 扫描多行字符串
readLineTemp = readLine;
// 判断几个导入完成的日志标记
if (waitStrings.Select(
x => FindContains(strArr, x) // 查找是否出现了的索引, 出现一个就行
).Any(
index => index != -1 // -1 就说明没有出现
&& FindContains(
strArr.Take(index).ToArray(), // 获取数组中的元素到出现的索引位置, 因为是倒序获取的, 就可以直接拿数量.
infoBgAdd // 如果不包含add就说明导入完成了.
) == -1)
)
return true;
}
// 日志更新时间超时
if (noChangeInTime > timeout) return false;
}
}
/// <summary>
/// 过滤出指定以后的日志记录
/// </summary>
/// <param name="startUnixTime">开始时间戳</param>
/// <param name="strArr">需要过滤的数组</param>
/// <returns>返回过滤后的新数组</returns>
private static string[] LogFilter(long startUnixTime, IEnumerable<string> strArr)
{
var newArr = new List<string>();
foreach (var s in strArr)
{
if (!Regex.IsMatch(s, @"^\[\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d\.\d{3}\].+")) continue;
if (LogTimeToUnixTimeMilliseconds(s) > startUnixTime)
{
newArr.Add(s);
}
else break;
}
return newArr.ToArray();
}
/// <summary>
/// 获取时间经过毫秒数 (当前时间与日志时间的差)
/// </summary>
/// <param name="str">单行日志的字符串</param>
/// <returns>返回时间差, 毫秒</returns>
private static long ElapsedMilliseconds(string str)
{
// 获取当前时间戳
var logUnixTime = LogTimeToUnixTimeMilliseconds(str);
// 获取时间经过毫秒数
var elapsedMillisecond = DateTimeOffset.Now.ToUnixTimeMilliseconds() - logUnixTime;
return elapsedMillisecond;
}
/// <summary>
/// 日志时间转毫秒级时间戳
/// </summary>
/// <param name="str">需要转换的字符串</param>
/// <returns>返回时间戳</returns>
private static long LogTimeToUnixTimeMilliseconds(string str)
{
// 提取时间字符串
var time = str.Substring(1, 23);
// 设置字符串转时间格式
var format = new DateTimeFormatInfo {ShortDatePattern = "yyyy-MM-dd HH:mm:ss.fff"};
// 字符串转时间
var dateTime = Convert.ToDateTime(time, format);
// 取时间戳
var logUnixTime = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
return logUnixTime;
}
/// <summary>
/// 查找指定子字符串出现的索引
/// </summary>
/// <param name="strArr">字符串数组</param>
/// <param name="value">查找的值</param>
/// <returns>返回查找到的索引</returns>
private static int FindContains(IReadOnlyList<string> strArr, string value)
{
for (var i = 0; i < strArr.Count; i++)
{
if (strArr[i].Contains(value)) return i;
}
return -1;
}
/// <summary>
/// 反向读取文本
/// </summary>
/// <param name="path">文本文件路径</param>
/// <param name="length">读取行数</param>
/// <returns>返回读取的字符串</returns>
private static string BackwardRead(string path, long length)
{
// 文件流: 开
var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// 流位置从 0 开始的, length 从 1 开始, 多了 1 需要减掉.
var position = fs.Length - 1;
// 用于存储读出来的所有字符串
var sb = new StringBuilder(1024);
// 读取 10 行
for (var i = 0; i < length; i++)
{
// 反向读取一行
position = BackwardReadLine(fs, position, ref sb, true);
// 到底了就跳出循环
if (position <= 0) break;
}
// 文件流: 关
fs.Close();
// 返回读取的字符串
return sb.ToString();
}
/// <summary>
/// 反向读取一行文本 (碰到 \r\n 为一行)
/// </summary>
/// <param name="fs">文件流</param>
/// <param name="position">流中的位置, 也就是开始位置</param>
/// <param name="sb">需要插入到的总字符串</param>
/// <param name="reverse">false 插入到开头, 原文件顺序; true 插入到末尾, 原文件最末尾一行变成第一行</param>
/// <param name="maxRead">单行最大容量限制, 单位为字节</param>
/// <returns>返回当前流中的位置 </returns>
private static long BackwardReadLine(Stream fs, long position, ref StringBuilder sb, bool reverse = false, int maxRead = 10240)
{
// 空文件, 直接返回.
if (fs.Length == 0) return 0;
//回车符
const byte n = 13;
//换行符
const byte r = 10;
// 读取一个字节
var readByte = 0;
// 读取长度计数
var readLengthCount = 0;
// 存储读取的字节
var arr = new List<byte>();
// 存储读取完一行的文本
while (readByte != n && readByte != r)
{
// (位置到底 || 到达单行最大读取长度限制) 就跳出
if (position <= 0 || readLengthCount >= maxRead) break;
// 修改流中的位置为传进来的位置 (当前位置)
fs.Position = position;
// 读取一个字节
readByte = fs.ReadByte();
// 返回 -1 就是没读到, 没读到就跳出.
if (readByte == -1) break;
// 将读取的值插入到数组
arr.Insert(0, (byte) readByte);
// 流中的位置向前移动 1.
position--;
// 读取长度 +1
readLengthCount++;
// 问题: 调用两次方法, 进两次循环, 才能读到一行. 我打算读 10 行结果只读了 5 行.
// 分析: Windows 换行是 \r\n 两个字符, 如果检测到其中一个就会跳出循环, 这里有两个连在一起, 就需要循环两次.
// 解决: 先多读一个字节, 看看是不是 \r 或 \n, 如果是就一起读进去, 如果不是就说明读完一行了, 得跳出循环了.
if (readByte == r || readByte == n)
{
// 重新给一下上面自减后的位置
fs.Position = position;
// 读一个字节
readByte = fs.ReadByte();
// 没得读了就跳出循环.
if (readByte == -1) break;
// 如果是 \r 或 \n , 就插入到这一行的数组中去, 位置自减
if (readByte == r || readByte == n)
{
// 进不来, 就不会修改下面这些变量的值了.
arr.Insert(0, (byte) readByte);
position--;
}
// 如果不是, 就说明这一行读完了, 因为前面 if 读到了 \r 或者 \n 才进来的, \r\n 分割行, 这里就直接跳出循环了.
else break;
}
// 如果不是 \r 或 \n 就接着下一次循环获取, 是 \r 或者 \n 就结束循环.
}
// List<byte> 转数组, 转 UTF8 的字符串
var str = Encoding.UTF8.GetString(arr.ToArray());
// 插入到总字符串的首个位置
sb.Insert(reverse ? sb.Length : 0, str);
// 返回下一个需要读取的位置
return position;
}
}
}
修订版本 | 更新时间 | 更新说明 |
---|---|---|
12 | 2023-06-22 15:15 | 尝试修复日志格式不符后导入自动删除报错的问题 |
11 | 2022-10-09 17:02 | 修复: 导入时 Eagle 在后台自动删除失效的问题. |
10 | 2022-10-03 01:12 | 修改: 检测间隔修改为3秒 |