chsrc/include/chsrc.h
2024-08-09 02:46:25 +08:00

840 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** ------------------------------------------------------------
* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2023-2024 Aoran Zeng, Heng Guo
* -------------------------------------------------------------
* File : chsrc.h
* Authors : Aoran Zeng <ccmywish@qq.com>
* | Heng Guo <2085471348@qq.com>
* Contributors : Peng Gao <gn3po4g@outlook.com>
* |
* Created on : <2023-08-29>
* Last modified : <2024-08-09>
*
* chsrc 头文件
* ------------------------------------------------------------*/
#include "xy.h"
#include "source.h"
#define App_Name "chsrc"
#define Exit_UserCause 1
#define Exit_Unsupported 2
#define Exit_MatinerIssue 3
#define Exit_FatalBug 4
#define Exit_FatalUnkownError 5
#define chsrc_log(str) xy_log(App_Name,str)
#define chsrc_succ(str) xy_succ(App_Name,str)
#define chsrc_info(str) xy_info(App_Name,str)
#define chsrc_warn(str) xy_warn(App_Name,str)
#define chsrc_error(str) xy_error(App_Name,str)
// 2系列都是带有括号的
#define chsrc_succ2(str) xy_succ_brkt(App_Name,"成功",str)
#define chsrc_log2(str) xy_info_brkt(App_Name,"LOG",str)
#define chsrc_warn2(str) xy_warn_brkt(App_Name,"警告",str)
#define chsrc_error2(str) xy_error_brkt(App_Name,"错误",str)
#define to_red(str) xy_str_to_red(str)
#define to_blue(str) xy_str_to_blue(str)
#define to_green(str) xy_str_to_green(str)
#define to_yellow(str) xy_str_to_yellow(str)
#define to_purple(str) xy_str_to_purple(str)
#define to_bold(str) xy_str_to_bold(str)
#define to_boldred(str) xy_str_to_bold(xy_str_to_red(str))
#define to_boldblue(str) xy_str_to_bold(xy_str_to_blue(str))
#define to_boldgreen(str) xy_str_to_bold(xy_str_to_green(str))
#define to_boldyellow(str) xy_str_to_bold(xy_str_to_yellow(str))
#define to_boldpurple(str) xy_str_to_bold(xy_str_to_purple(str))
void
chsrc_note2 (const char* str)
{
xy_log_brkt (to_yellow (App_Name), to_boldyellow ("提示"), to_yellow (str));
}
void
chsrc_log_check_result (const char *check_what, const char *check_type, bool exist)
{
if (!exist)
{
xy_log_brkt (App_Name, to_boldred ("检查"), xy_strjoin (5,
to_red ("x "), check_type, " ", to_red (check_what), " 不存在"));
}
else
{
xy_log_brkt (App_Name, to_boldgreen ("检查"), xy_strjoin (5,
to_green (""), check_type, " ", to_green (check_what), " 存在"));
}
}
void
chsrc_log_cmd_result (bool result, int ret_code)
{
if (result)
{
xy_log_brkt (to_green (App_Name), to_boldgreen ("运行"), to_green ("√ 命令执行成功"));
}
else
{
char buf[8] = {0};
sprintf (buf, "%d", ret_code);
char *log = xy_2strjoin (to_red ("x 命令执行失败,返回码 "), to_boldred (buf));
xy_log_brkt (to_red (App_Name), to_boldred ("运行"), log);
}
}
bool CliOpt_IPv6 = false;
bool CliOpt_Locally = false;
bool CliOpt_InEnglish = false;
bool CliOpt_DryRun = false;
bool
is_url (const char *str)
{
return (xy_str_start_with (str, "http://") || xy_str_start_with (str, "https://"));
}
/**
* 检测二进制程序是否存在
*
* @param check_cmd 检测 `prog_name` 是否存在的一段命令,一般来说,填 `prog_name` 本身即可,
* 但是某些情况下,需要使用其他命令绕过一些特殊情况,比如 python 这个命令在Windows上
* 会自动打开 Microsoft Store需避免
*
* @param prog_name 要检测的二进制程序名
*/
bool
query_program_exist (char *check_cmd, char *prog_name)
{
char *which = check_cmd;
int ret = system(which);
// char buf[32] = {0}; sprintf(buf, "错误码: %d", ret);
if (0 != ret)
{
// xy_warn (xy_strjoin(4, "× 命令 ", progname, " 不存在,", buf));
chsrc_log_check_result (prog_name, "命令", false);
return false;
}
else
{
chsrc_log_check_result (prog_name, "命令", true);
return true;
}
}
/**
* @note 此函数只能对接受 --version 选项的程序有效
*/
bool
chsrc_check_program (char *prog_name)
{
char *quiet_cmd = xy_str_to_quietcmd (xy_2strjoin (prog_name, " --version"));
return query_program_exist (quiet_cmd, prog_name);
}
/**
* @note 此函数具有强制性,检测不到就直接退出
*/
void
chsrc_ensure_program (char *prog_name)
{
char *quiet_cmd = xy_str_to_quietcmd (xy_2strjoin (prog_name, " --version"));
bool exist = query_program_exist (quiet_cmd, prog_name);
if (exist)
{
// OK, nothing should be done
}
else
{
chsrc_error (xy_strjoin (3, "未找到 ", prog_name, " 命令,请检查是否存在"));
exit (Exit_UserCause);
}
}
bool
chsrc_check_file (char *path)
{
if (xy_file_exist (path))
{
chsrc_log_check_result (path, "文件", true);
return true;
}
else
{
chsrc_log_check_result (path, "文件", false);
return false;
}
}
/**
* 用于 _setsrc 函数检测用户输入的镜像站code是否存在于该target可用源中
*
* @param target 目标名
* @param input 如果用户输入 default 或者 def则选择第一个源
*/
#define find_mirror(s, input) query_mirror_exist(s##_sources, s##_sources_n, (char*)#s+3, input)
int
query_mirror_exist (SourceInfo *sources, size_t size, char *target, char *input)
{
if (is_url (input))
{
chsrc_error ("暂不支持对该软件使用用户自定义源,请联系开发者询问原因或请求支持");
exit (Exit_Unsupported);
}
if (0==size || 1==size)
{
chsrc_error (xy_strjoin (3, "当前 ", target, " 无任何可用源,请联系维护者"));
exit (Exit_MatinerIssue);
}
if (2==size)
{
chsrc_succ (xy_strjoin (4, sources[1].mirror->name, "", target, " 目前唯一可用镜像站,感谢他们的慷慨支持"));
}
if (xy_streql ("reset", input))
{
puts ("将重置为上游默认源");
return 0; // 返回第1个因为第1个是上游默认源
}
if (xy_streql ("first", input))
{
puts ("将使用维护团队测速第一的源");
return 1; // 返回第2个因为第1个是上游默认源
}
int idx = 0;
SourceInfo source = sources[0];
bool exist = false;
for (int i=0; i<size; i++)
{
source = sources[i];
if (xy_streql (source.mirror->code, input))
{
idx = i;
exist = true;
break;
}
}
if (!exist)
{
chsrc_error (xy_strjoin (3, "镜像站 ", input, " 不存在"));
chsrc_error (xy_2strjoin ("查看可使用源,请使用 chsrc list ", target));
exit (Exit_UserCause);
}
return idx;
}
/**
* 该函数来自 oh-my-mirrorz.py由 @ccmywish 翻译为C语言但功劳和版权属于原作者
*/
char *
to_human_readable_speed (double speed)
{
char *scale[] = {"Byte/s", "KByte/s", "MByte/s", "GByte/s", "TByte/s"};
int i = 0;
while (speed > 1024.0)
{
i += 1;
speed /= 1024.0;
}
char *buf = xy_malloc0 (64);
sprintf (buf, "%.2f %s", speed, scale[i]);
char *new = NULL;
if (i <= 1 ) new = to_red (buf);
else
{
if (i == 2 && speed < 2.00) new = to_yellow (buf);
else new = to_green (buf);
}
return new;
}
/**
* 测速代码参考自 https://github.com/mirrorz-org/oh-my-mirrorz/blob/master/oh-my-mirrorz.py
* 功劳和版权属于原作者,由 @ccmywish 修改为C语言并做了额外调整
*
* @return 返回测得的速度,若出错,返回-1
*/
double
test_speed_url (const char *url)
{
char *time_sec = "6";
/* 现在我们切换至跳转后的链接来测速,不再使用下述判断
if (xy_str_start_with(url, "https://registry.npmmirror"))
{
// 这里 npmmirror 跳转非常慢需要1~3秒所以我们给它留够至少8秒测速时间否则非常不准
time_sec = "10";
}
*/
char *ipv6 = ""; // 默认不启用
if (CliOpt_IPv6==true)
{
ipv6 = "--ipv6";
}
// 我们用 —L因为Ruby China源会跳转到其他地方
// npmmirror 也会跳转
char *curl_cmd = xy_strjoin (7, "curl -qsL ", ipv6,
" -o " xy_os_devnull,
" -w \"%{http_code} %{speed_download}\" -m", time_sec,
" -A chsrc/" Chsrc_Version " ", url);
// chsrc_info (xy_2strjoin ("测速命令 ", curl_cmd));
char *buf = xy_run (curl_cmd, 0, NULL);
// 如果尾部有换行,删除
buf = xy_str_strip (buf);
// 分隔两部分数据
char *split = strchr (buf, ' ');
if (split) *split = '\0';
// puts(buf); puts(split+1);
int http_code = atoi (buf);
double speed = atof (split+1);
char *speedstr = to_human_readable_speed (speed);
if (200!=http_code)
{
char* httpcodestr = to_yellow (xy_2strjoin ("HTTP码 ", buf));
puts (xy_strjoin (3, speedstr, " | ", httpcodestr));
}
else
{
puts (speedstr);
}
return speed;
}
int
get_max_ele_idx_in_dbl_ary (double *array, int size)
{
double maxval = array[0];
int maxidx = 0;
for (int i=1; i<size; i++)
{
if (array[i]>maxval)
{
maxval = array[i];
maxidx = i;
}
}
return maxidx;
}
/**
* 自动测速选择镜像站和源
*/
#define auto_select(s) auto_select_(s##_sources, s##_sources_n, (char*)#s+3)
int
auto_select_ (SourceInfo *sources, size_t size, const char *target)
{
if (0==size || 1==size)
{
chsrc_error (xy_strjoin (3, "当前 ", target, " 无任何可用源,请联系维护者: chsrc issue"));
exit (Exit_MatinerIssue);
}
if (CliOpt_DryRun)
{
return 1; // Dry Run 时,跳过测速
}
bool only_one = false;
if (2==size) only_one = true;
char *check_curl = xy_str_to_quietcmd ("curl --version");
bool exist_curl = query_program_exist (check_curl, "curl");
if (!exist_curl)
{
chsrc_error ("没有curl命令无法测速");
exit (Exit_UserCause);
}
double speeds[size];
double speed = 0.0;
for (int i=0; i<size; i++)
{
SourceInfo src = sources[i];
const char* url = src.mirror->__bigfile_url;
if (NULL==url)
{
if (xy_streql ("upstream", src.mirror->code))
{
continue; // 上游默认源不测速
}
else
{
chsrc_warn (xy_strjoin (3, "开发者未提供 ", src.mirror->code, " 镜像站测速链接,跳过该站点"));
speed = 0;
}
}
else
{
printf ("%s", xy_strjoin (3, "测速 ", src.mirror->site , " ... "));
fflush (stdout);
speed = test_speed_url (url);
}
speeds[i] = speed;
}
int fast_idx = get_max_ele_idx_in_dbl_ary (speeds, size);
if (only_one)
chsrc_succ (xy_strjoin (4, sources[fast_idx].mirror->name, "", target, " 目前唯一可用镜像站,感谢他们的慷慨支持"));
else
puts (xy_2strjoin ("最快镜像站: ", to_green (sources[fast_idx].mirror->name)));
return fast_idx;
}
#define use_specific_mirror_or_auto_select(input, s) \
(NULL!=(input)) ? find_mirror(s, input) : auto_select(s)
bool
source_is_upstream (SourceInfo *source)
{
return xy_streql (source->mirror->code, "upstream");
}
bool
source_is_userdefine (SourceInfo *source)
{
return xy_streql (source->mirror->code, "user");
}
bool
source_has_empty_url (SourceInfo *source)
{
return source->url == NULL;
}
/**
* 用户*只可能*通过下面三种方式来换源,无论哪一种都会返回一个 SourceInfo 出来
*
* 1. 用户指定 MirrorCode
* 2. 用户什么都没指定 (将测速选择最快镜像)
* 3. 用户给了一个 URL
*
* @dependency 变量 option
* @dependency 变量 source
*/
#define chsrc_yield_source(for_what) \
if (is_url (option)) \
{ \
SourceInfo __tmp = { &UserDefine, option }; \
source = __tmp; \
} \
else \
{ \
int __index = use_specific_mirror_or_auto_select (option, for_what); \
source = for_what##_sources[__index]; \
}
#define split_between_source_changing_process puts ("--------------------------------")
/**
* 用于 _setsrc 函数
*
* 1. 告知用户选择了什么源和镜像
* 2. 对选择的源和镜像站进行一定的校验
*/
void
chsrc_confirm_source (SourceInfo *source)
{
// 由于实现问题,我们把本应该独立出去的默认上游源,也放在了可以换源的数组中,而且放在第一个
// chsrc 已经规避用户使用未实现的 `chsrc reset`
// 但是某些用户可能摸索着强行使用 chsrc set target upstream从而执行起该禁用的功能
// 之所以禁用,是因为有的 reset 我们并没有实现,我们在这里阻止这些邪恶的用户
if (source_is_upstream (source) && source_has_empty_url (source))
{
chsrc_error ("暂未对该软件实现重置");
exit (Exit_Unsupported);
}
else if (source_has_empty_url (source))
{
chsrc_error ("该源URL不存在请向开发团队提交bug");
exit (Exit_FatalBug);
}
else
{
puts (xy_strjoin (5, "选中镜像站: ", to_green (source->mirror->abbr), " (", to_green (source->mirror->code), ")"));
}
split_between_source_changing_process;
}
#define ChsrcTypeAuto "auto"
#define ChsrcTypeReset "reset"
#define ChsrcTypeSemiAuto "semiauto"
#define ChsrcTypeManual "manual"
#define ChsrcTypeUntested "untested"
/**
* @param source 可为NULL
* @param last_word 5种选择ChsrcTypeAuto | ChsrcTypeReset | ChsrcTypeSemiAuto | ChsrcTypeManual | ChsrcTypeUntested
*/
void
chsrc_say_lastly (SourceInfo *source, const char *last_word)
{
split_between_source_changing_process;
if (xy_streql (ChsrcTypeAuto, last_word))
{
if (source)
{
if (source_is_userdefine (source))
{
chsrc_log ("全自动换源完成; 邀您参与贡献帮助其他人使用该URL换源: chsrc issue");
}
else
{
chsrc_log (xy_2strjoin ("全自动换源完成,感谢镜像提供方: ", to_purple (source->mirror->name)));
}
}
else
{
chsrc_log ("全自动换源完成");
}
}
else if (xy_streql (ChsrcTypeReset, last_word))
{
// source_is_upstream (source)
chsrc_log (to_purple ("已重置为上游默认源"));
}
else if (xy_streql (ChsrcTypeSemiAuto, last_word))
{
if (source)
{
if (source_is_userdefine (source))
{
chsrc_log ("半自动换源完成,仍需按上述提示手工操作; 邀您参与贡献帮助其他人使用该URL换源: chsrc issue");
}
else
{
chsrc_log (xy_2strjoin ("半自动换源完成,仍需按上述提示手工操作,感谢镜像提供方: ", to_purple (source->mirror->name)));
}
}
else
{
chsrc_log ("半自动换源完成,仍需按上述提示手工操作");
}
chsrc_warn ("若您有更好的换源方案,邀您帮助: chsrc issue");
}
else if (xy_streql (ChsrcTypeManual, last_word))
{
if (source)
{
if (source_is_userdefine (source))
{
chsrc_log ("因实现约束需按上述提示手工操作; 邀您参与贡献帮助其他人使用该URL换源: chsrc issue");
}
else
{
chsrc_log (xy_2strjoin ("因实现约束需按上述提示手工操作,感谢镜像提供方: ", to_purple (source->mirror->name)));
}
}
else
{
chsrc_log ("因实现约束需按上述提示手工操作");
}
chsrc_warn ("若您有更好的换源方案,邀您帮助: chsrc issue");
}
else if (xy_streql (ChsrcTypeUntested, last_word))
{
if (source)
{
if (source_is_userdefine (source))
{
chsrc_log ("邀您参与贡献帮助其他人使用该URL换源: chsrc issue");
}
else
{
chsrc_log (xy_2strjoin ("感谢镜像提供方: ", to_purple (source->mirror->name)));
}
}
else
{
chsrc_log ("自动换源完成");
}
chsrc_warn ("该换源步骤已实现但未经测试或存在任何反馈,请报告使用情况: chsrc issue");
}
else
{
puts (last_word);
}
}
void
chsrc_ensure_root ()
{
char *euid = getenv ("$EUID");
if (NULL==euid)
{
char *buf = xy_run ("id -u", 0, NULL);
if (0!=atoi(buf)) goto not_root;
else return;
}
else
{
if (0!=atoi(euid)) goto not_root;
else return;
}
not_root:
chsrc_error ("请在命令前使用 sudo 或切换为root用户来保证必要的权限");
exit (Exit_UserCause);
}
#define RunOpt_Default 0x0000 // 默认若命令运行失败,直接退出
#define RunOpt_Dont_Notify_On_Success 0x0010 // 运行成功不提示用户,只有运行失败时才提示用户
#define RunOpt_No_Last_New_Line 0x0100 // 不输出最后的空行
#define RunOpt_Dont_Abort_On_Failure 0x1000 // 命令运行失败也不退出
static void
chsrc_run (const char *cmd, int run_option)
{
xy_log_brkt (to_blue (App_Name), to_boldblue ("运行"), to_blue (cmd));
if (CliOpt_DryRun)
{
return; // Dry Run 此时立即结束,并不真正执行
}
int status = system (cmd);
if (0==status)
{
if (! (RunOpt_Dont_Notify_On_Success & run_option))
{
chsrc_log_cmd_result (true, status);
}
}
else
{
chsrc_log_cmd_result (false, status);
if (! (run_option & RunOpt_Dont_Abort_On_Failure))
{
chsrc_error ("关键错误,强制结束");
exit (Exit_FatalUnkownError);
}
}
if (! (RunOpt_No_Last_New_Line & run_option))
{
puts ("");
}
}
static void
chsrc_view_file (const char *path)
{
char *cmd = NULL;
path = xy_uniform_path (path);
if (xy_on_windows)
{
cmd = xy_2strjoin ("type ", path);
}
else
{
cmd = xy_2strjoin ("cat ", path);
}
chsrc_run (cmd, RunOpt_Dont_Notify_On_Success|RunOpt_No_Last_New_Line);
}
static void
chsrc_ensure_dir (const char *dir)
{
dir = xy_uniform_path (dir);
if (xy_dir_exist (dir))
{
return;
}
// 不存在就生成
char *mkdir_cmd = NULL;
if (xy_on_windows)
{
mkdir_cmd = "md "; // 已存在时返回 errorlevel = 1
}
else
{
mkdir_cmd = "mkdir -p ";
}
char *cmd = xy_2strjoin (mkdir_cmd, dir);
cmd = xy_str_to_quietcmd (cmd);
chsrc_run (cmd, RunOpt_No_Last_New_Line|RunOpt_Dont_Notify_On_Success);
chsrc_note2 (xy_2strjoin ("目录不存在,已自动创建 ", dir));
}
static void
chsrc_append_to_file (const char *str, const char *file)
{
file = xy_uniform_path (file);
char *dir = xy_parent_dir (file);
chsrc_ensure_dir (dir);
char *cmd = NULL;
if (xy_on_windows)
{
cmd = xy_strjoin (4, "echo ", str, " >> ", file);
}
else
{
cmd = xy_strjoin (4, "echo '", str, "' >> ", file);
}
chsrc_run (cmd, RunOpt_No_Last_New_Line|RunOpt_Dont_Notify_On_Success);
}
static void
chsrc_prepend_to_file (const char *str, const char *file)
{
file = xy_uniform_path (file);
char *dir = xy_parent_dir (file);
chsrc_ensure_dir (dir);
char *cmd = NULL;
if (xy_on_windows)
{
xy_unimplement;
}
else
{
cmd = xy_strjoin (4, "sed -i '1i ", str, "' ", file);
}
chsrc_run (cmd, RunOpt_No_Last_New_Line|RunOpt_Dont_Notify_On_Success);
}
static void
chsrc_overwrite_file (const char *str, const char *file)
{
file = xy_uniform_path (file);
char *dir = xy_parent_dir (file);
chsrc_ensure_dir (dir);
char *cmd = NULL;
if (xy_on_windows)
{
cmd = xy_strjoin (4, "echo ", str, " > ", file);
}
else
{
cmd = xy_strjoin (4, "echo '", str, "' > ", file);
}
chsrc_run (cmd, RunOpt_Default);
}
static void
chsrc_backup (const char *path)
{
char *cmd = NULL;
bool exist = xy_file_exist (path);
if (!exist)
{
chsrc_note2 (xy_2strjoin ("文件不存在,跳过备份: ", path));
return;
}
if (xy_on_bsd || xy_on_macos)
{
/* BSD 和 macOS 的 cp 不支持 --backup 选项 */
cmd = xy_strjoin (5, "cp -f ", path, " ", path, ".bak");
}
else if (xy_on_windows)
{
// /Y 表示覆盖
cmd = xy_strjoin (5, "copy /Y ", path, " ", path, ".bak" );
}
else
{
cmd = xy_strjoin (5, "cp ", path, " ", path, ".bak --backup='t'");
}
chsrc_run (cmd, RunOpt_No_Last_New_Line|RunOpt_Dont_Notify_On_Success);
chsrc_note2 (xy_strjoin (3, "备份文件名为 ", path, ".bak"));
}
static char *
chsrc_get_cpuarch ()
{
char *ret;
bool exist;
if (xy_on_windows)
{
xy_unimplement;
}
exist = chsrc_check_program ("arch");
if (exist)
{
ret = xy_run ("arch", 0, NULL);
return ret;
}
exist = chsrc_check_program ("uname");
if (exist)
{
ret = xy_run ("uname -m", 0, NULL);
return ret;
}
else
{
chsrc_error ("无法检测到CPU类型");
exit (Exit_UserCause);
}
}
/* Target Info */
typedef struct TargetInfo_t {
void (*getfn) (char *option);
void (*setfn) (char *option);
void (*resetfn) (char *option);
SourceInfo *sources;
size_t sources_n;
} TargetInfo;
// 大部分target还不支持reset所以暂时先默认设置为NULL来过渡
#define def_target(t) TargetInfo t##_target = {t##_getsrc, t##_setsrc, NULL, t##_sources, t##_sources_n}
#define def_target_full(t) TargetInfo t##_target = {t##_getsrc, t##_setsrc, t##_resetsrc, t##_sources, t##_sources_n}
#define def_target_noget(t) TargetInfo t##_target = {NULL, t##_setsrc, NULL, t##_sources, t##_sources_n}