首页
iYoRoy DN42 Network
关于
友情链接
推荐
悠笙の喵罐头
Search
1
Docker下中心化部署EasyTier
1,436 阅读
2
给Android 4.9内核添加KernelSU支持
958 阅读
3
记一次为Android 4.9内核的ROM启用erofs支持
279 阅读
4
在TrueNAS上使用Docker安装1Panel
259 阅读
5
为黑群晖迁移RR引导盘
236 阅读
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
登录
Search
标签搜索
网络技术
Linux
BGP
BIRD
C&C++
DN42
Android
Docker
AOSP
Windows
MSVC
内部路由协议
OSPF
iBGP
Clearnet
ASN
服务
DNS
TrueNAS
Web
神楽悠笙
累计撰写
21
篇文章
累计收到
10
条评论
首页
栏目
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
页面
iYoRoy DN42 Network
关于
友情链接
推荐
悠笙の喵罐头
搜索到
6
篇与
的结果
跨平台服务编写日记 Ep.2 进程间通讯(IPC)
前情提要 上一篇文章实现了统一的日志管理,这篇文章来实现进程间消息通讯,即IPC。 分析 Windows 在Windows下,主要通过管道(Pipe)实现进程间通讯。管道又分为命名管道(Named Pipe)和匿名管道(Anonymous Pipe)。其中,匿名管道是单向的,通常在父进程和子进程之间通讯[2]。而命名管道则可以是单向管道或双工管道,并且支持一对多通讯[3]。顾名思义,识别命名管道的唯一方式是它的名称,因此两个进程只要都连接到同一个名字的命名管道即可实现通信。 我们需要实现进程间的双向通讯,因此采用命名管道。大致思路就是:进程作为伺服模式,也就是接收端启动时创建一个线程,创建一个命名管道并监听管道内消息。当管道被连接时从中读取管道内数据;当进程作为发送端启动时尝试连接到同一个名称的管道,并写入消息内容。 Linux 在Linux下通常使用socket进行进程间通讯。不过不同于监听端口,进程间通讯一般会选择监听一个sock文件[5],常见的服务类应用如docker daemon、mysql都是通过这种方式。 因此,大致思路如下:作为伺服模式启动的进程创建一个socket监听,并等待从中接收消息;发送端连接到socket套接字并发送消息。和上文命名管道的名称类似,socket套接字会映射一个唯一的.sock文件,发送方只要打开这个文件即可发送消息。(实际上打开方式不是常规的打开文件,而是用socket专用的打开方式[5]) 代码实现 初始化 为了实现共用一套主代码,我使用了和上一篇文章中一样的通过宏定义区分系统类型的方案,将Windows和Linux的代码分别写在service-windows.h和service-linux.h两个头文件中: #ifdef _WIN32 #include "service-windows.h" #elif defined(__linux__) #include "service-linux.h" #endif 当接收端进程启动时,创建一个线程处理收信息(使用std::thread作为多线程库): thread_bind = std::thread(bind_thread_main); 监听部分 Windows 在Windows下,只要尝试从指定名称的命名管道读取数据即可。其中,因为设置了管道为等待模式(即下文中CreateNamedPipe的第三个参数DWORD dwPipeMode中设置了PIPE_WAIT),ConnectNamedPipe会是阻塞模式,因此不用担心不断循环造成的性能损失。 void bind_thread_main() { while (!exit_requested.load()) { HANDLE hPipe = CreateNamedPipe( PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, // Output buffer size 1024, // Input buffer size 0, // Default timeout NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_WARN, "Failed to create pipe: %d", GetLastError()); continue; } if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) { char buffer[1024]; DWORD bytesRead; if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) { buffer[bytesRead] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } FlushFileBuffers(hPipe); DisconnectNamedPipe(hPipe); CloseHandle(hPipe); } else { CloseHandle(hPipe); } } } Linux 为了防止创建失败,在创建前会先尝试删除没有清理干净的sock文件,即代码中unlink(SOCKET_PATH)。SOCKET_PATH为全局变量,定义了套接字文件的路径。创建套接字时,指定family为AF_UNIX代表创建UNIX套接字,即.sock文件的这种类型(如果是网络套接字就是AF_INET)。timeval这段代码设置了一个超时限制,当accept函数等待时间超过设置的SOCKET_TIMEOUT超时时间(单位秒)后会自动结束阻塞并返回错误信息。创建完套接字后按照正常流程设置绑定和监听即可。 void bind_thread_main() { unlink(SOCKET_PATH); int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd == -1) { service_log.push(LEVEL_FATAL, "Failed to create socket"); exit_requested.store(true); return; } struct timeval tv; tv.tv_sec = SOCKET_TIMEOUT; tv.tv_usec = 0; setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_FATAL, "Bind failed"); close(server_fd); exit_requested.store(true); return; } if (listen(server_fd, 5) == -1) { service_log.push(LEVEL_FATAL, "Listen failed"); close(server_fd); exit_requested.store(true); return; } while (!exit_requested.load()) { int client_fd = accept(server_fd, nullptr, nullptr); if (client_fd != -1) { char buffer[1024]; int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } close(client_fd); } else { if (errno == EWOULDBLOCK || errno == EAGAIN) { continue; } service_log.push(LEVEL_WARN, "Failed to accept socket connection"); } } } 当读取到消息后,两份代码都会将消息保存至阻塞队列m_queueMsg中。 发送部分 Windows 打开指定管道并写入消息内容即可: bool send_message(const std::string& msg) { if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) { service_log.push(LEVEL_ERROR, "Failed to find valid pipe: %d", GetLastError()); return false; } HANDLE hPipe = CreateFile( PIPE_NAME, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_ERROR, "Failed to connect: %d", GetLastError()); return false; } DWORD bytesWritten; if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &bytesWritten, NULL)) { service_log.push(LEVEL_VERBOSE, "Message sent: %s", msg.c_str()); CloseHandle(hPipe); return true; } else { service_log.push(LEVEL_ERROR, "Message (%s) send failed: %d", msg.c_str(),GetLastError()); CloseHandle(hPipe); return false; } } Linux 同理,连接套接字,发送数据: bool send_message(const std::string& msg) { int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { service_log.push(LEVEL_ERROR, "Failed to create socket"); return false; } sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_ERROR, "Connect failed"); close(sock); return false; } if (write(sock, msg.c_str(), msg.size()) == -1) { service_log.push(LEVEL_ERROR, "Message send failed: %s", msg.c_str()); close(sock); return false; } else { service_log.push(LEVEL_VERBOSE, "Message sent success: %s", msg.c_str()); close(sock); return true; } } 清理 Windows下没什么需要清理的,Linux下删除套接字文件即可: unlink(SOCKET_PATH); 效果示意图 Windows Linux 样例代码下载: IPCTest.zip 参考文章: https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes https://www.cnblogs.com/alantu2018/p/8493809.html https://blog.csdn.net/dog250/article/details/100998838
2025年05月19日
86 阅读
0 评论
1 点赞
跨平台服务编写日记 Ep.1 统一的日志管理
前阵子心血来潮,想为在使用的一个跨平台的控制台服务类应用程序编写一个自己的管理程序,加一些功能。因而设计了一套简单的服务运行流程。 {alert type="warning"} 本系列文章中的观点和方案为我根据自己已有的知识储备结合DeepSeek的帮助所归纳设计,并未经过严格测试,并不保证在生产环境中使用的可行性和稳定性 {/alert} 大致思路 大致分为个线程,分别用作: 日志记录 目标应用实例管理,可能不止一个线程 监听IPC消息 处理IPC收到的消息(主进程) 本文着重讨论的是日志记录部分。 编写思路 为什么要给日志记录单开一个线程,个人考虑是因为本身就是多线程的架构,需要编写一个统一的日志记录模块。如果每个线程单独打印,则很有可能出现两个线程同时写入文件或者同时输出到控制台,造成日志混乱。 因此,日志记录大致思路就是: 定义一个队列,存储日志内容和等级 创建一个线程,不断地从线程中取出元素,根据设定的日志等级决定是否打印到控制台或者输出到文件 外部push日志内容到队列中 一些细节上的内容 保证可移植性,尽量使用STL库编写,如使用std::thread而不是pthread 保证线程安全,需要使用互斥锁之类的保护相应变量 让日志队列为空时线程等待,想到编写一个类似于Java下BlockingQueue的阻塞队列 指定一个日志等级,超过这个等级的日志才会被保存或者打印 通过va_list实现不定参数,使日志记录有sprintf的的使用体验 开始编写 有了上述思路,整体编写就很简单了。 BlockingQueue 偷懒了,这部分直接让DeepSeek写的 为了实现一个多线程安全的阻塞队列,当队列为空时调用front()会阻塞直到其他线程添加元素,我们可以结合互斥锁(std::mutex)和条件变量(std::condition_variable)来同步线程操作。 代码实现 互斥锁(std::mutex) 所有对队列的操作(push、front、pop、empty)都需要先获取锁,确保同一时间只有一个线程能修改队列,避免数据竞争。 条件变量(std::condition_variable) 当调用front()时,如果队列为空,线程会通过cv_.wait()释放锁并阻塞,直到其他线程调用push()添加元素后,通过cv_.notify_one()唤醒一个等待线程。 cv_.wait()需配合std::unique_lock,并在等待时自动释放锁,避免死锁。 使用谓词检查([this] { return !queue_.empty(); })防止虚假唤醒。 元素获取与移除 front()返回队首元素的拷贝(而非引用),确保调用者获得数据时队列的锁已释放,避免悬空引用。 pop()需显式调用以移除元素,确保队列状态可控。 #include <queue> // 队列 #include <mutex> // 互斥锁 #include <condition_variable> // 条件变量 template<typename T> class BlockingQueue { public: // 向队列中添加元素 void push(const T& item) { std::lock_guard<std::mutex> lock(mtx_); queue_.push(item); cv_.notify_one(); // 通知一个等待的线程 } // 获取队首元素(阻塞直到队列非空) T front() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); // 阻塞直到队列非空 return queue_.front(); } // 获取队首元素并移除 T take() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); T item = std::move(queue_.front()); // 移动语义避免拷贝 queue_.pop(); return item; } // 移除队首元素(需外部调用,非阻塞) void pop() { std::lock_guard<std::mutex> lock(mtx_); if (!queue_.empty()) { queue_.pop(); } } // 检查队列是否为空 bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } private: mutable std::mutex mtx_; // 互斥锁 std::condition_variable cv_; // 条件变量 std::queue<T> queue_; // 内部队列 }; Log类 Log.h #pragma once #include <iostream> #include <fstream> #include <cstring> #include <thread> #include <chrono> #include <mutex> #include <cstdio> #include <cstdarg> #include <atomic> #include "BlockingQueue.h" enum LogLevel { LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF }; struct LogMsg { short m_LogLevel; std::string m_strTimestamp; std::string m_strLogMsg; }; class Log { private: std::ofstream m_ofLogFile; // 日志文件输出流 std::mutex m_lockFile; // 文件操作互斥锁 std::thread m_threadMain; // 后台日志处理线程 BlockingQueue<LogMsg> m_msgQueue; // 线程安全阻塞队列 short m_levelLog, m_levelPrint; // 文件和控制台日志级别阈值 std::atomic<bool> m_exit_requested{ false }; // 线程退出标志 std::string getTime(); // 获取当前时间戳 std::string level2str(short level, bool character_only); // 级别转字符串 void logThread(); // 后台线程函数 public: Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO); ~Log(); void push(short level, const char* msg, ...); // 添加日志(支持格式化) void set_level(short loglevel, short printlevel); // 设置日志级别 bool open(std::string filename); // 打开日志文件 bool close(); // 关闭日志文件 }; Log.cpp #include "Log.h" std::string Log::getTime() { using sc = std::chrono::system_clock; std::time_t t = sc::to_time_t(sc::now()); char buf[20]; #ifdef _WIN32 std::tm timeinfo; localtime_s(&timeinfo,&t); sprintf_s(buf, "%04d.%02d.%02d-%02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ); #else strftime(buf, 20, "%Y.%m.%d-%H:%M:%S", localtime(&t)); #endif return buf; } std::string Log::level2str(short level, bool character_only) { switch (level) { case LEVEL_VERBOSE: return character_only ? "V" : "Verbose"; case LEVEL_WARN: return character_only ? "W" : "Warning"; case LEVEL_ERROR: return character_only ? "E" : "Error"; case LEVEL_FATAL: return character_only ? "F" : "Fatal"; } return character_only ? "I" : "Info"; } void Log::logThread() { while (true) { LogMsg front = m_msgQueue.take(); // 阻塞直到有消息 // 处理文件写入 if (front.m_LogLevel >= m_levelLog) { std::lock_guard<std::mutex> lock(m_lockFile); // RAII 管理锁 if (m_ofLogFile) { m_ofLogFile << front.m_strTimestamp << ' ' << level2str(front.m_LogLevel, true) << ": " << front.m_strLogMsg << std::endl; } } // 处理控制台打印 if (front.m_LogLevel >= m_levelPrint) { printf("%s %s: %s\n", front.m_strTimestamp.c_str(), level2str(front.m_LogLevel, true).c_str(), front.m_strLogMsg.c_str()); } // 检查退出条件:队列为空且标志为真 if (m_exit_requested.load() && m_msgQueue.empty()) break; } return; } Log::Log(short default_loglevel, short default_printlevel) { set_level(default_loglevel, default_printlevel); m_threadMain = std::thread(&Log::logThread, this); } Log::~Log() { m_exit_requested.store(true); m_msgQueue.push({ LEVEL_INFO, getTime(), "Exit." }); // 唤醒可能阻塞的线程 if (m_threadMain.joinable()) m_threadMain.join(); close(); // 确保文件关闭 } void Log::push(short level, const char* msg, ...) { va_list args; va_start(args, msg); const int len = vsnprintf(nullptr, 0, msg, args); va_end(args); if (len < 0) return; std::vector<char> buf(len + 1); va_start(args, msg); vsnprintf(buf.data(), buf.size(), msg, args); va_end(args); m_msgQueue.push({level,getTime(),buf.data()}); } void Log::set_level(short loglevel, short printlevel) { m_levelLog = loglevel; m_levelPrint = printlevel; } bool Log::open(std::string filename) { m_lockFile.lock(); m_ofLogFile.open(filename.c_str(), std::ios::out); m_lockFile.unlock(); return (bool)m_ofLogFile; } bool Log::close() { m_lockFile.lock(); m_ofLogFile.close(); m_lockFile.unlock(); return false; } 说明 类/结构说明 LogLevel 枚举 定义日志级别:VERBOSE, INFO, WARN, ERROR, FATAL, OFF。 OFF不应被用于记录的日志等级,仅用于当需要关闭日志记录时将阈值设置为该项以实现所有日志都不记录 LogMsg 结构体 封装日志消息: m_LogLevel:日志级别。 m_strTimestamp:时间戳字符串。 m_strLogMsg:日志内容。 成员变量说明 变量 说明 m_ofLogFile 文件输出流,用于写入日志文件。 m_lockFile 互斥锁,保护文件操作。 m_threadMain 后台线程,处理日志消息的消费。 m_msgQueue 阻塞队列,存储待处理的日志消息。 m_levelLog 写入文件的最低日志级别(高于此级别的消息会被记录)。 m_levelPrint 打印到控制台的最低日志级别。 m_exit_requested 原子标志,控制日志线程退出。 函数说明 函数 说明 getTime 获取当前时间戳字符串(跨平台实现)。 level2str 将日志级别转换为字符串(如 LEVEL_INFO → "I" 或 "Info")。 logThread 后台线程函数:消费队列消息,写入文件或打印。 构造函数 初始化日志级别,启动后台线程。 析构函数 设置退出标志,等待线程结束,确保处理剩余消息。 push 格式化日志消息(支持可变参数)并推入队列。 set_level 动态设置日志级别和打印级别。 open/close 打开/关闭日志文件。 完整代码及测试样例下载: demo.zip
2025年04月19日
58 阅读
0 评论
2 点赞
Docker下中心化部署EasyTier
EasyTier本身是个去中心化的p2p工具,任意节点都可作为转发服务器使用。但是每个节点的配置文件都得手动编写,从tailscale迁移过来时觉得有点不习惯。再加上摸索阶段经常需要修改配置文件的内容,故打算还是中心化部署EasyTier的Dashboard,统一管理设备。 项目仓库:https://github.com/easytier/easytier 官方文档并未提供单独部署config-server的方式,但是实际上也不难,服务端已经包含在下载下来的二进制文件中了。本文着重论述通过Docker Compose安装,需要二进制模式安装的可参考下方参考文章。 分析 dashboard的部署主要分为两个部分,一个是后端RESTful API,一个是前端web控制台。其中Release里的easytier-web-embed同时提供了这俩,因此只要运行此二进制文件即可实现功能。 开整 部署API和Web控制台 Docker部署,没什么好说的。 需要开放两个端口: 11211/tcp: API接口,HTTP 22020/udp: 用于客户端(easytier-core)和服务器通信 目录映射需要映射容器内/app文件夹,用作存储持久化数据。 Compose文件如下: services: easytier: restart: always hostname: easytier volumes: - /opt/easytier/api:/app ports: - "127.0.0.1:11211:11211" - "22020:22020/udp" environment: - TZ=Asia/Shanghai image: easytier/easytier:latest entrypoint: easytier-web-embed 此处镜像和官方文档的Docker部署客户端用的是同一个,默认入口点是easytier-core,因此运行webapi需要指定entrypoint为easytier-web-embed。 因为API接口需要HTTPS,故此处没有直接将11211端口暴露公网,而是暴露到127.0.0.1再使用反向代理实现HTTPS。 设置反向代理 我使用的是1Panel,因此直接在面板中创建一个网站,反向代理到设置的API端口即可。 注册控制台账号 完成部署后打开https://你的域名(如果是内建控制台版本则不需要加/web/),将Api Host改为https://你的域名,注意填Api Host的时候URL末尾不要带“/”,否则会出莫名其妙的问题。点击下方Register注册一个账号 再使用这个账号登录即可进入控制台。 客户端配置 将启动参数全部删除,仅保留--config-server udp://你的ip:22020/你的用户名即可。运行easytier-core,再回到控制台即可看到设备。 点击右边的设置按钮,点击Create为其创建网络,接下来的步骤就和本地GUI模式操作一样了,这里就不赘述了。保存后在network栏选择新建的网络即可加入。 因为Docker重启容器内数据会丢失,Docker下部署客户端时需要映射一个文件到容器内/usr/local/bin/et_machine_id用作保存machine id,否则每次重启后都需要重新为其配置网络。同时,给容器设置hostname可以作为设备在web控制台显示的名称。 这里贴一下我的compose: services: easytier: command: '--config-server udp://<ip>:22020/KaguraiYoRoy' environment: - TZ=Asia/Shanghai hostname: truenas image: easytier/easytier:latest labels: com.centurylinklabs.watchtower.enable: 'true' mem_limit: 0m network_mode: host privileged: True restart: always volumes: - >- /mnt/systemdata/DockerData/easytier/app/et_machine_id:/usr/local/bin/et_machine_id watchtower: command: '--interval 3600 --cleanup --label-enable' environment: - TZ=Asia/Shanghai - WATCHTOWER_NO_STARTUP_MESSAGE image: containrrr/watchtower restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock 参考: https://blog.mitsea.com/1a57bda595c580088006c17d6ba2a744/ https://github.com/EasyTier/EasyTier/issues/722 https://github.com/EasyTier/EasyTier/issues/577 https://github.com/EasyTier/EasyTier/pull/718
2025年04月15日
1,436 阅读
0 评论
4 点赞
通过Alist使TrueNAS同步到OneDrive
背景 手上有个E5订阅,本来用的方案是Docker运行driveone/onedrive:edge的方式来实现同步,但是这个方案一个是这种方式没有GUI/WebUI,一个是每次同步的时候都会占用掉CPU 25%-50%的性能。考虑到TrueNAS自带的同步方案可以向WebDAV同步,因此想到能用Alist来挂载OneDrive并转换成WebDAV供TrueNAS挂载。 折腾过程 安装Alist 为Alist创建持久化存储文件夹,并根据Alist官方文档编写Docker Compose: services: alist: environment: - PUID=3000 - PGID=950 - UMASK=022 image: xhofe/alist:latest ports: - '8088:5244' restart: always volumes: - /mnt/systemdata/DockerData/alist/etc:/opt/alist/data - /mnt/data/Storage:/mnt/data 此处我将Alist端口开放在8088,其中映射/mnt/data/Storage是为了让Alist可以管理本地的存储;映射/mnt/systemdata/DockerData/alist/etc作为存储Alist数据的文件夹。 关于如何配置Alist上的OneDrive本文不做讨论,请查询Alist官方文档。此处我将我的OneDrive挂载在/OneDrive。 完成后进入Alist后台-用户,编辑你的用户或者创建一个新用户,勾选Webdav 读取、Webdav 管理以使得该用户可以使用WebDAV。 配置TrueNAS同步 进入TrueNAS后台-Credentials-Backup Credentials,添加一个Cloud Credential,参数如下: Provider: WebDAV Name: 自定义 URL: Alist地址+/dav,例如我这里填写http://127.0.0.1:8088/dav WebDAV Service: OTHER Username和Password: Alist账号密码 Verify Credential确认没问题之后保存。 接着进入TrueNAS后台-Data Protection,添加一个Cloud Sync Task,Provider下的Credentials选择刚刚创建的Alist的WebDAV,点击下一步。此处的参数有很多种,详解如下: Direction: 分为PULL和PUSH,分别对应云端同步到本地和本地同步到云端 Transfer Mode: COPY: 复制文件,若源文件夹中先前有的文件后来删除了云端的不会被删除 MOVE: Copy后删除源文件夹相关文件 SYNC: 保持源文件夹和目标文件夹同步,源文件夹删除的文件也会在目标里删除 Directory/Files: 即本地需要同步的文件或文件夹 Folder: 即云端需要同步的文件夹 Description: 注释 Schedule: Cron定时,可以使用他预设的时段或者自己编写 比如我这里选择的是PUSH,SYNC,从/mnt/data/Storage同步到/OneDrive/TrueNAS,每天0:00执行。 编辑完成后保存,即可在你设置的时段自动将本地文件上传到OneDrive。 旧方案的项目地址: https://github.com/abraunegg/onedrive 参考文章: https://alist.nn.ci/zh/guide/install/docker.html https://alist.nn.ci/zh/guide/drivers/onedrive.html
2025年03月13日
150 阅读
0 评论
0 点赞
在TrueNAS上使用Docker安装1Panel
背景 家里TrueNAS性能剩余,想着部署个web服务。想要装个面板减少点工作量,又考虑到虚拟机性能折损和zfs cache对内存的要求比较大以及NAS本身性能也不好,便打算使用docker部署。再加上1Panel本身也是以docker为介质,二者共同控制TrueNAS宿主机的docker daemon,约等于直接将网站部署到TrueNAS本地并且也便于管理。 分析 {alert type="warning"} 本文默认TrueNAS可以访问dockerhub并且已经配置好了docker daemon {/alert} 环境信息 Storage Pool 存在两个存储池: /mnt/data: 1 x MIRROR | 2 wide | 2.73 TiB | HDD /mnt/systemdata: 1 x DISK | 1 wide | 223.57 GiB | SSD 其中docker数据存储在2号存储池中。 Datasets 存在三个数据集: Storage: 位于data存储池,存放冷数据 DockerData: 位于systemdata存储池,存放容器的持久化存储文件 KaguraiYoRoy: 位于systemdata,用户home文件夹 安装1Panel 使用了moelin/1panel:latest镜像部署,此步骤很多部分都可以参考镜像作者写的README。项目地址: {cloud title="okxlin/docker-1panel" type="github" url="https://github.com/okxlin/docker-1panel" password=""/} 在DockerData数据集创建了一个文件夹专门用于存储1panel数据,即用作容器内/opt/1panel,位于/mnt/systemdata/DockerData/1panel。 持久卷 因为要允许1Panel管理宿主机docker,因此需要映射/var/run/docker.sock和宿主的docker文件夹 映射上文为其创建的数据文件夹 TrueNAS的docker文件夹和一般的Linux位置不一样,一般的Linux位于/var/lib/docker而TrueNAS的位于/mnt/.ix-apps/docker。 环境变量和端口映射 环境变量和镜像作者的设置相同,传入TZ=Asia/Shanghai;端口映射根据需要自行设置即可,容器内端口为10086。 Docker Compose 有了上述信息,编写docker compose就容易许多了。 完整Docker Compose文件如下: services: 1panel: dns: - 223.5.5.5 environment: - TZ=Asia/Shanghai image: moelin/1panel:latest labels: createdBy: Apps ports: - '8085:10086' restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock - /mnt/.ix-apps/docker:/var/lib/docker - /mnt/systemdata/DockerData/1panel/opt:/opt/1panel - /mnt/systemdata/DockerData/1panel/root:/root - /etc/docker:/etc/docker 这里映射/root是因为我需要在容器内运行Git,而Git config存储在/root下; 设置dns是因为1Panel制作环境镜像的时候需要连网下载数据,不指定dns会报错。 安装完成后访问你设置的端口即可。 1Panel基础信息: 默认账户:1panel 默认密码:1panel_password 默认入口:entrance 故障处理 Docker镜像源 实际测试的时候发现如果不设置镜像源,即使配置了Proxy,在安装PHP环境的时候也会报错~~,并且配置了镜像源同时配置了Proxy也会安装失败,不清楚为什么~~ 打开TrueNAS的/etc/docker/daemon.json并添加registry-mirrors: { "data-root": "/mnt/.ix-apps/docker", "default-address-pools": [ { "base": "172.17.0.0/12", "size": 24 } ], "exec-opts": [ "native.cgroupdriver=cgroupfs" ], "iptables": true, "registry-mirrors": [ "https://docker.1panel.live" ], "storage-driver": "overlay2" } 保存,重启主机docker,再去1Panel里安装环境。 {alert type="warning"} 这一步的配置重启后会丢失,尽量一次性安装好环境和需要的app {/alert} 1Panel创建的容器无法启动 这个是因为在1Panel中,默认存储数据的文件夹是我们所映射出来的/opt/1panel,但是实际上创建的容器运行在TrueNAS里,访问的是TrueNAS里不存在的/opt/1panel,并且其/opt默认是只读的,因此在启动容器的时候会报错Read-only filesystem。 我自己的解决方案也很简单粗暴,在TrueNAS主机中先挂载/opt为可读写,再创建一个软链接指向1Panel的数据文件夹。 cd /opt mount -o remount,rw /opt ln -s /mnt/systemdata/DockerData/1panel/opt 1panel 然后就可以正常使用啦 有一个需要注意的点,在1Panel中安装OpenResty时记得避开使用80和443端口,这俩是TrueNAS的webui默认端口
2025年03月07日
259 阅读
0 评论
0 点赞
1
2