mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-28 17:22:15 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
010c037dde | ||
|
|
e017d4d9d4 |
@@ -1,80 +0,0 @@
|
||||
name: Contract Parity
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**/*.rs"
|
||||
- "crates/**/*.rs"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "src-tauri/Cargo.toml"
|
||||
- "crates/**/Cargo.toml"
|
||||
- "rust-toolchain.toml"
|
||||
- "src-tauri/tauri.conf*.json"
|
||||
- "src/services/bindings.ts"
|
||||
- "scripts/gen-contract.mjs"
|
||||
- "scripts/check-contract-changes.mjs"
|
||||
- "biome.json"
|
||||
- "biome.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "scripts/contract-inputs.json"
|
||||
- ".github/workflows/contract-parity.yml"
|
||||
- ".husky/pre-push"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
contract-parity:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Provision sidecar placeholders for tauri-build
|
||||
run: |
|
||||
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||
touch dist/index.html
|
||||
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||
done
|
||||
|
||||
- name: Check generated contract without changing the checkout
|
||||
run: pnpm gen:contract --check
|
||||
@@ -2,23 +2,11 @@ name: Frontend Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "**/*.js"
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.css"
|
||||
- "**/*.scss"
|
||||
- "**/*.json"
|
||||
- "**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "vite.config.*"
|
||||
- ".github/workflows/frontend-check.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -29,17 +17,46 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check frontend changes
|
||||
id: check_frontend
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
frontend:
|
||||
- 'src/**'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'knip.json'
|
||||
- 'eslint.config.ts'
|
||||
- 'tsconfig.json'
|
||||
- 'vite.config.*'
|
||||
|
||||
- name: Skip if no frontend changes
|
||||
if: steps.check_frontend.outputs.frontend != 'true'
|
||||
run: echo "No frontend changes, skipping frontend checks."
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
@@ -48,21 +65,94 @@ jobs:
|
||||
pnpm-shared-stable-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
|
||||
- name: Run Prettier check
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run ESLint
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run TypeScript typecheck
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm test
|
||||
|
||||
- name: Run Knip
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm knip:check
|
||||
|
||||
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: node scripts/check-architecture.mjs listen emit invoke
|
||||
|
||||
contract-parity:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check contract inputs
|
||||
id: check_inputs
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: scripts/contract-inputs.json
|
||||
|
||||
- name: Skip if contract inputs unchanged
|
||||
if: steps.check_inputs.outputs.contract != 'true'
|
||||
run: echo "No contract inputs changed, skipping parity check."
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Rust Cache
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
with:
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
|
||||
- name: Provision sidecar placeholders for tauri-build
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: |
|
||||
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||
touch dist/index.html
|
||||
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||
done
|
||||
|
||||
- name: Check generated contract without changing the checkout
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: pnpm gen:contract --check
|
||||
|
||||
@@ -2,13 +2,6 @@ name: Clippy Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**"
|
||||
- "crates/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- ".github/workflows/lint-clippy.yml"
|
||||
workflow_dispatch:
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -30,6 +23,28 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check src-tauri changes
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
id: check_changes
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src-tauri/**'
|
||||
|
||||
- name: Skip if src-tauri not changed
|
||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
|
||||
run: echo "No src-tauri changes, skipping clippy lint."
|
||||
|
||||
- name: Continue if src-tauri changed
|
||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
|
||||
run: echo "src-tauri changed, running clippy lint."
|
||||
|
||||
- name: Manual trigger - always run
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Manual trigger detected: skipping changes check and running clippy."
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -64,7 +79,7 @@ jobs:
|
||||
run: cargo clippy-all
|
||||
|
||||
- name: Run Windows security tests
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true')
|
||||
working-directory: ./src-tauri
|
||||
run: cargo test --all-features --target ${{ matrix.target }}
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ name: Check Formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**"
|
||||
- "**/*.rs"
|
||||
- ".github/workflows/rustfmt.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -23,10 +19,25 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check Rust changes
|
||||
id: check_rust
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src-tauri/**'
|
||||
- '**/*.rs'
|
||||
|
||||
- name: Skip if no Rust changes
|
||||
if: steps.check_rust.outputs.rust != 'true'
|
||||
run: echo "No Rust changes, skipping rustfmt."
|
||||
|
||||
- name: install Rust stable and rustfmt
|
||||
if: steps.check_rust.outputs.rust == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: run cargo fmt
|
||||
if: steps.check_rust.outputs.rust == 'true'
|
||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||
|
||||
Generated
+9
-13
@@ -1139,7 +1139,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2 0.11.1",
|
||||
"smartstring",
|
||||
"socket2",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
@@ -1169,6 +1168,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tristr",
|
||||
"warp",
|
||||
"webpki-roots",
|
||||
"windows 0.62.2",
|
||||
@@ -6720,18 +6720,6 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
@@ -8505,6 +8493,14 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tristr"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
||||
+1
-2
@@ -85,9 +85,8 @@ reqwest = { version = "0.13.5", features = [
|
||||
] }
|
||||
rust_iso3166 = "0.2.0"
|
||||
|
||||
smartstring = { version = "1.0.1" }
|
||||
|
||||
serde = { version = "1.0.229" }
|
||||
tristr = { path = "../tristr", default-features = false, features = ["serde", "specta"] }
|
||||
serde_json = { version = "1.0.154" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.13.2" }
|
||||
|
||||
@@ -3,15 +3,10 @@
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
- 修复 代理 在全局模式下无法展开节点组
|
||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||
- 修复恢复备份的同时修改配置,可能导致配置丢失或错乱的问题
|
||||
- 修复切换规则模式与配置变更并发时可能丢失或错乱的问题
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
@@ -23,8 +18,6 @@
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ mod chatgpt;
|
||||
mod claude;
|
||||
mod disney_plus;
|
||||
mod gemini;
|
||||
mod muse;
|
||||
mod netflix;
|
||||
mod prime_video;
|
||||
mod spotify;
|
||||
@@ -31,7 +30,6 @@ enum UnlockCheck {
|
||||
ChatGptWeb,
|
||||
Claude,
|
||||
Gemini,
|
||||
MuseAi,
|
||||
YouTubePremium,
|
||||
BahamutAnime,
|
||||
Netflix,
|
||||
@@ -48,7 +46,6 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb,
|
||||
Self::Claude,
|
||||
Self::Gemini,
|
||||
Self::MuseAi,
|
||||
Self::YouTubePremium,
|
||||
Self::BahamutAnime,
|
||||
Self::Netflix,
|
||||
@@ -69,7 +66,6 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
|
||||
Self::Claude => claude::CLAUDE_NAME,
|
||||
Self::Gemini => gemini::GEMINI_NAME,
|
||||
Self::MuseAi => muse::MUSE_AI_NAME,
|
||||
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
|
||||
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
|
||||
Self::Netflix => netflix::NETFLIX_NAME,
|
||||
@@ -87,7 +83,6 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
|
||||
Self::Claude => claude::check_claude(client).await,
|
||||
Self::Gemini => gemini::check_gemini(client).await,
|
||||
Self::MuseAi => muse::check_muse_ai(client).await,
|
||||
Self::YouTubePremium => youtube::check_youtube_premium(client).await,
|
||||
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
|
||||
Self::Netflix => netflix::check_netflix(client).await,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::utils::{classify_restricted_status, get_trace_location};
|
||||
|
||||
use super::UnlockItem;
|
||||
|
||||
pub(crate) const MUSE_AI_NAME: &str = "Muse AI";
|
||||
|
||||
const MUSE_URL: &str = "https://muse.ai";
|
||||
const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
|
||||
|
||||
// The aymh ("Are You a Human") gateway answers plain HTTP requests with a
|
||||
// generic Meta error page that always contains the Facebook logo asset.
|
||||
const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"];
|
||||
|
||||
pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem {
|
||||
// muse.ai itself is served by Vercel, so resolve the exit region through
|
||||
// the generic Cloudflare trace endpoint instead of a service-specific one.
|
||||
let region = get_trace_location(client, CLOUDFLARE_TRACE_URL)
|
||||
.await
|
||||
.map(|code| code.trim().to_ascii_uppercase())
|
||||
.map(|code| UnlockItem::region_label(&code));
|
||||
|
||||
let Ok(response) = client.get(MUSE_URL).send().await else {
|
||||
return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region);
|
||||
};
|
||||
|
||||
let status = response.status();
|
||||
let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai");
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
// Every region is redirected to the aymh anti-bot gateway before the app
|
||||
// loads, and a non-browser HTTP client cannot finish the JavaScript
|
||||
// challenge. It returns the Meta error page, so reaching this point only
|
||||
// proves the exit can connect to Muse's auth infrastructure; availability
|
||||
// cannot be verified over plain HTTP and is reported as `Risky`.
|
||||
let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker));
|
||||
|
||||
let result = if behind_anti_bot {
|
||||
"Risky"
|
||||
} else if status.is_success() {
|
||||
"Yes"
|
||||
} else {
|
||||
classify_restricted_status(status).unwrap_or("Failed")
|
||||
};
|
||||
|
||||
UnlockItem::checked(MUSE_AI_NAME, result, region)
|
||||
}
|
||||
+2
-3
@@ -14,7 +14,7 @@
|
||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsrs -p . --noEmit && vite build",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
@@ -32,7 +32,7 @@
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"gen:contract": "node scripts/gen-contract.mjs",
|
||||
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
|
||||
"typecheck": "tsrs -p . --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"knip:check": "knip",
|
||||
@@ -128,7 +128,6 @@
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
"terser": "^5.51.2",
|
||||
"tsrs": "0.20.0-ts7.1.0-beta",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0",
|
||||
|
||||
Generated
+3
-88
@@ -416,9 +416,6 @@ importers:
|
||||
terser:
|
||||
specifier: ^5.51.2
|
||||
version: 5.51.2
|
||||
tsrs:
|
||||
specifier: 0.20.0-ts7.1.0-beta
|
||||
version: 0.20.0-ts7.1.0-beta
|
||||
typescript:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
@@ -2221,52 +2218,6 @@ packages:
|
||||
'@tauri-apps/plugin-updater@2.14.0':
|
||||
resolution: {integrity: sha512-gdylJ9tger4hkyOn24QZd6MNOKVo2Eh53iw5th2Qj1vKTR/+u4PIwJWuiis5hohYvobn6iOvlnALfrqIPSzh7Q==}
|
||||
|
||||
'@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-mhHj8QGUmdJDBvJEnMXx5P/jvAKXYU3hy/z3qxrWBo8XHlbLES32PgC0ZpTRbJA960ReTpVqqvCOYaC6EL32/Q==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tsrs/darwin-x64@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-CsRUnyYRLrhmChS7ol7LgmdqMXTu+mVlDU+7JgbW8RpkJRGLZ/zgEDEOeD8qrndt8rpP9oW1T1SJ36HSzB3/sg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-VNwpdI/iICwQMMlS/MoSZCvKFByEn2+g7D0HREWG+3r3GRayXgbLv8iaNSHKT3QGcu8pXr3lH9170ghJQi2zRg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-cXnqPpmj5gzeGi4u1OV0ctd5df5t4ivwyeToCa4pBrKtjYodu0EeAo5Y76SHbJwggbiA1gzEJVj1vvuciQm5mQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-jlhJSrO9lxNh0TF5EU1i+Frok5sRWsBvCuQhmdqMZKIN2pSHsDZl3CSmKUsJ4tNr50np9USCPpkkxwby2Eiqhg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-pestGEPplhEPzIERc7zwuQ5zhh9ZwMSvAPNQy5TM0je/QuaizmM1zKoPYJ4NiaPzLh2M25HtP4HcpumTqmF3KQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tsrs/win32-x64@0.20.0-ts7.1.0-beta':
|
||||
resolution: {integrity: sha512-sstxBfhjjOSYvSHFfSmROau2oGAVNLwAnj+H0h+ywLo7N7ueFKIDk14HAkIYa/F9iVboiICCpHDY/pmBO71sSg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tybys/wasm-util@0.10.4':
|
||||
resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
|
||||
|
||||
@@ -4819,11 +4770,6 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tsrs@0.20.0-ts7.1.0-beta:
|
||||
resolution: {integrity: sha512-8NifAn2tcna3t/nluu1yFvp0fYE28vElDpnD4jiWik69+MDjiHgxdjH8sUYUEFmf/2hl6QIE5ExVGpJzQbBDPw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
hasBin: true
|
||||
|
||||
tunnel@0.0.6:
|
||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
||||
@@ -6936,27 +6882,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.12.4
|
||||
|
||||
'@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/darwin-x64@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tsrs/win32-x64@0.20.0-ts7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.4':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -7297,7 +7222,7 @@ snapshots:
|
||||
|
||||
'@wdio/globals@9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
|
||||
dependencies:
|
||||
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
|
||||
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
|
||||
webdriverio: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
|
||||
|
||||
'@wdio/logger@10.1.0':
|
||||
@@ -8281,11 +8206,11 @@ snapshots:
|
||||
|
||||
expect-type@1.4.0: {}
|
||||
|
||||
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
|
||||
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
|
||||
dependencies:
|
||||
'@vitest/snapshot': 4.1.11
|
||||
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
|
||||
'@wdio/logger': 9.29.1
|
||||
'@wdio/logger': 10.1.0
|
||||
deep-eql: 5.0.2
|
||||
expect: 30.5.2
|
||||
jest-matcher-utils: 30.5.2
|
||||
@@ -9940,16 +9865,6 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsrs@0.20.0-ts7.1.0-beta:
|
||||
optionalDependencies:
|
||||
'@tsrs/darwin-arm64': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/darwin-x64': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/linux-arm64-gnu': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/linux-arm64-musl': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/linux-x64-gnu': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/linux-x64-musl': 0.20.0-ts7.1.0-beta
|
||||
'@tsrs/win32-x64': 0.20.0-ts7.1.0-beta
|
||||
|
||||
tunnel@0.0.6: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
|
||||
@@ -10,11 +10,3 @@ allowBuilds:
|
||||
unrs-resolver: true
|
||||
minimumReleaseAgeExclude:
|
||||
- dayjs@1.11.21
|
||||
- "@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/darwin-x64@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/win32-x64@0.20.0-ts7.1.0-beta"
|
||||
- tsrs@0.20.0-ts7.1.0-beta
|
||||
|
||||
@@ -79,13 +79,6 @@
|
||||
"matchPackageNames": ["typescript"],
|
||||
"allowedVersions": "<7.0.0 || >=7.1.0"
|
||||
},
|
||||
{
|
||||
"description": "Review tsrs bumps alone; the version encodes the pinned TypeScript release",
|
||||
"matchPackageNames": ["tsrs", "/^@tsrs//"],
|
||||
"groupName": "tsrs",
|
||||
"rangeStrategy": "pin",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Prevent project from supply chain attack",
|
||||
"matchDatasources": ["npm"],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"scripts/contract-inputs.json",
|
||||
".github/workflows/contract-parity.yml",
|
||||
".github/workflows/frontend-check.yml",
|
||||
".husky/pre-push"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -266,13 +266,13 @@ async function getLatestReleaseVersion() {
|
||||
}
|
||||
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`mihomo unsupported platform "${platform}-${arch}"`)
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`)
|
||||
}
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`mihomo alpha unsupported platform "${platform}-${arch}"`)
|
||||
throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`)
|
||||
}
|
||||
|
||||
function mihomoAlpha() {
|
||||
function clashMetaAlpha() {
|
||||
const name = META_ALPHA_MAP[`${platform}-${arch}`]
|
||||
const isWin = platform === 'win32'
|
||||
const urlExt = isWin ? 'zip' : 'gz'
|
||||
@@ -285,7 +285,7 @@ function mihomoAlpha() {
|
||||
}
|
||||
}
|
||||
|
||||
function mihomo() {
|
||||
function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`]
|
||||
const isWin = platform === 'win32'
|
||||
const urlExt = isWin ? 'zip' : 'gz'
|
||||
@@ -733,12 +733,13 @@ const tasks = [
|
||||
{
|
||||
name: 'verge-mihomo-alpha',
|
||||
func: () =>
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())),
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
retry: 5,
|
||||
},
|
||||
{
|
||||
name: 'verge-mihomo',
|
||||
func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
|
||||
func: () =>
|
||||
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||
retry: 5,
|
||||
},
|
||||
// After both sidecar tasks: it hashes what they downloaded.
|
||||
|
||||
@@ -52,9 +52,9 @@ anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tristr = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
bitflags = { workspace = true }
|
||||
socket2 = { workspace = true }
|
||||
warp = { version = "0.4.3", features = ["server"] }
|
||||
|
||||
+11
-14
@@ -5,8 +5,8 @@ use crate::{
|
||||
feat,
|
||||
utils::dirs,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tauri::{AppHandle, Manager as _, ipc::Channel};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -59,10 +59,10 @@ pub async fn restart_app() -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn install_update(
|
||||
app_handle: AppHandle,
|
||||
version: std::string::String,
|
||||
version: String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> CmdResult<bool> {
|
||||
let version: String = version.into();
|
||||
let version: String = version;
|
||||
SilentUpdater::global()
|
||||
.install_update(&app_handle, &version, |event| {
|
||||
let _ = on_event.send(event);
|
||||
@@ -79,25 +79,22 @@ pub fn cancel_update_download() {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_app_dir() -> CmdResult<std::string::String> {
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_icon_cache(
|
||||
url: std::string::String,
|
||||
name: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
let url: String = url.into();
|
||||
let name: String = name.into();
|
||||
feat::download_icon_cache(url, name).await.map(Into::into)
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let url: String = url;
|
||||
let name: String = name;
|
||||
feat::download_icon_cache(url, name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
|
||||
let path: String = path.into();
|
||||
feat::copy_icon_file(path, icon_info).await.map(Into::into)
|
||||
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
||||
let path: String = path;
|
||||
feat::copy_icon_file(path, icon_info).await
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, feat};
|
||||
use feat::LocalBackupFile;
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -17,15 +17,15 @@ pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::delete_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::restore_local_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
@@ -33,15 +33,15 @@ pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
|
||||
let source: String = source.into();
|
||||
feat::import_local_backup(source).await.map(Into::into).stringify_err()
|
||||
pub async fn import_local_backup(source: String) -> CmdResult<String> {
|
||||
let source: String = source;
|
||||
feat::import_local_backup(source).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
let destination: String = destination.into();
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
let destination: String = destination;
|
||||
feat::export_local_backup(filename, destination).await.stringify_err()
|
||||
}
|
||||
|
||||
+16
-16
@@ -12,8 +12,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -37,8 +37,8 @@ pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||
let payload: String = payload.into();
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
let payload: String = payload;
|
||||
feat::change_clash_mode(payload)
|
||||
.await
|
||||
.with_error_code("CLASH_MODE_UPDATE_FAILED")
|
||||
@@ -47,14 +47,14 @@ pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
|
||||
Ok(Config::clash().await.data_arc().get_mode())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
|
||||
let clash_core: String = clash_core.into();
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
|
||||
let clash_core: String = clash_core;
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
@@ -112,8 +112,8 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
|
||||
let url: String = url.into();
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let url: String = url;
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
@@ -148,12 +148,12 @@ pub fn take_dns_override_notice() -> bool {
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_dns_override(
|
||||
profile_uid: std::string::String,
|
||||
profile_uid: String,
|
||||
enabled: bool,
|
||||
confirmation: Option<std::string::String>,
|
||||
confirmation: Option<String>,
|
||||
) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||
let profile_uid: String = profile_uid.into();
|
||||
let confirmation = confirmation.map(String::from);
|
||||
let profile_uid: String = profile_uid;
|
||||
let confirmation = confirmation;
|
||||
feat::set_dns_override(profile_uid, enabled, confirmation)
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
||||
@@ -212,14 +212,14 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
|
||||
use crate::utils::dirs;
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||
|
||||
match fs::read_to_string(&dns_path).await {
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Ok(content) => Ok(Some(content.into())),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(CommandFailure::plain(error)),
|
||||
}
|
||||
@@ -244,7 +244,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<String>> {
|
||||
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
|
||||
Ok(logs)
|
||||
Ok(logs.into_iter().collect())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub use clash_verge_media_unlock::UnlockItem;
|
||||
use reqwest::{Client, Proxy};
|
||||
use std::time::Duration;
|
||||
use tauri::{command, ipc::Channel};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
@@ -23,7 +24,7 @@ async fn create_client() -> Result<Client, String> {
|
||||
.cookie_store(true)
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
.map_err(|error| format!("创建HTTP客户端失败: {error}"))
|
||||
.map_err(|error| format!("创建HTTP客户端失败: {error}").into())
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -40,5 +41,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
|
||||
#[specta::specta]
|
||||
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
|
||||
let client = create_client().await?;
|
||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
|
||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
|
||||
@@ -8,9 +8,9 @@ pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
pub struct CommandFailure {
|
||||
/// Stable frontend identifier, when classified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<std::string::String>,
|
||||
pub code: Option<String>,
|
||||
/// Human-readable diagnostic detail.
|
||||
pub detail: std::string::String,
|
||||
pub detail: String,
|
||||
/// Requested operation when known by the command.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<crate::core::notification::FailedOperation>,
|
||||
@@ -20,8 +20,8 @@ impl CommandFailure {
|
||||
/// A failure with a stable code.
|
||||
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: Some(code.to_owned()),
|
||||
detail: detail.to_string(),
|
||||
code: Some(code.into()),
|
||||
detail: detail.to_string().into(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl CommandFailure {
|
||||
pub fn plain(detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail: detail.to_string(),
|
||||
detail: detail.to_string().into(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
@@ -50,16 +50,6 @@ impl From<&str> for CommandFailure {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::String> for CommandFailure {
|
||||
fn from(detail: std::string::String) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail,
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CommandFailure {
|
||||
fn from(detail: String) -> Self {
|
||||
Self::plain(detail)
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::core::{proxy_control, sysopt::Sysopt};
|
||||
use gethostname::gethostname;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_clash_verge_sysinfo;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -24,8 +25,8 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
|
||||
|
||||
Ok(SystemProxy {
|
||||
enable,
|
||||
server: format!("{host}:{port}"),
|
||||
bypass,
|
||||
server: format!("{host}:{port}").into(),
|
||||
bypass: bypass.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +40,10 @@ pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
|
||||
};
|
||||
let Autoproxy { enable, url } = auto_proxy;
|
||||
|
||||
Ok(AutoProxy { enable, url })
|
||||
Ok(AutoProxy {
|
||||
enable,
|
||||
url: url.into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -52,10 +56,10 @@ pub fn get_embedded_server_port() -> CmdResult<u16> {
|
||||
#[specta::specta]
|
||||
pub fn get_system_hostname() -> String {
|
||||
match gethostname().into_string() {
|
||||
Ok(name) => name,
|
||||
Ok(name) => name.into(),
|
||||
Err(os_string) => {
|
||||
let fallback = format!("{os_string:?}");
|
||||
fallback.trim_matches('"').to_string()
|
||||
fallback.trim_matches('"').into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +68,9 @@ pub fn get_system_hostname() -> String {
|
||||
#[specta::specta]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -77,7 +84,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
if names.iter().any(|name| name == &interface.name) {
|
||||
result.push(interface.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,17 @@ use crate::{
|
||||
use clash_verge_draft::{Draft, SharedDraft};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tristr::alias::String;
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn profile_import_error(err: &anyhow::Error) -> std::string::String {
|
||||
fn profile_import_error(err: &anyhow::Error) -> String {
|
||||
if let Some(cause) = err.chain().find(|cause| cause.to_string().contains("TLS 1.0/1.1")) {
|
||||
return cause.to_string();
|
||||
return cause.to_string().into();
|
||||
}
|
||||
|
||||
format!("导入订阅失败: {err:#}")
|
||||
format!("导入订阅失败: {err:#}").into()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -68,7 +68,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||
#[tauri::command]
|
||||
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
|
||||
#[specta::specta]
|
||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => it,
|
||||
Err(e) => {
|
||||
@@ -98,9 +98,9 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
|
||||
let active_id: String = active_id.into();
|
||||
let over_id: String = over_id.into();
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
let active_id: String = active_id;
|
||||
let over_id: String = over_id;
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
|
||||
@@ -121,9 +121,9 @@ pub async fn reorder_profile(active_id: std::string::String, over_id: std::strin
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> CmdResult {
|
||||
let item: PrfItem = item.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
let file_data = file_data;
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
profiles_save_file_safe()
|
||||
@@ -138,8 +138,8 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let index: String = index;
|
||||
match feat::update_profile(&index, option.as_ref(), true).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
@@ -151,8 +151,8 @@ pub async fn update_profile(index: std::string::String, option: Option<PrfOption
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let index: String = index;
|
||||
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
@@ -325,7 +325,6 @@ async fn perform_config_update(
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileSelection {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
}
|
||||
|
||||
@@ -333,13 +332,9 @@ pub struct ProfileSelection {
|
||||
#[serde(default)]
|
||||
pub struct ProfileCreate {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
@@ -361,15 +356,10 @@ impl From<ProfileCreate> for PrfItem {
|
||||
#[serde(default)]
|
||||
pub struct ProfilePatch {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
@@ -436,9 +426,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
|
||||
pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
|
||||
let profile: PrfItem = profile.into();
|
||||
let index: String = index.into();
|
||||
let index: String = index;
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
@@ -479,7 +469,7 @@ pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) ->
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||
pub async fn view_profile(index: String) -> CmdResult {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let file = profiles_ref
|
||||
@@ -504,7 +494,7 @@ pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
@@ -524,7 +514,7 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
|
||||
.join(file.as_str());
|
||||
match tokio::fs::try_exists(&path).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return Ok(std::string::String::new()),
|
||||
Ok(false) => return Ok(String::new()),
|
||||
Err(err) => {
|
||||
return Err(coded_error(
|
||||
"PROFILE_READ_FAILED",
|
||||
@@ -535,12 +525,12 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
|
||||
}
|
||||
|
||||
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
|
||||
Ok(data.into())
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
|
||||
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
let timer = Timer::global();
|
||||
let next_time = timer.get_next_update_time(&uid).await;
|
||||
Ok(next_time)
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
|
||||
#[tauri::command]
|
||||
@@ -48,7 +49,7 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
|
||||
.filter_map(|name| name.as_str())
|
||||
.filter(|name| !name.is_empty() && *name != "GLOBAL")
|
||||
.filter(|name| seen.insert(*name))
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.into_iter()
|
||||
.flat_map(|providers| providers.keys())
|
||||
.filter_map(|name| name.as_str())
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
all_providers.sort();
|
||||
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
|
||||
@@ -75,7 +76,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.flatten()
|
||||
.filter_map(|group| group.as_mapping())
|
||||
.filter_map(|group| {
|
||||
let name = group.get("name")?.as_str()?.to_owned();
|
||||
let name: String = group.get("name")?.as_str()?.into();
|
||||
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
|
||||
all_providers.clone()
|
||||
} else {
|
||||
@@ -85,14 +86,14 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|provider| provider.as_str())
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
};
|
||||
let exclude_types = group
|
||||
.get("exclude-type")
|
||||
.and_then(|types| types.as_str())
|
||||
.filter(|types| !types.is_empty())
|
||||
.map(|types| types.split('|').map(str::to_owned).collect())
|
||||
.map(|types| types.split('|').map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
Some((
|
||||
name,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -17,7 +17,7 @@ pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>>
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
@@ -36,22 +36,17 @@ pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
#[serde(transparent)]
|
||||
pub struct RuntimeLogs(
|
||||
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
|
||||
HashMap<String, Vec<(String, String)>>,
|
||||
);
|
||||
pub struct RuntimeLogs(HashMap<String, Vec<(String, String)>>);
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
|
||||
crate::enhance::take_discarded_keys_notice().map(Into::into)
|
||||
pub fn take_discarded_keys_notice() -> Option<String> {
|
||||
crate::enhance::take_discarded_keys_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_proxy_chain_config(
|
||||
proxy_chain_exit_node: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::CmdResult;
|
||||
use super::{CmdResult, CommandFailure};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
|
||||
@@ -11,18 +11,15 @@ use crate::{
|
||||
utils::dirs,
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn save_profile_file(
|
||||
index: std::string::String,
|
||||
file_data: Option<std::string::String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
let index: String = index.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
|
||||
let index: String = index;
|
||||
let file_data = file_data;
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(ValidationOutcome::Valid),
|
||||
@@ -153,7 +150,7 @@ async fn handle_saved_profile_file(
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {e:#}");
|
||||
restore_original(file_path, original_content, original_existed).await?;
|
||||
return Err(e.to_string().into());
|
||||
return Err(CommandFailure::plain(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +178,7 @@ async fn handle_saved_profile_file(
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
|
||||
restore_original(file_path, original_content, original_existed).await?;
|
||||
Err(err.to_string().into())
|
||||
Err(CommandFailure::plain(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
service::{SERVICE_MANAGER, ServiceStatus, request_runtime_provider_sync},
|
||||
},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
@@ -74,7 +75,7 @@ where
|
||||
if reasons.is_empty() {
|
||||
return Err(proxy_aware_coded_error(&error, error_code));
|
||||
}
|
||||
let reason = reasons.join("\n");
|
||||
let reason: String = reasons.join("\n").into();
|
||||
fallback(reason.clone())
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
|
||||
@@ -179,7 +180,7 @@ mod tests {
|
||||
let error = match availability {
|
||||
Some(availability) => anyhow::Error::new(InstallationVerificationError {
|
||||
status: InstallationStatus {
|
||||
service_sha256: String::new(),
|
||||
service_sha256: std::string::String::new(),
|
||||
protocol: ProtocolInfo::current(),
|
||||
cores: vec![CoreInspection {
|
||||
name: "verge-mihomo.exe".into(),
|
||||
@@ -203,7 +204,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
result,
|
||||
Ok(ServiceInstallOutcome::Sidecar {
|
||||
reason: format!("verge-mihomo.exe: {reason}")
|
||||
reason: format!("verge-mihomo.exe: {reason}").into()
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::core::{
|
||||
notification::{self, PendingFailure},
|
||||
runstate::{RUN_STATE, RunStateView},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Returns one coherent core/service snapshot instead of independently refreshed state.
|
||||
#[tauri::command]
|
||||
|
||||
@@ -4,18 +4,14 @@ use crate::{
|
||||
config::{Config, IVerge},
|
||||
core, feat,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn save_webdav_config(
|
||||
url: std::string::String,
|
||||
username: std::string::String,
|
||||
password: std::string::String,
|
||||
) -> CmdResult<()> {
|
||||
let url: String = url.into();
|
||||
let username: String = username.into();
|
||||
let password: String = password.into();
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let url: String = url;
|
||||
let username: String = username;
|
||||
let password: String = password;
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
@@ -48,15 +44,15 @@ pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::restore_webdav_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct IClashTemp(pub Mapping);
|
||||
@@ -84,12 +85,12 @@ impl IClashTemp {
|
||||
#[cfg(unix)]
|
||||
map.insert(
|
||||
"external-controller-unix".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
Self::guard_external_controller_ipc().as_str().into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
map.insert(
|
||||
"external-controller-pipe".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
Self::guard_external_controller_ipc().as_str().into(),
|
||||
);
|
||||
map.insert("tun".into(), tun_config.into());
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
@@ -134,12 +135,18 @@ impl IClashTemp {
|
||||
config.insert("mixed-port".into(), mixed_port.into());
|
||||
config.insert("socks-port".into(), socks_port.into());
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
config.insert("external-controller".into(), ctrl.as_str().into());
|
||||
|
||||
#[cfg(unix)]
|
||||
config.insert("external-controller-unix".into(), external_controller_unix.into());
|
||||
config.insert(
|
||||
"external-controller-unix".into(),
|
||||
external_controller_unix.as_str().into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
|
||||
config.insert(
|
||||
"external-controller-pipe".into(),
|
||||
external_controller_pipe.as_str().into(),
|
||||
);
|
||||
config
|
||||
}
|
||||
|
||||
@@ -166,9 +173,9 @@ impl IClashTemp {
|
||||
port: Self::guard_port(config),
|
||||
server: Self::guard_client_ctrl(config),
|
||||
secret: config.get("secret").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
Value::String(val_str) => Some(val_str.clone().into()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string().into()),
|
||||
Value::Number(val_num) => Some(val_num.to_string().into()),
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
@@ -180,45 +187,88 @@ impl IClashTemp {
|
||||
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
|
||||
pub fn get_mode(&self) -> Option<String> {
|
||||
self.0.get("mode").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::String(val_str) => Some(val_str.clone().into()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get(key)
|
||||
.get("redir-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(default);
|
||||
.unwrap_or(7895);
|
||||
if port == 0 {
|
||||
port = default;
|
||||
port = 7895;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn guard_tproxy_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
|
||||
let mut port = config
|
||||
.get("tproxy-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
|
||||
let raw_value = config.get("mixed-port");
|
||||
|
||||
let mut port = raw_value
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_MIXED);
|
||||
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_MIXED;
|
||||
}
|
||||
|
||||
port
|
||||
}
|
||||
|
||||
fn guard_socks_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
|
||||
let mut port = config
|
||||
.get("socks-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7898);
|
||||
if port == 0 {
|
||||
port = 7898;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
fn guard_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
|
||||
let mut port = config
|
||||
.get("port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7899);
|
||||
if port == 0 {
|
||||
port = 7899;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
|
||||
@@ -233,7 +283,7 @@ impl IClashTemp {
|
||||
false => Cow::Borrowed(val_str),
|
||||
};
|
||||
|
||||
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
|
||||
SocketAddr::from_str(&val).ok().map(|s| s.to_string().into())
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
@@ -247,7 +297,7 @@ impl IClashTemp {
|
||||
if socket.ip().is_unspecified() {
|
||||
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
}
|
||||
socket.to_string()
|
||||
socket.to_string().into()
|
||||
}
|
||||
Err(_) => "127.0.0.1:9097".into(),
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ use backon::{ExponentialBuilder, Retryable as _};
|
||||
use clash_verge_draft::Draft;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use tokio::sync::{Mutex, MutexGuard, OnceCell};
|
||||
use tokio::time::sleep;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
@@ -205,7 +205,7 @@ impl Config {
|
||||
Self::write_runtime_file(&yaml).await
|
||||
}
|
||||
|
||||
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
|
||||
pub(crate) async fn runtime_config_yaml() -> Result<String> {
|
||||
let runtime = Self::runtime().await;
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let runtime_lastest = runtime.latest_arc();
|
||||
@@ -216,7 +216,7 @@ impl Config {
|
||||
.or_else(|| runtime_data.config.as_ref())
|
||||
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
|
||||
let yaml_str = serde_yaml_ng::to_string(config)?;
|
||||
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
|
||||
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str).into())
|
||||
})
|
||||
.await
|
||||
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
|
||||
|
||||
@@ -4,8 +4,8 @@ use clash_verge_draft::DraftTransaction;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tristr::alias::String;
|
||||
|
||||
use super::{Config, IVerge};
|
||||
use crate::core::handle::Handle;
|
||||
@@ -110,7 +110,6 @@ impl DnsOverrideState {
|
||||
pub struct ProfileDnsSettings {
|
||||
pub enabled: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub confirmation: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
use tristr::alias::String;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
@@ -28,7 +29,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
let mut combined = nonce.to_vec();
|
||||
combined.extend(ciphertext);
|
||||
Ok(STANDARD.encode(combined))
|
||||
Ok(STANDARD.encode(combined).into())
|
||||
}
|
||||
|
||||
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
@@ -46,7 +47,10 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
||||
.decrypt(nonce.into(), ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||
|
||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf8(plaintext)
|
||||
.map_err(|e| e.into())
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -14,15 +14,6 @@ mod verge;
|
||||
pub(crate) use self::config::Config;
|
||||
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
|
||||
|
||||
macro_rules! patch_opt_field {
|
||||
($lv:expr, $rv:expr, $key:tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use patch_opt_field;
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::Value;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MixedPortFallback {
|
||||
@@ -141,8 +142,8 @@ impl Config {
|
||||
}
|
||||
|
||||
pub(crate) fn block_startup_core(error: &anyhow::Error) {
|
||||
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
|
||||
report_fallback_error(error.to_string());
|
||||
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string().into());
|
||||
report_fallback_error(error.to_string().into());
|
||||
}
|
||||
|
||||
pub(crate) fn startup_core_block_reason() -> Option<String> {
|
||||
|
||||
+114
-97
@@ -1,5 +1,3 @@
|
||||
use super::patch_opt_field;
|
||||
|
||||
use crate::{
|
||||
config::profiles,
|
||||
utils::{
|
||||
@@ -11,9 +9,9 @@ use crate::{
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
// TODO, use other re-export
|
||||
use reqwest_dav::re_exports::url::form_urlencoded;
|
||||
use tauri::Url;
|
||||
@@ -32,24 +30,18 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfItem {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub uid: Option<String>,
|
||||
|
||||
/// enum value: remote | local | script | merge
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -64,20 +56,16 @@ pub struct PrfItem {
|
||||
pub option: Option<PrfOption>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub home: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfSelected {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub now: Option<String>,
|
||||
}
|
||||
|
||||
@@ -93,7 +81,6 @@ pub struct PrfExtra {
|
||||
#[serde(default)]
|
||||
pub struct PrfOption {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -116,15 +103,10 @@ pub struct PrfOption {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_auto_update: Option<bool>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub merge: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub script: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub rules: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub proxies: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub groups: Option<String>,
|
||||
}
|
||||
|
||||
@@ -133,18 +115,19 @@ impl PrfOption {
|
||||
match (one, other) {
|
||||
(Some(a_ref), Some(b_ref)) => {
|
||||
let mut result = a_ref.clone();
|
||||
patch_opt_field!(result, b_ref, user_agent);
|
||||
patch_opt_field!(result, b_ref, with_proxy);
|
||||
patch_opt_field!(result, b_ref, self_proxy);
|
||||
patch_opt_field!(result, b_ref, danger_accept_invalid_certs);
|
||||
patch_opt_field!(result, b_ref, allow_auto_update);
|
||||
patch_opt_field!(result, b_ref, update_interval);
|
||||
patch_opt_field!(result, b_ref, merge);
|
||||
patch_opt_field!(result, b_ref, script);
|
||||
patch_opt_field!(result, b_ref, rules);
|
||||
patch_opt_field!(result, b_ref, proxies);
|
||||
patch_opt_field!(result, b_ref, groups);
|
||||
patch_opt_field!(result, b_ref, timeout_seconds);
|
||||
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
|
||||
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
|
||||
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
|
||||
result.danger_accept_invalid_certs =
|
||||
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs);
|
||||
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
|
||||
result.update_interval = b_ref.update_interval.or(result.update_interval);
|
||||
result.merge = b_ref.merge.clone().or(result.merge);
|
||||
result.script = b_ref.script.clone().or(result.script);
|
||||
result.rules = b_ref.rules.clone().or(result.rules);
|
||||
result.proxies = b_ref.proxies.clone().or(result.proxies);
|
||||
result.groups = b_ref.groups.clone().or(result.groups);
|
||||
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
|
||||
Some(result)
|
||||
}
|
||||
(Some(a_ref), None) => Some(a_ref.clone()),
|
||||
@@ -182,45 +165,13 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn append_companion_item(item: &mut Self) -> Result<String> {
|
||||
profiles::profiles_append_item_safe(item).await?;
|
||||
item.uid
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("appended companion item has no uid"))
|
||||
}
|
||||
|
||||
async fn ensure_companion_items(
|
||||
merge: &mut Option<String>,
|
||||
script: &mut Option<String>,
|
||||
rules: &mut Option<String>,
|
||||
proxies: &mut Option<String>,
|
||||
groups: &mut Option<String>,
|
||||
) -> Result<()> {
|
||||
if merge.is_none() {
|
||||
*merge = Some(Self::append_companion_item(&mut Self::from_merge(None)).await?);
|
||||
}
|
||||
if script.is_none() {
|
||||
*script = Some(Self::append_companion_item(&mut Self::from_script(None)).await?);
|
||||
}
|
||||
if rules.is_none() {
|
||||
*rules = Some(Self::append_companion_item(&mut Self::from_rules()).await?);
|
||||
}
|
||||
if proxies.is_none() {
|
||||
*proxies = Some(Self::append_companion_item(&mut Self::from_proxies()).await?);
|
||||
}
|
||||
if groups.is_none() {
|
||||
*groups = Some(Self::append_companion_item(&mut Self::from_groups()).await?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn from_local(
|
||||
name: String,
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<Self> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let uid = help::get_uid("L");
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let opt_ref = option.as_ref();
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
@@ -230,7 +181,31 @@ impl PrfItem {
|
||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None);
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None);
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
@@ -369,11 +344,11 @@ impl PrfItem {
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(normalize_profile_home_url);
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
|
||||
.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()));
|
||||
let data = resp.text();
|
||||
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
@@ -384,7 +359,31 @@ impl PrfItem {
|
||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||
}
|
||||
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None);
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None);
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
@@ -415,7 +414,7 @@ impl PrfItem {
|
||||
let (id, template) = if let Some(uid) = uid {
|
||||
(uid, tmpl::ITEM_MERGE.into())
|
||||
} else {
|
||||
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
(help::get_uid("m"), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
};
|
||||
let file = format!("{id}.yaml").into();
|
||||
|
||||
@@ -430,11 +429,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
pub(super) fn from_script(uid: Option<String>) -> Self {
|
||||
let id = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
help::get_uid("s").into()
|
||||
};
|
||||
let id = if let Some(uid) = uid { uid } else { help::get_uid("s") };
|
||||
let file = format!("{id}.js").into(); // js ext
|
||||
Self {
|
||||
uid: Some(id),
|
||||
@@ -446,30 +441,46 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self {
|
||||
let uid = help::get_uid(prefix).into();
|
||||
fn from_rules() -> Self {
|
||||
let uid = help::get_uid("r");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some(itype.into()),
|
||||
itype: Some("rules".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(template.into()),
|
||||
file_data: Some(tmpl::ITEM_RULES.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
Self::new_seq_item("r", "rules", tmpl::ITEM_RULES)
|
||||
}
|
||||
|
||||
fn from_proxies() -> Self {
|
||||
Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES)
|
||||
let uid = help::get_uid("p");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_PROXIES.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_groups() -> Self {
|
||||
Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS)
|
||||
let uid = help::get_uid("g");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_GROUPS.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file(&self) -> Result<String> {
|
||||
@@ -485,20 +496,26 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! current_field {
|
||||
($name:ident, $field:ident) => {
|
||||
pub(crate) fn $name(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.$field.as_ref())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
current_field!(current_merge, merge);
|
||||
current_field!(current_script, script);
|
||||
current_field!(current_rules, rules);
|
||||
current_field!(current_proxies, proxies);
|
||||
current_field!(current_groups, groups);
|
||||
pub(crate) fn current_merge(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_script(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.script.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_rules(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_proxies(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_groups(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
PrfOption, patch_opt_field,
|
||||
PrfOption,
|
||||
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
||||
};
|
||||
use crate::core::notify::{Refresh, announce};
|
||||
@@ -17,7 +17,6 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -30,6 +29,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
|
||||
use tokio::{fs, task::JoinHandle};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
|
||||
@@ -51,7 +51,6 @@ const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IProfiles {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
@@ -86,6 +85,14 @@ impl ProfileDeletePlan {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.to_owned();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
|
||||
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
|
||||
@@ -107,7 +114,7 @@ impl IProfiles {
|
||||
let mut home_changed = false;
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
if item.itype.as_deref() == Some("remote") {
|
||||
item.option
|
||||
@@ -236,15 +243,15 @@ impl IProfiles {
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid.as_deref() == Some(uid) {
|
||||
patch_opt_field!(each, item, itype);
|
||||
patch_opt_field!(each, item, name);
|
||||
patch_opt_field!(each, item, desc);
|
||||
patch_opt_field!(each, item, file);
|
||||
patch_opt_field!(each, item, url);
|
||||
patch_opt_field!(each, item, selected);
|
||||
patch_opt_field!(each, item, extra);
|
||||
patch_opt_field!(each, item, updated);
|
||||
patch_opt_field!(each, item, option);
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file().await;
|
||||
@@ -412,21 +419,6 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
/// Applies `edit` to the committed profiles; commits only when the edit succeeds.
|
||||
async fn with_profiles_edit<F, Fut>(edit: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(IProfiles) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<IProfiles>> + Send,
|
||||
{
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
let profiles = edit(profiles).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles_append_item_to_safe(&profiles, item).await
|
||||
@@ -442,19 +434,43 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
|
||||
with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.patch_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
|
||||
with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.reorder(active_id, over_id).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_save_file_safe() -> Result<()> {
|
||||
with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
profiles.save_file().await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
|
||||
with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.update_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -696,21 +712,19 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> {
|
||||
let current = profiles.current.clone()?;
|
||||
profiles
|
||||
.items
|
||||
.as_mut()?
|
||||
.iter_mut()
|
||||
.find(|item| item.uid.as_ref() == Some(¤t))
|
||||
}
|
||||
|
||||
/// Records a backend-made selection so the next core start restores it.
|
||||
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
|
||||
let group_name = String::from(group_name);
|
||||
let node = String::from(node);
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
@@ -745,7 +759,14 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
|
||||
|
||||
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::enhance::field::use_keys;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted},
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
constants::network,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
@@ -8,7 +8,7 @@ use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
@@ -39,7 +39,7 @@ pub struct IVerge {
|
||||
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Mihomo)
|
||||
/// show memory info (only for Clash Meta)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -432,90 +432,98 @@ impl IVerge {
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
patch_opt_field!(self, patch, app_log_level);
|
||||
patch_opt_field!(self, patch, app_log_max_size);
|
||||
patch_opt_field!(self, patch, app_log_max_count);
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
patch_opt_field!(self, patch, language);
|
||||
patch_opt_field!(self, patch, theme_mode);
|
||||
patch_opt_field!(self, patch, tray_event);
|
||||
patch_opt_field!(self, patch, env_type);
|
||||
patch_opt_field!(self, patch, start_page);
|
||||
patch_opt_field!(self, patch, startup_script);
|
||||
patch_opt_field!(self, patch, traffic_graph);
|
||||
patch_opt_field!(self, patch, enable_memory_usage);
|
||||
patch_opt_field!(self, patch, enable_group_icon);
|
||||
patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur);
|
||||
patch!(app_log_level);
|
||||
patch!(app_log_max_size);
|
||||
patch!(app_log_max_count);
|
||||
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
patch!(env_type);
|
||||
patch!(start_page);
|
||||
patch!(startup_script);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
patch!(enable_group_icon);
|
||||
patch!(pause_render_traffic_stats_on_blur);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch_opt_field!(self, patch, tray_icon);
|
||||
patch_opt_field!(self, patch, menu_icon);
|
||||
patch_opt_field!(self, patch, menu_order);
|
||||
patch_opt_field!(self, patch, proxy_group_tools_position);
|
||||
patch_opt_field!(self, patch, notice_position);
|
||||
patch_opt_field!(self, patch, collapse_navbar);
|
||||
patch_opt_field!(self, patch, common_tray_icon);
|
||||
patch_opt_field!(self, patch, sysproxy_tray_icon);
|
||||
patch_opt_field!(self, patch, tun_tray_icon);
|
||||
patch!(tray_icon);
|
||||
patch!(menu_icon);
|
||||
patch!(menu_order);
|
||||
patch!(proxy_group_tools_position);
|
||||
patch!(notice_position);
|
||||
patch!(collapse_navbar);
|
||||
patch!(common_tray_icon);
|
||||
patch!(sysproxy_tray_icon);
|
||||
patch!(tun_tray_icon);
|
||||
|
||||
patch_opt_field!(self, patch, enable_tun_mode);
|
||||
patch_opt_field!(self, patch, enable_auto_launch);
|
||||
patch_opt_field!(self, patch, enable_silent_start);
|
||||
patch_opt_field!(self, patch, enable_hover_jump_navigator);
|
||||
patch_opt_field!(self, patch, hover_jump_navigator_delay);
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
patch!(hover_jump_navigator_delay);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch_opt_field!(self, patch, verge_redir_port);
|
||||
patch!(verge_redir_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch_opt_field!(self, patch, verge_redir_enabled);
|
||||
patch!(verge_redir_enabled);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch_opt_field!(self, patch, verge_tproxy_port);
|
||||
patch!(verge_tproxy_port);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch_opt_field!(self, patch, verge_tproxy_enabled);
|
||||
patch_opt_field!(self, patch, verge_mixed_port);
|
||||
patch_opt_field!(self, patch, verge_socks_port);
|
||||
patch_opt_field!(self, patch, verge_socks_enabled);
|
||||
patch_opt_field!(self, patch, verge_port);
|
||||
patch_opt_field!(self, patch, verge_http_enabled);
|
||||
patch_opt_field!(self, patch, enable_system_proxy);
|
||||
patch_opt_field!(self, patch, enable_proxy_guard);
|
||||
patch_opt_field!(self, patch, enable_bypass_check);
|
||||
patch_opt_field!(self, patch, use_default_bypass);
|
||||
patch_opt_field!(self, patch, system_proxy_bypass);
|
||||
patch_opt_field!(self, patch, proxy_guard_duration);
|
||||
patch_opt_field!(self, patch, proxy_auto_config);
|
||||
patch_opt_field!(self, patch, pac_file_content);
|
||||
patch_opt_field!(self, patch, proxy_host);
|
||||
patch_opt_field!(self, patch, theme_setting);
|
||||
patch_opt_field!(self, patch, web_ui_list);
|
||||
patch_opt_field!(self, patch, clash_core);
|
||||
patch_opt_field!(self, patch, hotkeys);
|
||||
patch_opt_field!(self, patch, enable_global_hotkey);
|
||||
patch!(verge_tproxy_enabled);
|
||||
patch!(verge_mixed_port);
|
||||
patch!(verge_socks_port);
|
||||
patch!(verge_socks_enabled);
|
||||
patch!(verge_port);
|
||||
patch!(verge_http_enabled);
|
||||
patch!(enable_system_proxy);
|
||||
patch!(enable_proxy_guard);
|
||||
patch!(enable_bypass_check);
|
||||
patch!(use_default_bypass);
|
||||
patch!(system_proxy_bypass);
|
||||
patch!(proxy_guard_duration);
|
||||
patch!(proxy_auto_config);
|
||||
patch!(pac_file_content);
|
||||
patch!(proxy_host);
|
||||
patch!(theme_setting);
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
patch!(enable_global_hotkey);
|
||||
|
||||
patch_opt_field!(self, patch, auto_close_connection);
|
||||
patch_opt_field!(self, patch, auto_check_update);
|
||||
patch_opt_field!(self, patch, default_latency_test);
|
||||
patch_opt_field!(self, patch, default_latency_timeout);
|
||||
patch_opt_field!(self, patch, enable_auto_delay_detection);
|
||||
patch_opt_field!(self, patch, auto_delay_detection_interval_minutes);
|
||||
patch_opt_field!(self, patch, enable_builtin_enhanced);
|
||||
patch_opt_field!(self, patch, proxy_layout_column);
|
||||
patch_opt_field!(self, patch, test_list);
|
||||
patch_opt_field!(self, patch, auto_log_clean);
|
||||
patch_opt_field!(self, patch, enable_auto_backup_schedule);
|
||||
patch_opt_field!(self, patch, auto_backup_interval_hours);
|
||||
patch_opt_field!(self, patch, auto_backup_on_change);
|
||||
patch!(auto_close_connection);
|
||||
patch!(auto_check_update);
|
||||
patch!(default_latency_test);
|
||||
patch!(default_latency_timeout);
|
||||
patch!(enable_auto_delay_detection);
|
||||
patch!(auto_delay_detection_interval_minutes);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(auto_log_clean);
|
||||
patch!(enable_auto_backup_schedule);
|
||||
patch!(auto_backup_interval_hours);
|
||||
patch!(auto_backup_on_change);
|
||||
|
||||
patch_opt_field!(self, patch, webdav_url);
|
||||
patch_opt_field!(self, patch, webdav_username);
|
||||
patch_opt_field!(self, patch, webdav_password);
|
||||
patch!(webdav_url);
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch_opt_field!(self, patch, enable_tray_speed);
|
||||
patch_opt_field!(self, patch, tray_proxy_groups_display_mode);
|
||||
patch_opt_field!(self, patch, tray_inline_outbound_modes);
|
||||
patch_opt_field!(self, patch, enable_auto_light_weight_mode);
|
||||
patch_opt_field!(self, patch, auto_light_weight_minutes);
|
||||
patch_opt_field!(self, patch, enable_dns_settings);
|
||||
patch_opt_field!(self, patch, home_cards);
|
||||
patch_opt_field!(self, patch, enable_external_controller);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(tray_proxy_groups_display_mode);
|
||||
patch!(tray_inline_outbound_modes);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(enable_external_controller);
|
||||
}
|
||||
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
|
||||
@@ -14,13 +14,6 @@ pub mod network {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sysproxy {
|
||||
/// Single source of the macOS default bypass list; both proxy backends compose from it.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
}
|
||||
|
||||
pub mod timing {
|
||||
use super::Duration;
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
OnceLock,
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||
},
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
@@ -19,6 +20,7 @@ use tokio::{
|
||||
fs,
|
||||
sync::{Mutex, watch},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
const DEFAULT_INTERVAL_HOURS: u64 = 24;
|
||||
const MIN_INTERVAL_HOURS: u64 = 1;
|
||||
@@ -81,9 +83,8 @@ impl Default for AutoBackupSettings {
|
||||
}
|
||||
|
||||
pub struct AutoBackupManager {
|
||||
settings: Arc<RwLock<AutoBackupSettings>>,
|
||||
settings_tx: watch::Sender<AutoBackupSettings>,
|
||||
// The watch channel is the single settings store; no duplicate copies.
|
||||
settings_rx: watch::Receiver<AutoBackupSettings>,
|
||||
runner_started: AtomicBool,
|
||||
exec_lock: Mutex<()>,
|
||||
last_backup: AtomicI64,
|
||||
@@ -93,10 +94,10 @@ impl AutoBackupManager {
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
|
||||
INSTANCE.get_or_init(|| {
|
||||
let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default());
|
||||
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
||||
Self {
|
||||
settings_tx,
|
||||
settings_rx,
|
||||
settings: Arc::new(RwLock::new(AutoBackupSettings::default())),
|
||||
settings_tx: tx,
|
||||
runner_started: AtomicBool::new(false),
|
||||
exec_lock: Mutex::new(()),
|
||||
last_backup: AtomicI64::new(0),
|
||||
@@ -106,6 +107,9 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -113,6 +117,9 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn refresh_settings(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -187,7 +194,7 @@ impl AutoBackupManager {
|
||||
}
|
||||
|
||||
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> {
|
||||
let snapshot = *self.settings_rx.borrow();
|
||||
let snapshot = *self.settings.read();
|
||||
|
||||
if trigger.is_schedule() && !snapshot.schedule_enabled {
|
||||
return Ok(());
|
||||
@@ -205,7 +212,7 @@ impl AutoBackupManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?;
|
||||
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug())).await?;
|
||||
self.last_backup.store(Local::now().timestamp(), Ordering::Release);
|
||||
|
||||
if let Err(err) = cleanup_auto_backups().await {
|
||||
@@ -233,8 +240,8 @@ impl AutoBackupManager {
|
||||
|
||||
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
|
||||
match file_name.rsplit_once('.') {
|
||||
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
|
||||
None => format!("{file_name}{AUTO_MARKER}{slug}"),
|
||||
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}").into(),
|
||||
None => format!("{file_name}{AUTO_MARKER}{slug}").into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use backon::{ConstantBuilder, Retryable as _};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
@@ -15,6 +14,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{fs, time::timeout};
|
||||
use tristr::alias::String;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{config::Config, core::handle, core::lightweight::entry_lightweight_m
|
||||
use anyhow::{Result, bail};
|
||||
use arc_swap::ArcSwap;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
@@ -18,6 +17,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Enum representing all available hotkey functions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -466,3 +466,12 @@ impl Hotkey {
|
||||
(del_list, add_list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,47 +40,8 @@ impl LightweightState {
|
||||
|
||||
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
|
||||
|
||||
struct WindowListener {
|
||||
handler_id: AtomicU32,
|
||||
event: &'static str,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl WindowListener {
|
||||
fn setup(&self, on_event: impl Fn() + Send + 'static) {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label);
|
||||
}
|
||||
let handler_id = window.listen(self.event, move |_event| on_event());
|
||||
self.handler_id.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
let id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了{}监听", self.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://close-requested",
|
||||
label: "窗口关闭",
|
||||
};
|
||||
|
||||
static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://focus",
|
||||
label: "窗口焦点",
|
||||
};
|
||||
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
|
||||
|
||||
@@ -193,27 +154,55 @@ pub async fn add_light_weight_timer() {
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
WINDOW_CLOSE_LISTENER.setup(|| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
WINDOW_CLOSE_LISTENER.cancel();
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
WEBVIEW_FOCUS_LISTENER.setup(|| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_webview_focus_listener() {
|
||||
WEBVIEW_FOCUS_LISTENER.cancel();
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_light_weight_timer() {
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// The config key of the Mixed Port, named once so callers stop spelling it out.
|
||||
pub(crate) const MIXED_PORT_KEY: &str = "mixed-port";
|
||||
@@ -163,7 +164,7 @@ pub(crate) fn probe_listener(request: &ListenerProbe) -> ListenerProbeOutcome {
|
||||
Ok(address) => address,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: error.to_string(),
|
||||
message: error.to_string().into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -201,7 +202,7 @@ pub(crate) fn probe_proxy_port_change(
|
||||
Ok(claims) => claims,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: format!("current proxy listener configuration is invalid: {error:#}"),
|
||||
message: format!("current proxy listener configuration is invalid: {error:#}").into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -212,7 +213,7 @@ pub(crate) fn probe_proxy_port_change(
|
||||
Ok(claims) => claims,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: format!("candidate proxy listener configuration is invalid: {error:#}"),
|
||||
message: format!("candidate proxy listener configuration is invalid: {error:#}").into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -401,7 +402,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
|
||||
claim.socket_addr(),
|
||||
transport_name(claim.transport),
|
||||
error
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -412,7 +414,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
|
||||
claim.socket_addr(),
|
||||
transport_name(claim.transport),
|
||||
error
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -739,7 +742,7 @@ mod tests {
|
||||
let port = listener.local_addr()?.port();
|
||||
assert_eq!(
|
||||
probe_listener(&ListenerProbe {
|
||||
address: format!("0.0.0.0:{port}"),
|
||||
address: format!("0.0.0.0:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
}),
|
||||
ListenerProbeOutcome::Conflict {
|
||||
@@ -755,7 +758,7 @@ mod tests {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -773,7 +776,7 @@ mod tests {
|
||||
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -809,7 +812,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
}),
|
||||
ListenerProbeOutcome::Available
|
||||
@@ -822,7 +825,7 @@ mod tests {
|
||||
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let port = socket.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Udp],
|
||||
});
|
||||
assert_eq!(
|
||||
|
||||
@@ -15,7 +15,6 @@ use clash_verge_draft::DraftTransaction;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_service_ipc::StageRuntimeOutcome;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::PathBuf,
|
||||
@@ -23,6 +22,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri_plugin_mihomo::Error as MihomoError;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// A staging result separated from the policy that acts on it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -92,7 +92,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
|
||||
match crate::core::service::stage_runtime_by_service(path).await {
|
||||
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
|
||||
Ok(StageRequest::Refused { code, message }) => {
|
||||
let message = message.into();
|
||||
let message = message;
|
||||
if StageRequest::is_about_the_bundle(code) {
|
||||
StageAttempt::RefusedTheBundle(message)
|
||||
} else {
|
||||
|
||||
@@ -7,10 +7,10 @@ use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
|
||||
@@ -194,7 +194,7 @@ async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, Chec
|
||||
start_service: Start,
|
||||
confirm_idle: Check,
|
||||
start_sidecar: Fallback,
|
||||
record_unavailable: impl FnOnce(std::string::String),
|
||||
record_unavailable: impl FnOnce(String),
|
||||
) -> Result<()>
|
||||
where
|
||||
Start: FnOnce() -> StartFuture,
|
||||
@@ -217,7 +217,7 @@ where
|
||||
// A lost start response can hide a running core; the shared check must resolve occupancy.
|
||||
if let Err(probe_error) = confirm_idle().await {
|
||||
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
|
||||
record_unavailable(format!("{error:#}"));
|
||||
record_unavailable(format!("{error:#}").into());
|
||||
return Err(error);
|
||||
}
|
||||
logging!(
|
||||
@@ -650,7 +650,7 @@ impl CoreManager {
|
||||
|error| self.start_sidecar_after_service_failure(error),
|
||||
|reason| {
|
||||
crate::core::runstate::RUN_STATE
|
||||
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
|
||||
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason.into()));
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -688,7 +688,7 @@ impl CoreManager {
|
||||
}
|
||||
let reason = match state.health {
|
||||
ServiceHealth::Unavailable(reason) => reason,
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".into(),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
|
||||
@@ -712,7 +712,7 @@ impl CoreManager {
|
||||
if Handle::global().is_exiting() {
|
||||
return Err(error.context("application exited before Sidecar fallback"));
|
||||
}
|
||||
let reason = format!("{error:#}");
|
||||
let reason: String = format!("{error:#}").into();
|
||||
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
|
||||
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
|
||||
let result = self.start_core_by_sidecar().await;
|
||||
@@ -1590,7 +1590,7 @@ mod tests {
|
||||
started.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
},
|
||||
|reason| store.observe(ServiceHealth::Unavailable(reason)),
|
||||
|reason| store.observe(ServiceHealth::Unavailable(reason.into())),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use tauri_plugin_mihomo::MihomoExt as _;
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
|
||||
let detail = error.to_string();
|
||||
@@ -27,7 +28,7 @@ pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
|
||||
@@ -40,7 +41,7 @@ fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
tun.as_mapping_mut()
|
||||
.context("invalid Sidecar TUN configuration")?
|
||||
.insert(Value::from("enable"), Value::Bool(false));
|
||||
Ok(serde_yaml_ng::to_string(&config)?)
|
||||
Ok(serde_yaml_ng::to_string(&config)?.into())
|
||||
}
|
||||
|
||||
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
|
||||
@@ -153,7 +154,7 @@ impl CoreManager {
|
||||
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => service::get_clash_logs_by_service().await,
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().into_iter().map(Into::into).collect()),
|
||||
RunningMode::NotRunning => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
@@ -257,7 +258,9 @@ impl CoreManager {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(line)
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let message = String::from_utf8_lossy_owned(line);
|
||||
// from_utf8_lossy_owned exists only on the std String type; the tristr
|
||||
// alias shadows the bare name. LogRing stores std Strings.
|
||||
let message = std::string::String::from_utf8_lossy_owned(line);
|
||||
Logger::global().writer_sidecar_log(Level::Error, &message);
|
||||
CLASH_LOGGER.append_log(message);
|
||||
}
|
||||
@@ -265,12 +268,12 @@ impl CoreManager {
|
||||
let _ = terminated.send(());
|
||||
let manager = Self::global();
|
||||
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
|
||||
let message = if let Some(code) = term.code {
|
||||
format!("Process terminated with code: {}", code)
|
||||
let message: String = if let Some(code) = term.code {
|
||||
format!("Process terminated with code: {}", code).into()
|
||||
} else if let Some(signal) = term.signal {
|
||||
format!("Process terminated by signal: {}", signal)
|
||||
format!("Process terminated by signal: {}", signal).into()
|
||||
} else {
|
||||
String::from("Process terminated")
|
||||
"Process terminated".into()
|
||||
};
|
||||
Logger::global().writer_sidecar_log(Level::Info, &message);
|
||||
CLASH_LOGGER.clear_logs();
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use super::NoticeStatus;
|
||||
use crate::{APP_HANDLE, singleton};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
|
||||
use tristr::alias::String;
|
||||
|
||||
use super::notification::{FrontendEvent, NotificationSystem};
|
||||
|
||||
@@ -73,7 +70,7 @@ impl Handle {
|
||||
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||
}
|
||||
|
||||
pub fn notice(status: NoticeStatus, message: impl Into<Arc<str>>) {
|
||||
pub fn notice(status: NoticeStatus, message: impl Into<String>) {
|
||||
Self::send_event(FrontendEvent::NoticeMessage {
|
||||
status,
|
||||
message: message.into(),
|
||||
|
||||
@@ -3,11 +3,10 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(test)]
|
||||
use serde_json::json;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::{collections::HashMap, future::Future};
|
||||
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FrontendEvent<'a> {
|
||||
@@ -17,7 +16,7 @@ pub enum FrontendEvent<'a> {
|
||||
RefreshProxyConfig,
|
||||
NoticeMessage {
|
||||
status: NoticeStatus,
|
||||
message: Arc<str>,
|
||||
message: String,
|
||||
},
|
||||
ProfileChanged {
|
||||
current_profile_id: &'a String,
|
||||
@@ -77,10 +76,8 @@ impl FailedOperation {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PendingFailure {
|
||||
/// Stable code used as the table key.
|
||||
#[specta(type = std::string::String)]
|
||||
pub code: String,
|
||||
/// Full diagnostic context chain.
|
||||
#[specta(type = std::string::String)]
|
||||
pub detail: String,
|
||||
pub operation: FailedOperation,
|
||||
/// Monotonic identity for repeated failures under the same code.
|
||||
@@ -247,7 +244,6 @@ static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::de
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
|
||||
pub struct SidecarFailureSnapshot {
|
||||
pub revision: u64,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
@@ -391,7 +387,7 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
|
||||
(
|
||||
FrontendEvent::NoticeMessage {
|
||||
status: NoticeStatus::Info,
|
||||
message: Arc::from("ok"),
|
||||
message: String::from("ok"),
|
||||
},
|
||||
"verge://notice-message",
|
||||
json!(["info", "ok"]),
|
||||
@@ -456,16 +452,16 @@ frontend_events! {
|
||||
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
|
||||
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
|
||||
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
|
||||
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
|
||||
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
|
||||
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
|
||||
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
notice: (NoticeStatus, String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.clone());
|
||||
profile_changed: String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.clone();
|
||||
timer_updated: String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.clone();
|
||||
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.clone() };
|
||||
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.clone() };
|
||||
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
|
||||
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct ProfileUpdatePayload {
|
||||
pub uid: std::string::String,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::utils::dirs;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clash_verge_service_ipc::{OwnerCredentials, OwnerIdentity};
|
||||
use std::path::Path;
|
||||
#[cfg(windows)]
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) fn current_owner_credentials() -> Result<OwnerCredentials> {
|
||||
current_owner_credentials_for_root(&dirs::app_home_dir()?)
|
||||
@@ -39,16 +41,16 @@ pub(crate) fn current_owner_credentials_for_root(app_root: &Path) -> Result<Owne
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<String>)> {
|
||||
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<std::string::String>)> {
|
||||
let sid = windows_owner::current_sid()?;
|
||||
let token = windows_owner::load_or_create_token(app_data_root, &sid)?;
|
||||
Ok((OwnerIdentity::Windows { sid }, Some(token)))
|
||||
Ok((OwnerIdentity::Windows { sid: sid.into() }, Some(token.into())))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_current_identity() -> Result<OwnerIdentity> {
|
||||
Ok(OwnerIdentity::Windows {
|
||||
sid: windows_owner::current_sid()?,
|
||||
sid: windows_owner::current_sid()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,7 +81,7 @@ pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
|
||||
let sid = windows_owner::current_sid()?;
|
||||
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)"))
|
||||
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)").into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -644,17 +646,19 @@ mod windows_owner {
|
||||
}
|
||||
let value = LocalWideString(value);
|
||||
let length = (0..).take_while(|index| unsafe { *value.0.add(*index) } != 0).count();
|
||||
String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
|
||||
// from_utf16 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
|
||||
.context("process SID is not valid UTF-16")
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn encode_token(token: &[u8; TOKEN_BYTES]) -> String {
|
||||
let mut encoded = String::with_capacity(TOKEN_BYTES * 2);
|
||||
let mut encoded = std::string::String::with_capacity(TOKEN_BYTES * 2);
|
||||
for byte in token {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(encoded, "{byte:02x}");
|
||||
}
|
||||
encoded
|
||||
encoded.into()
|
||||
}
|
||||
|
||||
fn wide_path(path: &Path) -> Result<Vec<u16>> {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
notification::{self, FailedOperation},
|
||||
runstate::RUN_STATE,
|
||||
service,
|
||||
sysopt::{Sysopt, format_bypass},
|
||||
sysopt::Sysopt,
|
||||
},
|
||||
process::AsyncHandler,
|
||||
utils::{
|
||||
@@ -24,6 +24,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{sync::Mutex, time::timeout};
|
||||
use tristr::alias::String;
|
||||
|
||||
const CLEAR_ATTEMPTS: u32 = 3;
|
||||
const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100);
|
||||
@@ -204,6 +205,8 @@ const fn refusal_classification(service_usable: Option<bool>) -> SysproxyFailure
|
||||
}
|
||||
|
||||
const LOOPBACK_HOST: &str = "127.0.0.1";
|
||||
const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
const MAX_SERVICE_BYPASS_LEN: usize = 8192;
|
||||
|
||||
static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new();
|
||||
@@ -318,14 +321,21 @@ fn truncate_utf8(value: &mut String, max_bytes: usize) {
|
||||
while !value.is_char_boundary(boundary) {
|
||||
boundary -= 1;
|
||||
}
|
||||
value.truncate(boundary);
|
||||
let truncated = value.as_str()[..boundary].into();
|
||||
*value = truncated;
|
||||
}
|
||||
|
||||
fn service_bypass(verge: &IVerge) -> Result<String> {
|
||||
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
|
||||
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
|
||||
|
||||
let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into();
|
||||
let mut bypass = if custom.is_empty() {
|
||||
MACOS_DEFAULT_BYPASS.into()
|
||||
} else if verge.use_default_bypass.unwrap_or(true) {
|
||||
format!("{MACOS_DEFAULT_BYPASS},{custom}").into()
|
||||
} else {
|
||||
custom.into()
|
||||
};
|
||||
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
|
||||
Ok(bypass)
|
||||
}
|
||||
@@ -345,7 +355,7 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
|
||||
Ok(MacosProxyConfig::Global {
|
||||
host: LOOPBACK_HOST.to_owned(),
|
||||
port: mixed_port,
|
||||
bypass: service_bypass(verge)?,
|
||||
bypass: service_bypass(verge)?.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -360,7 +370,7 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
|
||||
// Direct fallback satisfies a disable request.
|
||||
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
|
||||
ProxyApplyOutcome::DirectFallback { message } => {
|
||||
Err(SysproxyFailure::DirectFallback { detail: message }.into())
|
||||
Err(SysproxyFailure::DirectFallback { detail: message.into() }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -636,7 +646,7 @@ const GUARD_FAILURES_BEFORE_STOPPING: u32 = 3;
|
||||
|
||||
async fn report_guard_stopped(captured_generation: u64, error: &anyhow::Error) {
|
||||
let stopped = anyhow::Error::new(SysproxyFailure::GuardStopped {
|
||||
detail: format!("{error:#}"),
|
||||
detail: format!("{error:#}").into(),
|
||||
});
|
||||
// Do not publish a late failure from a replaced guard.
|
||||
let recorded = SERVICE_PROXY_OPERATIONS
|
||||
@@ -1098,7 +1108,7 @@ mod tests {
|
||||
let requested = MacosProxyConfig::Global {
|
||||
host: "127.0.0.1".to_owned(),
|
||||
port: 7890,
|
||||
bypass: String::new(),
|
||||
bypass: std::string::String::new(),
|
||||
};
|
||||
|
||||
let error = service_apply_result(
|
||||
@@ -1138,8 +1148,8 @@ mod tests {
|
||||
let events = Mutex::new(Vec::new());
|
||||
super::settle_table(
|
||||
effect,
|
||||
|asked| events.lock().push(format!("retire:{asked:?}")),
|
||||
|operation, _| events.lock().push(format!("file:{operation:?}")),
|
||||
|asked| events.lock().push(format!("retire:{asked:?}").into()),
|
||||
|operation, _| events.lock().push(format!("file:{operation:?}").into()),
|
||||
);
|
||||
events.into_inner()
|
||||
}
|
||||
@@ -1268,7 +1278,7 @@ mod tests {
|
||||
let combined = rollback_failure(
|
||||
mappable(),
|
||||
anyhow::anyhow!("the service could not set it").context(SysproxyFailure::DirectFallback {
|
||||
detail: "fell back to direct".to_owned(),
|
||||
detail: "fell back to direct".into(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::Serialize;
|
||||
use tauri_plugin_mihomo::models::{
|
||||
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub struct ProxyViewInput {
|
||||
pub runtime_group_order: Vec<String>,
|
||||
@@ -270,7 +271,7 @@ impl ProxyViewBuilder {
|
||||
|
||||
let global = core_groups
|
||||
.remove("GLOBAL")
|
||||
.map(|proxy| build_group("GLOBAL".to_owned(), proxy, &resolver));
|
||||
.map(|proxy| build_group(String::from("GLOBAL"), proxy, &resolver));
|
||||
let (groups, order_source) = build_ordered_groups(core_groups, runtime_group_order, &resolver);
|
||||
let direct = core_node_ids.get("DIRECT").cloned();
|
||||
let standalone = build_standalone(&core_node_ids);
|
||||
@@ -294,6 +295,7 @@ fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String
|
||||
let mut nodes = BTreeMap::new();
|
||||
|
||||
for (name, proxy) in proxies.proxies {
|
||||
let name = String::from(name);
|
||||
if proxy.all.is_some() {
|
||||
groups.insert(name, proxy);
|
||||
} else {
|
||||
@@ -311,7 +313,7 @@ fn build_core_records(
|
||||
let mut ids = BTreeMap::new();
|
||||
|
||||
for (index, (name, proxy)) in core_nodes.into_iter().enumerate() {
|
||||
let record_id = format!("c:{index}");
|
||||
let record_id = String::from(format!("c:{index}"));
|
||||
ids.insert(name.clone(), record_id.clone());
|
||||
records.insert(
|
||||
record_id.clone(),
|
||||
@@ -332,7 +334,13 @@ fn build_provider_records(
|
||||
records: &mut BTreeMap<String, ProxyNodeView>,
|
||||
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
|
||||
let providers = providers
|
||||
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
|
||||
.map(|providers| {
|
||||
providers
|
||||
.providers
|
||||
.into_iter()
|
||||
.map(|(name, provider)| (String::from(name), provider))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut views = Vec::new();
|
||||
let mut candidates = BTreeMap::new();
|
||||
@@ -355,9 +363,9 @@ fn build_provider_records(
|
||||
let mut proxy_record_ids = Vec::new();
|
||||
|
||||
for (member_index, proxy) in proxies.into_iter().enumerate() {
|
||||
let record_id = format!("p:{provider_index}:{member_index}");
|
||||
let proxy_name = proxy.name.clone();
|
||||
let node_name = proxy.name.clone();
|
||||
let record_id = String::from(format!("p:{provider_index}:{member_index}"));
|
||||
let proxy_name = String::from(proxy.name.as_str());
|
||||
let node_name = String::from(proxy.name.as_str());
|
||||
candidates
|
||||
.entry(proxy_name.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
@@ -380,7 +388,7 @@ fn build_provider_records(
|
||||
views.push(ProxyProviderView {
|
||||
name: provider_name,
|
||||
vehicle_type,
|
||||
updated_at,
|
||||
updated_at: updated_at.map(String::from),
|
||||
subscription_info: subscription_info.map(|subscription_info| ProxySubscriptionInfo {
|
||||
upload: subscription_info.upload,
|
||||
download: subscription_info.download,
|
||||
@@ -417,6 +425,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|member| {
|
||||
let member = String::from(member);
|
||||
let (occurrence, scoped) = seen
|
||||
.entry(member.clone())
|
||||
.or_insert_with(|| (0, resolver.scoped_candidates(&name, &member)));
|
||||
@@ -429,11 +438,11 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
ProxyGroupView {
|
||||
name,
|
||||
proxy_type,
|
||||
now,
|
||||
fixed,
|
||||
now: now.map(String::from),
|
||||
fixed: fixed.map(String::from),
|
||||
hidden,
|
||||
icon,
|
||||
test_url,
|
||||
icon: icon.map(String::from),
|
||||
test_url: test_url.map(String::from),
|
||||
history,
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
@@ -467,10 +476,10 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
name,
|
||||
proxy_type,
|
||||
history,
|
||||
id,
|
||||
id: id.map(String::from),
|
||||
hidden,
|
||||
icon,
|
||||
test_url,
|
||||
icon: icon.map(String::from),
|
||||
test_url: test_url.map(String::from),
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
xudp,
|
||||
|
||||
@@ -64,7 +64,7 @@ impl RunStateEnv for RealEnv {
|
||||
Ok(ServiceVersionReply {
|
||||
core,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
message: response.message.into(),
|
||||
protocol: response.data,
|
||||
})
|
||||
}
|
||||
@@ -123,6 +123,7 @@ mod fake {
|
||||
|
||||
use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply};
|
||||
use crate::core::manager::RunningMode;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// A fail-closed scripted machine that records outbound effects.
|
||||
#[derive(Debug)]
|
||||
@@ -163,7 +164,7 @@ mod fake {
|
||||
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
}))
|
||||
}
|
||||
@@ -173,7 +174,7 @@ mod fake {
|
||||
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: None,
|
||||
}))
|
||||
}
|
||||
@@ -181,12 +182,12 @@ mod fake {
|
||||
#[must_use]
|
||||
pub fn service_unreachable(self) -> Self {
|
||||
self.with_evidence(true)
|
||||
.always_replying(Err("ipc transport refused".to_owned()))
|
||||
.always_replying(Err("ipc transport refused".into()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn evidence_unavailable(mut self) -> Self {
|
||||
self.evidence = Err("registry probe failed".to_owned());
|
||||
self.evidence = Err("registry probe failed".into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ mod fake {
|
||||
|
||||
#[must_use]
|
||||
pub fn privileged_operations_fail(self, reason: &str) -> Self {
|
||||
*self.privileged_outcome.lock() = Err(reason.to_owned());
|
||||
*self.privileged_outcome.lock() = Err(reason.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -256,7 +257,7 @@ mod fake {
|
||||
replies
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Err("no service configured".to_owned()))
|
||||
.unwrap_or_else(|| Err("no service configured".into()))
|
||||
};
|
||||
reply.map_err(|error| anyhow!(error))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Core lifecycle remains in `CoreManager`.
|
||||
|
||||
use crate::core::manager::RunningMode;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum ServiceHealth {
|
||||
|
||||
@@ -173,7 +173,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
|
||||
Type::Service,
|
||||
"failed to inspect trusted service evidence: {error:#}"
|
||||
);
|
||||
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}"));
|
||||
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}").into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -331,7 +331,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
|
||||
pub fn allow_sidecar_after_service_refusal(&self, reason: tristr::alias::String) -> Result<()> {
|
||||
let mut state = self.service.lock();
|
||||
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
|
||||
bail!("cannot fall back while a service operation is pending");
|
||||
@@ -496,7 +496,7 @@ mod tests {
|
||||
ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(clash_verge_service_ipc::ProtocolInfo::current()),
|
||||
}
|
||||
}
|
||||
@@ -713,7 +713,7 @@ mod tests {
|
||||
for health in [
|
||||
ServiceHealth::NotInstalled,
|
||||
ServiceHealth::VersionMismatch,
|
||||
ServiceHealth::Unavailable("boom".to_owned()),
|
||||
ServiceHealth::Unavailable("boom".into()),
|
||||
] {
|
||||
let store = with_env(FakeEnv::new());
|
||||
store.observe(health.clone());
|
||||
@@ -858,7 +858,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn awaiting_readiness_retries_a_silent_service_then_accepts_its_reply() {
|
||||
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".to_owned()), Ok(ready_reply())]));
|
||||
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".into()), Ok(ready_reply())]));
|
||||
|
||||
let state = store
|
||||
.await_ready(2, Duration::ZERO)
|
||||
|
||||
@@ -9,6 +9,7 @@ use clash_verge_service_ipc::{
|
||||
};
|
||||
|
||||
use super::health::ServiceHealth;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// What the Service replied to a protocol-version query.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -50,7 +51,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
|
||||
return match &reply.core {
|
||||
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
|
||||
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
|
||||
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}"))
|
||||
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}").into())
|
||||
}
|
||||
_ => ServiceVersionCheck::CoreUnavailable(
|
||||
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
|
||||
@@ -77,9 +78,12 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
|
||||
reply.code, reply.message, client.epoch
|
||||
)
|
||||
};
|
||||
ServiceVersionCheck::NeedsReinstall(format!(
|
||||
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
|
||||
))
|
||||
ServiceVersionCheck::NeedsReinstall(
|
||||
format!(
|
||||
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Classify a version reply into the probe outcome used by [`classify_service_health`].
|
||||
@@ -104,7 +108,7 @@ pub fn classify_service_health(
|
||||
match current {
|
||||
CurrentServiceProbe::Ready => ServiceHealth::Ready,
|
||||
CurrentServiceProbe::VersionMismatch => ServiceHealth::VersionMismatch,
|
||||
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.to_owned()),
|
||||
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.into()),
|
||||
CurrentServiceProbe::Missing if has_install_marker => ServiceHealth::VersionMismatch,
|
||||
CurrentServiceProbe::Missing => ServiceHealth::NotInstalled,
|
||||
}
|
||||
@@ -119,7 +123,7 @@ mod tests {
|
||||
ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
}
|
||||
}
|
||||
@@ -154,7 +158,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: None,
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -172,7 +176,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 7,
|
||||
message: "nope".to_owned(),
|
||||
message: "nope".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -188,7 +192,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(info),
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -209,7 +213,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(info),
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -228,7 +232,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: None,
|
||||
code,
|
||||
message: "unsupported command".to_owned(),
|
||||
message: "unsupported command".into(),
|
||||
protocol: protocol.clone(),
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -250,7 +254,7 @@ mod tests {
|
||||
fn an_unreachable_service_keeps_the_reason() {
|
||||
assert_eq!(
|
||||
classify_service_health(CurrentServiceProbe::Unavailable, true, "socket refused"),
|
||||
ServiceHealth::Unavailable("socket refused".to_owned())
|
||||
ServiceHealth::Unavailable("socket refused".into())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) const GEO_ASSETS: &[&str] = &[
|
||||
"Country.mmdb",
|
||||
@@ -64,7 +65,7 @@ struct ProviderPathOwners {
|
||||
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
|
||||
let config_root = std::fs::canonicalize(config_root)?;
|
||||
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
|
||||
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
|
||||
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).into()).collect();
|
||||
for section in ["proxy-providers", "rule-providers"] {
|
||||
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
|
||||
continue;
|
||||
@@ -89,7 +90,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
|
||||
match (is_remote, provider.get("url").and_then(Value::as_str)) {
|
||||
(true, Some(url)) => owners
|
||||
.remote
|
||||
.entry(url.to_owned())
|
||||
.entry(url.into())
|
||||
.or_default()
|
||||
.push((section, name.clone())),
|
||||
_ => owners.local = true,
|
||||
@@ -105,7 +106,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
|
||||
for (url, providers) in owners.remote {
|
||||
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
|
||||
for (section, name) in providers {
|
||||
config[section][&name]["path"] = Value::String(replacement.clone());
|
||||
config[section][&name]["path"] = Value::String(replacement.clone().into());
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
@@ -131,15 +132,16 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
|
||||
let mut suffix = String::new();
|
||||
let mut sequence = 0;
|
||||
loop {
|
||||
let candidate = path
|
||||
let candidate: String = path
|
||||
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
.replace('\\', "/")
|
||||
.into();
|
||||
if reserved.insert(candidate.clone()) {
|
||||
return candidate;
|
||||
}
|
||||
sequence += 1;
|
||||
suffix = format!("-{sequence}");
|
||||
suffix = format!("-{sequence}").into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +169,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
|
||||
)?;
|
||||
for filename in GEO_ASSETS {
|
||||
let source = config_root.join(filename);
|
||||
if source.is_file() && destinations.insert((*filename).to_string()) {
|
||||
if source.is_file() && destinations.insert((*filename).into()) {
|
||||
assets.push(RuntimeAsset {
|
||||
source: std::fs::canonicalize(&source)?.to_string_lossy().into_owned(),
|
||||
destination: (*filename).to_string(),
|
||||
@@ -228,9 +230,9 @@ fn collect_provider_assets(
|
||||
}
|
||||
remote_providers.push(RemoteProviderRef {
|
||||
section,
|
||||
name: name.as_str().unwrap_or_default().to_owned(),
|
||||
name: name.as_str().unwrap_or_default().into(),
|
||||
provider: RemoteProvider {
|
||||
destination: destination.clone(),
|
||||
destination: destination.clone().into(),
|
||||
url,
|
||||
},
|
||||
});
|
||||
@@ -259,13 +261,13 @@ fn collect_provider_assets(
|
||||
if destinations.insert(destination.clone()) {
|
||||
assets.push(RuntimeAsset {
|
||||
source: source.to_string_lossy().into_owned(),
|
||||
destination: destination.clone(),
|
||||
destination: destination.clone().into(),
|
||||
});
|
||||
}
|
||||
destination
|
||||
}
|
||||
};
|
||||
provider.insert(Value::String("path".to_owned()), Value::String(destination));
|
||||
provider.insert(Value::String("path".to_owned()), Value::String(destination.into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -339,7 +341,7 @@ fn normalized_destination(relative: &Path) -> Result<String> {
|
||||
if destination.as_os_str().is_empty() {
|
||||
bail!("local runtime provider destination is empty");
|
||||
}
|
||||
Ok(destination.to_string_lossy().replace('\\', "/"))
|
||||
Ok(destination.to_string_lossy().replace('\\', "/").into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -385,12 +387,12 @@ mod tests {
|
||||
for declared in readback {
|
||||
assert!(bundle.remote_providers.contains(&declared.provider));
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["path"],
|
||||
config[declared.section][declared.name.as_str()]["path"],
|
||||
declared.provider.destination
|
||||
);
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["url"],
|
||||
original[declared.section][&declared.name]["url"]
|
||||
config[declared.section][declared.name.as_str()]["url"],
|
||||
original[declared.section][declared.name.as_str()]["url"]
|
||||
);
|
||||
}
|
||||
std::fs::remove_dir_all(root)?;
|
||||
|
||||
@@ -36,6 +36,7 @@ use std::{
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(None);
|
||||
@@ -62,7 +63,7 @@ pub(crate) fn is_not_auto_started(reason: &str) -> bool {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn notify_service_fallback(reason: &str) {
|
||||
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned());
|
||||
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.into());
|
||||
Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, "");
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ fn app_data_owner_command(credentials: &OwnerCredentials) -> Option<String> {
|
||||
return None;
|
||||
};
|
||||
let path = credentials.app_data_dir.replace('\'', r"'\''");
|
||||
Some(format!("sudo chown -R {uid}:{gid} '{path}'"))
|
||||
Some(format!("sudo chown -R {uid}:{gid} '{path}'").into())
|
||||
}
|
||||
|
||||
/// Capabilities of the service session that owns the running Core.
|
||||
@@ -243,22 +244,24 @@ fn macos_service_install_markers() -> Vec<String> {
|
||||
format!(
|
||||
"/Library/LaunchDaemons/{}.plist",
|
||||
clash_verge_service_ipc::MACOS_SERVICE_ID
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
format!(
|
||||
"/Library/PrivilegedHelperTools/{}.bundle",
|
||||
clash_verge_service_ipc::MACOS_SERVICE_ID
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".to_owned(),
|
||||
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".into(),
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".to_owned(),
|
||||
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".into(),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_service_install_marker_exists() -> std::io::Result<bool> {
|
||||
for marker in macos_service_install_markers() {
|
||||
if path_entry_exists_without_follow(Path::new(&marker))? {
|
||||
if path_entry_exists_without_follow(Path::new(marker.as_str()))? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@@ -335,7 +338,8 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
|
||||
output.status
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim() != "not-found")
|
||||
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8_lossy(&output.stdout).trim() != "not-found")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -564,12 +568,12 @@ fn service_core_path(clash_core: &str, bin_ext: &str) -> Result<PathBuf> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn escape_osascript_double_quoted_string(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"").into()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn shell_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', r"'\''"))
|
||||
format!("'{}'", value.replace('\'', r"'\''")).into()
|
||||
}
|
||||
|
||||
fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
|
||||
@@ -593,76 +597,55 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result<std::process::ExitStatus> {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => {
|
||||
let mut command = RunasCommand::new(program);
|
||||
if hide_window {
|
||||
command.show(false);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
_ => {
|
||||
let mut command = StdCommand::new(program);
|
||||
if hide_window {
|
||||
// 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false).
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
};
|
||||
Ok(status)
|
||||
}
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
|
||||
let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?;
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
Ok(uninstall_path)
|
||||
}
|
||||
|
||||
fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> {
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?,
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
let status = run_maybe_elevated(&uninstall_path, true)?;
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = if linux_running_as_root() {
|
||||
StdCommand::new(&uninstall_path).status()?
|
||||
} else {
|
||||
let result = StdCommand::new(&elevator)
|
||||
let result = StdCommand::new(elevator.as_str())
|
||||
.arg("--disable-internal-agent")
|
||||
.arg(&uninstall_path)
|
||||
.status()?;
|
||||
@@ -687,7 +670,14 @@ fn uninstall_service() -> Result<()> {
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
|
||||
ensure_uninstall_success(&status)
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -702,12 +692,16 @@ fn linux_running_as_root() -> bool {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into();
|
||||
|
||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||
|
||||
@@ -721,7 +715,14 @@ fn uninstall_service() -> Result<()> {
|
||||
|
||||
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
||||
|
||||
ensure_uninstall_success(&status)
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_service() -> Result<()> {
|
||||
@@ -846,7 +847,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
|
||||
if response.code > 0 {
|
||||
return Ok(StageRequest::Refused {
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
message: response.message.into(),
|
||||
});
|
||||
}
|
||||
response
|
||||
@@ -882,7 +883,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
|
||||
if store.state().mode == crate::core::manager::RunningMode::NotRunning
|
||||
&& refusal.code != ServiceErrorCode::ProxyClearFailed as u16
|
||||
{
|
||||
store.observe(ServiceHealth::Unavailable(refusal.to_string()));
|
||||
store.observe(ServiceHealth::Unavailable(refusal.to_string().into()));
|
||||
}
|
||||
refusal.into()
|
||||
}
|
||||
@@ -890,7 +891,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
|
||||
/// Sidecar stays blocked until a reinstall replaces the residual helper, so ask for repair.
|
||||
pub(super) fn record_residual_service(error: &anyhow::Error) {
|
||||
if let Some(residual) = error.downcast_ref::<clash_verge_service_ipc::execution::ResidualServiceError>() {
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string()));
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string().into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,7 +905,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
let proposed_session_token = generate_service_session_token()?;
|
||||
let request = StartClashRequest {
|
||||
runtime,
|
||||
proposed_session_token: proposed_session_token.clone(),
|
||||
proposed_session_token: proposed_session_token.clone().into(),
|
||||
macos_proxy: None,
|
||||
};
|
||||
|
||||
@@ -942,8 +943,8 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
&RUN_STATE,
|
||||
ServiceStartRefusal {
|
||||
code: response.code,
|
||||
core_path: request.runtime.core_path,
|
||||
message: err_msg,
|
||||
core_path: request.runtime.core_path.into(),
|
||||
message: err_msg.into(),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -955,7 +956,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
|
||||
proof: OwnerSessionProof {
|
||||
generation: result.session.generation,
|
||||
token: proposed_session_token,
|
||||
token: proposed_session_token.into(),
|
||||
},
|
||||
supports_runtime_staging: capabilities.runtime_staging,
|
||||
supports_runtime_file_read: capabilities.runtime_file_read,
|
||||
@@ -1015,7 +1016,10 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
Ok(response.data.unwrap_or_default())
|
||||
Ok(response
|
||||
.data
|
||||
.map(|logs| logs.into_iter().map(Into::into).collect())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
|
||||
@@ -1033,7 +1037,8 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
|
||||
}
|
||||
let encoded = response.data.context("服务未返回核心日志快照")?;
|
||||
let content = decode_hex(&encoded).context("服务返回了无效的核心日志快照")?;
|
||||
Ok(String::from_utf8_lossy_owned(content))
|
||||
// from_utf8_lossy_owned exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8_lossy_owned(content).into())
|
||||
}
|
||||
|
||||
fn decode_hex(encoded: &str) -> Result<Vec<u8>> {
|
||||
@@ -1347,7 +1352,7 @@ async fn read_chunk(
|
||||
}
|
||||
Ok(Some(match response.data.context("服务未返回运行时文件")? {
|
||||
RuntimeFileOutcome::Absent => (None, 0, None),
|
||||
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex), len, mtime_ns),
|
||||
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex.into()), len, mtime_ns),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1359,11 +1364,11 @@ async fn provider_load_times() -> Result<ProviderLoadTimes> {
|
||||
let proxies = proxies.map_err(|error| anyhow!("failed to query proxy providers: {error}"))?;
|
||||
let mut loaded = ProviderLoadTimes::new();
|
||||
for (name, provider) in rules.providers {
|
||||
loaded.insert(("rule-providers", name), epoch_nanos(&provider.updated_at));
|
||||
loaded.insert(("rule-providers", name.into()), epoch_nanos(&provider.updated_at));
|
||||
}
|
||||
for (name, provider) in proxies.providers {
|
||||
loaded.insert(
|
||||
("proxy-providers", name),
|
||||
("proxy-providers", name.into()),
|
||||
provider.updated_at.as_deref().and_then(epoch_nanos),
|
||||
);
|
||||
}
|
||||
@@ -1589,7 +1594,7 @@ const fn owner_recovery_policy(reason: OwnerRecoveryReason, is_macos: bool) -> O
|
||||
fn mark_service_unavailable_after_owner_loss<E: RunStateEnv>(store: &RunStateStore<E>, reason: OwnerRecoveryReason) {
|
||||
if matches!(reason, OwnerRecoveryReason::TransportFailure) {
|
||||
store.observe(ServiceHealth::Unavailable(
|
||||
"service control IPC unavailable after sustained transport failure".to_owned(),
|
||||
"service control IPC unavailable after sustained transport failure".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1850,7 +1855,7 @@ async fn wait_for_service_ipc() -> Result<()> {
|
||||
}
|
||||
Err(ReadyWaitError::Unreachable(error)) => {
|
||||
tracing::Span::current().record("outcome", "unreachable");
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}").into()));
|
||||
Err(error).context(CONTEXT)
|
||||
}
|
||||
Err(ReadyWaitError::Rejected(error)) => {
|
||||
@@ -2665,7 +2670,7 @@ mod tests {
|
||||
let outcome = super::run_action_restoring_sidecar(&store, was_allowed, async {
|
||||
store.perform(PendingAction::Install).await?;
|
||||
// Simulate `wait_for_service_ipc` recording health before it fails.
|
||||
store.observe(ServiceHealth::Unavailable("service never answered".to_owned()));
|
||||
store.observe(ServiceHealth::Unavailable("service never answered".into()));
|
||||
bail!("service IPC did not become available")
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -8,7 +8,6 @@ use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -18,6 +17,7 @@ use std::{
|
||||
};
|
||||
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ProxyApplyStep {
|
||||
@@ -241,19 +241,20 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState
|
||||
}
|
||||
|
||||
pub(crate) struct Sysopt {
|
||||
// Owns the proxy target pair; every reader and writer goes through this lock.
|
||||
proxy_state_lock: TokioMutex<(Sysproxy, Autoproxy)>,
|
||||
update_lock: TokioMutex<()>,
|
||||
guard_operation_lock: TokioMutex<()>,
|
||||
reset_sysproxy: AtomicBool,
|
||||
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
|
||||
guard: Arc<RwLock<GuardMonitor>>,
|
||||
}
|
||||
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())),
|
||||
update_lock: TokioMutex::new(()),
|
||||
guard_operation_lock: TokioMutex::new(()),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
|
||||
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
|
||||
}
|
||||
}
|
||||
@@ -268,9 +269,10 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
static BYPASS_SEPARATOR: &str = ",";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS;
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.into()
|
||||
} else if use_default {
|
||||
@@ -359,7 +361,7 @@ impl Sysopt {
|
||||
}
|
||||
}
|
||||
while self.access_guard().read().get_state().is_pendding() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
// `start()` false may also mean a guard was already running.
|
||||
!self.access_guard().read().get_state().is_stopped()
|
||||
@@ -407,21 +409,18 @@ impl Sysopt {
|
||||
/// configuration instead of a partially-applied one (e.g. SOCKS already
|
||||
/// disabled but HTTP still enabled mid-transition).
|
||||
pub(crate) async fn wait_idle(&self) {
|
||||
let _ = self.proxy_state_lock.lock().await;
|
||||
let _ = self.update_lock.lock().await;
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the proxy state lock must stay held until the OS write finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn update_sysproxy(&self) -> Result<()> {
|
||||
let mut proxy_state = self.proxy_state_lock.lock().await;
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
// Configured, not live: this runs while the Core is being started or restarted, and
|
||||
// asking a Core that is not up yet would only fall back here anyway.
|
||||
let port = MixedPort::desired().await;
|
||||
let pac_port = server::embedded_server_port()?;
|
||||
// 先 await, 避免持有锁导致的 Send 问题
|
||||
let bypass = get_bypass().await;
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
|
||||
@@ -432,7 +431,7 @@ impl Sysopt {
|
||||
);
|
||||
|
||||
let (sys, auto, guard_type) = {
|
||||
let (sys, auto) = &mut *proxy_state;
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
sys.host = proxy_host.into();
|
||||
sys.port = port;
|
||||
sys.bypass = bypass.into();
|
||||
@@ -529,10 +528,6 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the proxy state lock must stay held until the disable finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn reset_sysproxy(&self) -> Result<()> {
|
||||
if self
|
||||
.reset_sysproxy
|
||||
@@ -544,13 +539,13 @@ impl Sysopt {
|
||||
defer! {
|
||||
self.reset_sysproxy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
let mut proxy_state = self.proxy_state_lock.lock().await;
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let _guard_operation = self.guard_operation_lock.lock().await;
|
||||
let drained = self.stop_proxy_guard_locked().await;
|
||||
|
||||
// 直接关闭所有代理
|
||||
let (sys, auto) = {
|
||||
let (sys, auto) = &mut *proxy_state;
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
sys.enable = false;
|
||||
auto.enable = false;
|
||||
(sys.clone(), auto.clone())
|
||||
@@ -682,8 +677,8 @@ mod tests {
|
||||
(calls.into_inner(), result.is_ok())
|
||||
}
|
||||
|
||||
fn failure_text(result: anyhow::Result<()>) -> std::string::String {
|
||||
result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned())
|
||||
fn failure_text(result: anyhow::Result<()>) -> String {
|
||||
result.map_or_else(|error| format!("{error:#}").into(), |()| String::from("no failure"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
@@ -23,6 +22,7 @@ use tokio::{
|
||||
};
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::{DelayQueue, delay_queue::Key};
|
||||
use tristr::alias::String;
|
||||
|
||||
enum TimerCommand {
|
||||
Apply(HashMap<String, TaskSchedule>),
|
||||
@@ -502,7 +502,6 @@ mod tests {
|
||||
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
|
||||
use crate::config::{PrfItem, PrfOption};
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -515,6 +514,7 @@ mod tests {
|
||||
use tokio::sync::Barrier;
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::DelayQueue;
|
||||
use tristr::alias::String;
|
||||
|
||||
struct CancellationProbe {
|
||||
cancelled: Arc<AtomicBool>,
|
||||
|
||||
@@ -19,7 +19,6 @@ use tokio::fs;
|
||||
|
||||
use super::handle;
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
@@ -27,6 +26,7 @@ use tauri::{
|
||||
AppHandle, Wry,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
mod menu_def;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -636,8 +636,7 @@ async fn create_tray_menu(
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let include_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_updater::{Update, UpdaterExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub struct SilentUpdater {
|
||||
update_ready: AtomicBool,
|
||||
@@ -55,8 +56,8 @@ impl SilentUpdater {
|
||||
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
|
||||
|
||||
let meta = UpdateCacheMeta {
|
||||
version: version.to_string(),
|
||||
downloaded_at: Utc::now().to_rfc3339(),
|
||||
version: version.into(),
|
||||
downloaded_at: Utc::now().to_rfc3339().into(),
|
||||
};
|
||||
let meta_path = cache_dir.join("pending_update.json");
|
||||
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
|
||||
@@ -133,9 +134,8 @@ fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str
|
||||
use base64::Engine as _;
|
||||
|
||||
let decode = |value: &str| -> Result<String> {
|
||||
Ok(String::from_utf8(
|
||||
base64::engine::general_purpose::STANDARD.decode(value)?,
|
||||
)?)
|
||||
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8(base64::engine::general_purpose::STANDARD.decode(value)?)?.into())
|
||||
};
|
||||
let config: tauri_plugin_updater::Config = serde_json::from_value(
|
||||
app_handle
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
fmt,
|
||||
hash::{DefaultHasher, Hash as _, Hasher as _},
|
||||
@@ -10,6 +9,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::core::handle;
|
||||
@@ -114,14 +114,8 @@ impl fmt::Display for ValidationSkipReason {
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ValidationOutcome {
|
||||
Valid,
|
||||
Invalid {
|
||||
kind: ValidationErrorKind,
|
||||
#[specta(type = std::string::String)]
|
||||
message: String,
|
||||
},
|
||||
Skipped {
|
||||
reason: ValidationSkipReason,
|
||||
},
|
||||
Invalid { kind: ValidationErrorKind, message: String },
|
||||
Skipped { reason: ValidationSkipReason },
|
||||
Busy,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::{core::service, utils::dirs};
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{Result, bail};
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub fn invoke_uwptools() -> Result<()> {
|
||||
let resource_dir = dirs::app_resources_dir()?;
|
||||
@@ -11,7 +14,13 @@ pub fn invoke_uwptools() -> Result<()> {
|
||||
bail!("enableLoopback exe not found");
|
||||
}
|
||||
|
||||
service::run_maybe_elevated(&tool_path, false)?;
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
|
||||
match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
||||
_ => StdCommand::new(tool_path).status()?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (config.mode === 'script') {
|
||||
config.mode = 'rule'
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (Array.isArray(config.proxies)) {
|
||||
config.proxies.forEach((p, i) => {
|
||||
if (p.type === 'hysteria' && typeof p.alpn === 'string') {
|
||||
config.proxies[i].alpn = [p.alpn]
|
||||
}
|
||||
})
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -4,8 +4,8 @@ use crate::{
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainItem {
|
||||
@@ -22,6 +22,12 @@ pub enum ChainType {
|
||||
Groups(SeqMap),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChainSupport {
|
||||
ClashMeta,
|
||||
ClashMetaAlpha,
|
||||
}
|
||||
|
||||
// Helper trait to allow async conversion
|
||||
pub trait AsyncChainItemFrom {
|
||||
async fn from_async(item: &PrfItem) -> Option<ChainItem>;
|
||||
@@ -47,16 +53,70 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
uid,
|
||||
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
|
||||
}),
|
||||
"rules" | "proxies" | "groups" => {
|
||||
"rules" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
let data = match itype {
|
||||
"rules" => ChainType::Rules(seq_map),
|
||||
"proxies" => ChainType::Proxies(seq_map),
|
||||
_ => ChainType::Groups(seq_map),
|
||||
};
|
||||
Some(ChainItem { uid, data })
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Rules(seq_map),
|
||||
})
|
||||
}
|
||||
"proxies" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Proxies(seq_map),
|
||||
})
|
||||
}
|
||||
"groups" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Groups(seq_map),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, Self)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
// meta 的一些处理
|
||||
let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
(ChainSupport::ClashMeta, meta_guard),
|
||||
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
|
||||
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
||||
Self {
|
||||
uid: uid.into(),
|
||||
data: ChainType::Script(data.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainSupport {
|
||||
pub fn is_support(&self, core: Option<&String>) -> bool {
|
||||
match core {
|
||||
Some(core) => matches!(
|
||||
(self, core.as_str()),
|
||||
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 12] = [
|
||||
"mode",
|
||||
@@ -27,7 +27,7 @@ fn lowercase_key(key: &str) -> Value {
|
||||
pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config.iter() {
|
||||
for (key, value) in config.into_iter() {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value.clone());
|
||||
}
|
||||
@@ -35,6 +35,17 @@ pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_lowercase_owned(config: Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value);
|
||||
}
|
||||
}
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_sort(mut config: Mapping) -> Mapping {
|
||||
let mut sorted = Mapping::new();
|
||||
HANDLE_FIELDS.into_iter().for_each(|key| {
|
||||
|
||||
+128
-65
@@ -8,7 +8,7 @@ mod tun;
|
||||
|
||||
use self::{
|
||||
chain::{AsyncChainItemFrom as _, ChainItem, ChainType},
|
||||
field::{use_keys, use_lowercase, use_sort},
|
||||
field::{use_keys, use_lowercase_owned, use_sort},
|
||||
merge::use_merge,
|
||||
script::use_script,
|
||||
seq::{SeqMap, use_seq},
|
||||
@@ -26,14 +26,15 @@ use anyhow::{Context as _, Result};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
#[derive(Debug)]
|
||||
struct ConfigValues {
|
||||
clash_config: Mapping,
|
||||
clash_core: Option<String>,
|
||||
enable_tun: bool,
|
||||
enable_builtin: bool,
|
||||
socks_enabled: bool,
|
||||
@@ -127,7 +128,8 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
|
||||
let dns_settings = verge_arc.dns_settings_for(profile_uid);
|
||||
let dns_override_confirmation = dns_settings.confirmation;
|
||||
|
||||
let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
|
||||
Some(verge_arc.get_valid_clash_core()),
|
||||
enable_tun_mode.unwrap_or(false),
|
||||
enable_builtin_enhanced.unwrap_or(true),
|
||||
verge_socks_enabled.unwrap_or(false),
|
||||
@@ -146,6 +148,7 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
|
||||
|
||||
ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
@@ -196,25 +199,35 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
|
||||
let name = current_item.name.clone().unwrap_or_default();
|
||||
|
||||
let ProfileItems {
|
||||
merge_item: default_merge,
|
||||
script_item: default_script,
|
||||
rules_item: default_rules,
|
||||
proxies_item: default_proxies,
|
||||
groups_item: default_groups,
|
||||
global_merge: default_global_merge,
|
||||
global_script: default_global_script,
|
||||
..
|
||||
} = ProfileItems::default();
|
||||
|
||||
let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!(
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,),
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
);
|
||||
|
||||
Ok(ProfileItems {
|
||||
@@ -230,16 +243,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_override_items(
|
||||
async fn process_global_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
@@ -249,11 +262,11 @@ async fn process_override_items(
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
result_map.entry(global_merge.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
@@ -264,7 +277,7 @@ async fn process_override_items(
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
result_map.insert(global_script.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
@@ -531,6 +544,46 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
async fn process_profile_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
let notes: ResultLog = authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
config = res_config;
|
||||
logs.extend(
|
||||
authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
/// Merges the app's clash config over the profile; switches are parameters to keep it pure.
|
||||
fn merge_default_config(
|
||||
mut config: Mapping,
|
||||
@@ -602,12 +655,25 @@ fn merge_default_config(
|
||||
config
|
||||
}
|
||||
|
||||
fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping {
|
||||
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping {
|
||||
if enable_builtin {
|
||||
// The JS path saw a lowercased view; lowercase once, only when the builtin runs.
|
||||
config = use_lowercase(&config);
|
||||
logging!(debug, Type::Core, "run builtin script verge_hy_alpn");
|
||||
config = builtin_hy_alpn(config);
|
||||
let items: Vec<_> = ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.collect();
|
||||
if !items.is_empty() {
|
||||
// The JS path saw a lowercased view; lowercase once, only when a builtin runs.
|
||||
config = use_lowercase_owned(config);
|
||||
for item in items {
|
||||
logging!(debug, Type::Core, "run builtin script {}", item.uid);
|
||||
config = match item.uid.as_str() {
|
||||
"verge_hy_alpn" => builtin_hy_alpn(config),
|
||||
"verge_meta_guard" => builtin_meta_guard(config),
|
||||
_ => config,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
@@ -627,6 +693,14 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
fn builtin_meta_guard(mut config: Mapping) -> Mapping {
|
||||
if config.get("mode").and_then(Value::as_str) == Some("script") {
|
||||
config.insert(Value::from("mode"), Value::from("rule"));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
|
||||
// built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in
|
||||
const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"];
|
||||
@@ -805,6 +879,7 @@ pub async fn enhance(
|
||||
let cfg_vals = get_config_values(profile_uid).await;
|
||||
let ConfigValues {
|
||||
clash_config,
|
||||
clash_core,
|
||||
enable_tun,
|
||||
enable_builtin,
|
||||
socks_enabled,
|
||||
@@ -854,13 +929,13 @@ pub async fn enhance(
|
||||
tproxy_enabled,
|
||||
);
|
||||
|
||||
let config = apply_builtin_scripts(config, enable_builtin);
|
||||
let config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
||||
let config = use_tun(config, enable_tun);
|
||||
let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await;
|
||||
|
||||
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings);
|
||||
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -871,7 +946,7 @@ pub async fn enhance(
|
||||
)
|
||||
.await;
|
||||
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
let (config, exists_keys, result_map) = process_profile_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -1353,7 +1428,7 @@ mod authoritative_field_tests {
|
||||
mod tests {
|
||||
use super::{
|
||||
AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address,
|
||||
process_override_items, use_keys,
|
||||
process_global_items, process_profile_items, use_keys,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1401,28 +1476,22 @@ mod tests {
|
||||
data: ChainType::Merge(merge.clone()),
|
||||
};
|
||||
let profile_name = "test-profile".into();
|
||||
let (config, keys, logs) = process_override_items(
|
||||
let (config, keys, logs) = process_global_items(
|
||||
config,
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
merge_item("global-merge"),
|
||||
ChainItem {
|
||||
uid: "global-script".into(),
|
||||
data: ChainType::Script(script.into()),
|
||||
},
|
||||
ChainItem::to_script("global-script", script),
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, _, logs) = process_override_items(
|
||||
let (config, _, logs) = process_profile_items(
|
||||
config,
|
||||
keys,
|
||||
logs,
|
||||
merge_item("profile-merge"),
|
||||
ChainItem {
|
||||
uid: "profile-script".into(),
|
||||
data: ChainType::Script(script.into()),
|
||||
},
|
||||
ChainItem::to_script("profile-script", script),
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
@@ -1467,19 +1536,16 @@ mod tests {
|
||||
nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}",
|
||||
)),
|
||||
};
|
||||
let global_script = ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(
|
||||
r#"function main(config) {
|
||||
let global_script = ChainItem::to_script(
|
||||
"Script",
|
||||
r#"function main(config) {
|
||||
config["global-script-wins"] = "global-script";
|
||||
config["profile-merge-wins"] = "global-script";
|
||||
config["profile-script-wins"] = "global-script";
|
||||
config.nested.winner = "global-script";
|
||||
return config;
|
||||
}"#
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
}"#,
|
||||
);
|
||||
let profile_merge = ChainItem {
|
||||
uid: "profile-merge".into(),
|
||||
data: ChainType::Merge(mapping(
|
||||
@@ -1487,21 +1553,18 @@ mod tests {
|
||||
nested: {winner: profile-merge}}",
|
||||
)),
|
||||
};
|
||||
let profile_script = ChainItem {
|
||||
uid: "profile-script".into(),
|
||||
data: ChainType::Script(
|
||||
r#"function main(config) {
|
||||
let profile_script = ChainItem::to_script(
|
||||
"profile-script",
|
||||
r#"function main(config) {
|
||||
config["profile-script-wins"] = "profile-script";
|
||||
config.nested.winner = "profile-script";
|
||||
return config;
|
||||
}"#
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
}"#,
|
||||
);
|
||||
|
||||
let profile_name = "test-profile".into();
|
||||
let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new());
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
config,
|
||||
exists_keys,
|
||||
HashMap::new(),
|
||||
@@ -1511,7 +1574,7 @@ mod tests {
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, exists_keys, _) = process_override_items(
|
||||
let (config, exists_keys, _) = process_profile_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{process::AsyncHandler, utils::tmpl};
|
||||
|
||||
use super::field::use_lowercase;
|
||||
use super::field::{use_lowercase, use_lowercase_owned};
|
||||
use anyhow::{Error, Result};
|
||||
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::Arc;
|
||||
use tristr::alias::String;
|
||||
|
||||
const MAX_OUTPUTS: usize = 1000;
|
||||
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
|
||||
@@ -16,13 +16,13 @@ const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
|
||||
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
|
||||
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
|
||||
type EvalOutcome = (Result<Mapping, String>, Vec<(String, String)>);
|
||||
|
||||
/// Never fails: exceptions are logged and the input config is returned unchanged.
|
||||
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
|
||||
// The template is identity-plus-lowercasing; skip the Boa round-trip.
|
||||
if script == tmpl::ITEM_SCRIPT {
|
||||
return (use_lowercase(&config), vec![], vec![]);
|
||||
return (use_lowercase_owned(config), vec![], vec![]);
|
||||
}
|
||||
|
||||
let json = match serde_json::to_string(&use_lowercase(&config)) {
|
||||
@@ -37,9 +37,9 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
|
||||
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
|
||||
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
|
||||
Ok(Ok(evaluated)) => evaluated,
|
||||
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
|
||||
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}").into()), vec![]),
|
||||
Err(_elapsed) => (
|
||||
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
|
||||
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}").into()),
|
||||
vec![],
|
||||
),
|
||||
};
|
||||
@@ -59,7 +59,7 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
|
||||
(result, changed_keys, logs)
|
||||
}
|
||||
Err(reason) => {
|
||||
logs.push(("exception".into(), reason.into()));
|
||||
logs.push(("exception".into(), reason));
|
||||
(config, vec![], logs)
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
let outputs_clone = Arc::clone(&outputs);
|
||||
let total_size_clone = Arc::clone(&total_size);
|
||||
|
||||
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
|
||||
let bail = |reason: String| (Err(reason), outputs.lock().to_vec());
|
||||
|
||||
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
|
||||
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
||||
@@ -137,13 +137,13 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
JsValue::from(JsString::from(config_json)),
|
||||
Attribute::all(),
|
||||
) {
|
||||
return bail(format!("failed to bind config for script: {err}"));
|
||||
return bail(format!("failed to bind config for script: {err}").into());
|
||||
}
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
let safe_name = escape_js_string_for_single_quote(name);
|
||||
if safe_name.len() > 1024 {
|
||||
return bail("Name parameter too long".to_owned());
|
||||
return bail("Name parameter too long".into());
|
||||
}
|
||||
|
||||
let code = format!(
|
||||
@@ -157,30 +157,30 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
|
||||
let result = match context.eval(Source::from_bytes(code.as_str())) {
|
||||
Ok(result) if result.is_string() => result,
|
||||
_ => return bail("main function should return object".to_owned()),
|
||||
_ => return bail("main function should return object".into()),
|
||||
};
|
||||
let result = match result.to_string(&mut context) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
|
||||
Err(e) => return bail(format!("Failed to convert JS result to string: {e}").into()),
|
||||
};
|
||||
let result = match result.to_std_string() {
|
||||
Ok(result) => result,
|
||||
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
|
||||
Err(_) => return bail("Failed to convert JS string to std string".into()),
|
||||
};
|
||||
|
||||
if result.len() > MAX_JSON_SIZE {
|
||||
return bail("Script result exceeds maximum allowed size".to_owned());
|
||||
return bail("Script result exceeds maximum allowed size".into());
|
||||
}
|
||||
|
||||
match parse_json_safely(&result) {
|
||||
Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()),
|
||||
Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()),
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Script execution error: {err:#}. Script name: {name}"
|
||||
);
|
||||
bail("Script execution failed".to_owned())
|
||||
bail("Script execution failed".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Sequence, Value};
|
||||
use std::collections::HashSet;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SeqMap {
|
||||
@@ -12,8 +13,8 @@ pub struct SeqMap {
|
||||
fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
|
||||
seq.iter()
|
||||
.filter_map(|item| match item {
|
||||
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(str::to_owned),
|
||||
Value::String(name) => Some(name.to_owned()),
|
||||
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(Into::into),
|
||||
Value::String(name) => Some(name.to_owned().into()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
@@ -58,10 +59,10 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
if let Value::String(s) = item {
|
||||
!delete.contains(s)
|
||||
!delete.iter().any(|deleted| deleted == s)
|
||||
} else if let Value::Mapping(m) = item {
|
||||
if let Some(Value::String(name)) = m.get("name") {
|
||||
!delete.contains(name)
|
||||
!delete.iter().any(|deleted| deleted == name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -99,7 +100,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
if let Value::String(name) = p {
|
||||
!delete.contains(name)
|
||||
!delete.iter().any(|deleted| deleted == name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -119,12 +120,12 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
let mut seen_proxy_names = HashSet::new();
|
||||
for name in &added_proxy_names {
|
||||
if seen_proxy_names.insert(name.clone()) {
|
||||
merged_proxies.push(Value::String(name.clone()));
|
||||
merged_proxies.push(Value::String(name.clone().into()));
|
||||
}
|
||||
}
|
||||
for value in base_group_proxies {
|
||||
if let Value::String(name) = &value
|
||||
&& !seen_proxy_names.insert(name.to_owned())
|
||||
&& !seen_proxy_names.insert(name.to_owned().into())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -180,7 +181,7 @@ proxy-groups:
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
delete: vec!["proxy1".into()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
@@ -317,7 +318,7 @@ proxy-groups: "invalid"
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
delete: vec!["proxy1".into()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
if current_mode == "fake-ip" {
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
|
||||
@@ -12,17 +12,14 @@ use chrono::Utc;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Serialize, specta::Type)]
|
||||
pub struct LocalBackupFile {
|
||||
#[specta(type = std::string::String)]
|
||||
pub filename: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub path: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub last_modified: String,
|
||||
pub content_length: u64,
|
||||
}
|
||||
@@ -33,9 +30,6 @@ async fn finalize_restored_verge_config(
|
||||
webdav_username: Option<String>,
|
||||
webdav_password: Option<String>,
|
||||
) -> Result<()> {
|
||||
// The lock must cover every restored layer; an unlocked apply races a concurrently staged draft.
|
||||
let config_write = Config::lock_config_write().await;
|
||||
|
||||
// A broken restored `verge.yaml` is a restore failure, not a reason to load defaults.
|
||||
let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?;
|
||||
restored.webdav_url = webdav_url;
|
||||
@@ -46,42 +40,43 @@ async fn finalize_restored_verge_config(
|
||||
let restored_clash = IClashTemp::new().await;
|
||||
let clash_draft = Config::clash().await;
|
||||
clash_draft.edit_draft(|d| {
|
||||
*d = restored_clash;
|
||||
*d = restored_clash.clone();
|
||||
});
|
||||
clash_draft.apply();
|
||||
|
||||
let restored_profiles = IProfiles::new().await;
|
||||
let profiles_draft = Config::profiles().await;
|
||||
profiles_draft.edit_draft(|d| {
|
||||
*d = restored_profiles;
|
||||
*d = restored_profiles.clone();
|
||||
});
|
||||
profiles_draft.apply();
|
||||
|
||||
let verge_draft = Config::verge().await;
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
{
|
||||
// Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads.
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `patch_verge` re-acquires the lock through `try_lock_config_write`.
|
||||
drop(config_write);
|
||||
|
||||
// Run configuration side effects without rewriting the already-restored file.
|
||||
if let Err(err) = super::patch_verge(&restored, true).await {
|
||||
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
|
||||
@@ -136,35 +131,14 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Backup creation strips the WebDAV credentials, so the live ones are kept.
|
||||
async fn restore_backup_archive(path: PathBuf) -> Result<()> {
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let home_dir = app_home_dir()?;
|
||||
AsyncHandler::spawn_blocking(move || -> Result<()> {
|
||||
let file = std::fs::File::open(&path)?;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
zip.extract(&home_dir)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|join| anyhow!("backup extraction task failed: {join}"))??;
|
||||
|
||||
// Held for the config-data transition only; extraction is plain filesystem work.
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = verge.latest_arc();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
||||
.join(filename.as_str());
|
||||
@@ -181,7 +155,12 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
err
|
||||
})?;
|
||||
|
||||
let res = restore_backup_archive(backup_storage_path.clone()).await;
|
||||
let value = backup_storage_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
|
||||
let _ = backup_storage_path.remove_if_exists().await;
|
||||
res
|
||||
}
|
||||
@@ -350,7 +329,22 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
return Err(anyhow!("Backup file not found: {}", filename));
|
||||
}
|
||||
|
||||
restore_backup_archive(target_path).await
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
|
||||
|
||||
+46
-13
@@ -3,19 +3,26 @@ use crate::core::notify::{Refresh, announce};
|
||||
use crate::{
|
||||
config::{Config, MixedPort},
|
||||
core::{CoreManager, handle, tray},
|
||||
feat::run_exit_cleanup_flow,
|
||||
feat::clean_async,
|
||||
process::AsyncHandler,
|
||||
utils,
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
|
||||
LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Failed to set TLS versions")
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
Arc::new(config)
|
||||
});
|
||||
|
||||
pub async fn restart_clash_core() {
|
||||
match CoreManager::global().restart_core().await {
|
||||
@@ -31,9 +38,38 @@ pub async fn restart_clash_core() {
|
||||
|
||||
pub async fn restart_app() {
|
||||
logging!(debug, Type::System, "启动重启应用流程");
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result.all_success { 0 } else { 1 }
|
||||
);
|
||||
|
||||
if !cleanup_result.core_stopped {
|
||||
handle::Handle::global().clear_is_exiting();
|
||||
// A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore.
|
||||
// A core that kept running keeps its DNS, whatever an unapplied config draft says.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let manager = CoreManager::global();
|
||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||
if matches!(
|
||||
*manager.get_running_mode(),
|
||||
crate::core::manager::RunningMode::NotRunning
|
||||
) {
|
||||
crate::utils::resolve::dns::sync_public_dns().await;
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::AppRestartCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,14 +103,11 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let clash = Config::clash().await;
|
||||
// Serialize with patch transactions: an unlocked commit can flush a staged draft mid-rollback.
|
||||
let save_result = {
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
clash.data_arc().save_config().await
|
||||
};
|
||||
if save_result.is_ok() {
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
|
||||
let clash_data = clash.data_arc();
|
||||
if clash_data.save_config().await.is_ok() {
|
||||
announce(Refresh::Clash);
|
||||
tray::Tray::global().update_menu_and_icon().await;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::{
|
||||
process::Command,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
const RELEASE_VERSION_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const ALPHA_BASE_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha";
|
||||
@@ -50,7 +51,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
|
||||
// A core already broken by the in-place updater cannot report a version; upgrading is the repair.
|
||||
let installed = read_core_version(&target).unwrap_or_else(|error| {
|
||||
logging!(warn, Type::Core, "core upgrade: unreadable installed core: {error:#}");
|
||||
std::string::String::new()
|
||||
String::new()
|
||||
});
|
||||
|
||||
let (proxy, latest) = resolve_latest_version(alpha).await?;
|
||||
@@ -172,14 +173,15 @@ fn managed_core_path(core: &str) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
/// Pins the package to the resolved version, so a release moving on mid-upgrade cannot 404.
|
||||
fn package_url(alpha: bool, version: &str) -> Result<std::string::String> {
|
||||
fn package_url(alpha: bool, version: &str) -> Result<String> {
|
||||
let asset = asset_base_name(alpha)?;
|
||||
let extension = if cfg!(windows) { "zip" } else { "gz" };
|
||||
Ok(if alpha {
|
||||
format!("{ALPHA_BASE_URL}/{asset}-{version}.{extension}")
|
||||
} else {
|
||||
format!("{RELEASE_DOWNLOAD_URL}/{version}/{asset}-{version}.{extension}")
|
||||
})
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Mirrors the asset map in `scripts/prebuild.mjs` so an upgrade keeps the build variant
|
||||
@@ -218,11 +220,11 @@ fn asset_base_name(alpha: bool) -> Result<&'static str> {
|
||||
|
||||
/// Returns the proxy that reached GitHub so the package download reuses it.
|
||||
#[tracing::instrument(skip_all, level = "debug", fields(alpha))]
|
||||
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::String)> {
|
||||
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
|
||||
let url = if alpha {
|
||||
format!("{ALPHA_BASE_URL}/version.txt")
|
||||
String::from(format!("{ALPHA_BASE_URL}/version.txt"))
|
||||
} else {
|
||||
RELEASE_VERSION_URL.to_owned()
|
||||
String::from(RELEASE_VERSION_URL)
|
||||
};
|
||||
let mut last_error = None;
|
||||
|
||||
@@ -234,7 +236,7 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::
|
||||
match attempt {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
// An error page answered with 200 must not become a version, nor reach the URL.
|
||||
let version = response.text().trim().to_owned();
|
||||
let version = String::from(response.text().trim());
|
||||
if !is_usable_version(&version) {
|
||||
logging!(
|
||||
warn,
|
||||
@@ -446,7 +448,7 @@ fn new_command(program: impl AsRef<OsStr>) -> Command {
|
||||
}
|
||||
|
||||
/// Parses the version out of `Mihomo Meta v1.19.30 darwin arm64 with go1.22.12 ...`.
|
||||
fn read_core_version(path: &Path) -> Result<std::string::String> {
|
||||
fn read_core_version(path: &Path) -> Result<String> {
|
||||
let output = new_command(path)
|
||||
.arg("-v")
|
||||
.output()
|
||||
@@ -456,11 +458,12 @@ fn read_core_version(path: &Path) -> Result<std::string::String> {
|
||||
bail!("{} -v exited with {}", path.display(), output.status);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
|
||||
stdout
|
||||
.split_whitespace()
|
||||
.nth(2)
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.with_context(|| format!("unexpected version output from {}: {stdout}", path.display()))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::core::notify::{Refresh, announce};
|
||||
use anyhow::{Result, ensure};
|
||||
use clash_verge_draft::DraftTransaction;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
@@ -19,10 +19,7 @@ use crate::{
|
||||
#[serde(tag = "status", rename_all = "snake_case")]
|
||||
pub enum DnsOverrideOutcome {
|
||||
Applied,
|
||||
ConfirmationRequired {
|
||||
#[specta(type = std::string::String)]
|
||||
source: String,
|
||||
},
|
||||
ConfirmationRequired { source: String },
|
||||
}
|
||||
|
||||
pub async fn set_dns_override(
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
use crate::{
|
||||
cmd::{CmdResult, StringifyErr as _},
|
||||
cmd::{CmdResult, CommandFailure, StringifyErr as _},
|
||||
utils::dirs::{self, PathBufExec as _},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct IconInfo {
|
||||
#[specta(type = std::string::String)]
|
||||
name: String,
|
||||
#[specta(type = std::string::String)]
|
||||
previous_t: String,
|
||||
#[specta(type = std::string::String)]
|
||||
current_t: String,
|
||||
}
|
||||
|
||||
@@ -43,7 +40,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
|
||||
Ok(icon_path)
|
||||
}
|
||||
|
||||
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
|
||||
fn normalized_text_prefix(content: &[u8]) -> String {
|
||||
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
|
||||
let start = content
|
||||
.iter()
|
||||
@@ -51,7 +48,8 @@ fn normalized_text_prefix(content: &[u8]) -> std::string::String {
|
||||
.unwrap_or(content.len());
|
||||
let end = content.len().min(start.saturating_add(2048));
|
||||
let prefix = &content[start..end];
|
||||
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase()
|
||||
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase().into()
|
||||
}
|
||||
|
||||
fn looks_like_html(content: &[u8]) -> bool {
|
||||
@@ -95,7 +93,10 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
if !is_supported_icon_content(&content) {
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
return Err(format!("Downloaded content is not a valid image: {}", url.as_str()).into());
|
||||
return Err(CommandFailure::plain(format!(
|
||||
"Downloaded content is not a valid image: {}",
|
||||
url.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -164,7 +165,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
|
||||
|
||||
match fs::copy(file_path, &dest_path).await {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().into()),
|
||||
Err(err) => Err(err.to_string().into()),
|
||||
Err(err) => Err(CommandFailure::plain(err)),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".into())
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
@@ -47,11 +47,26 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
announce(Refresh::Proxies);
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "切换代理失败: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
match handle::Handle::mihomo()
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "代理切换最终失败: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
@@ -216,7 +231,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
|
||||
};
|
||||
let message = mask_err(&message);
|
||||
let message = if profile_persisted {
|
||||
format!("订阅已保存,但未能确认已应用到内核: {message}")
|
||||
format!("订阅已保存,但未能确认已应用到内核: {message}").into()
|
||||
} else {
|
||||
message
|
||||
};
|
||||
|
||||
@@ -130,7 +130,9 @@ pub async fn open_or_close_dashboard() {
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult {
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
// 设置退出标志
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
@@ -160,18 +162,11 @@ pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> C
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
stop_failed_notice,
|
||||
cleanup_result.stop_error.clone().unwrap_or_default(),
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
cleanup_result
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await;
|
||||
|
||||
utils::server::shutdown_embedded_server();
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::cmd;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
tauri_specta::Builder::new()
|
||||
@@ -121,7 +122,7 @@ pub struct WebDavFile {
|
||||
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
|
||||
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
|
||||
Self {
|
||||
href: file.href,
|
||||
href: file.href.into(),
|
||||
last_modified: file.last_modified,
|
||||
}
|
||||
}
|
||||
@@ -148,20 +149,20 @@ pub struct IpAddress {
|
||||
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
|
||||
fn from(interface: network_interface::NetworkInterface) -> Self {
|
||||
Self {
|
||||
name: interface.name,
|
||||
name: interface.name.into(),
|
||||
addr: interface
|
||||
.addr
|
||||
.into_iter()
|
||||
.map(|address| match address {
|
||||
network_interface::Addr::V4(address) => InterfaceAddress::V4(IpAddress {
|
||||
ip: address.ip.to_string(),
|
||||
ip: address.ip.to_string().into(),
|
||||
}),
|
||||
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
|
||||
ip: address.ip.to_string(),
|
||||
ip: address.ip.to_string().into(),
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
mac_addr: interface.mac_addr,
|
||||
mac_addr: interface.mac_addr.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-14
@@ -11,6 +11,7 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub async fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
|
||||
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
|
||||
@@ -34,12 +35,13 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
|
||||
let bytes = tokio::fs::read(path)
|
||||
.await
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
let yaml_str =
|
||||
String::from_utf8(bytes).with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
let yaml_str = std::string::String::from_utf8(bytes)
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
let label = path.to_path_buf();
|
||||
|
||||
// Parsing a multi-megabyte profile is pure CPU; keep it off the async worker.
|
||||
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str, label))
|
||||
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str.into(), label))
|
||||
.await
|
||||
.map_err(|join| anyhow!("yaml parse task failed: {join}"))?
|
||||
}
|
||||
@@ -139,7 +141,7 @@ const ALPHABET: [char; 62] = [
|
||||
|
||||
pub fn get_uid(prefix: &str) -> String {
|
||||
let id = nanoid!(11, &ALPHABET);
|
||||
format!("{prefix}{id}")
|
||||
format!("{prefix}{id}").into()
|
||||
}
|
||||
|
||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
@@ -171,7 +173,7 @@ pub fn mask_url(url: &str) -> String {
|
||||
let scheme_and_host = &path_part[..host_end];
|
||||
let path = &path_part[host_end..]; // starts with '/' or empty
|
||||
|
||||
let mut result = scheme_and_host.to_owned();
|
||||
let mut result: String = scheme_and_host.into();
|
||||
|
||||
if !path.is_empty() {
|
||||
let masked: Vec<&str> = path
|
||||
@@ -186,8 +188,8 @@ pub fn mask_url(url: &str) -> String {
|
||||
let masked_query: Vec<String> = query
|
||||
.split('&')
|
||||
.map(|param| match param.find('=') {
|
||||
Some(eq) => format!("{}=***", ¶m[..eq]),
|
||||
None => param.to_owned(),
|
||||
Some(eq) => format!("{}=***", ¶m[..eq]).into(),
|
||||
None => param.into(),
|
||||
})
|
||||
.collect();
|
||||
result.push_str(&masked_query.join("&"));
|
||||
@@ -198,7 +200,8 @@ pub fn mask_url(url: &str) -> String {
|
||||
|
||||
/// Masks every HTTP(S) URL embedded in a log string while preserving surrounding text.
|
||||
pub fn mask_err(err: &str) -> String {
|
||||
let mut result = String::with_capacity(err.len());
|
||||
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
|
||||
let mut result = std::string::String::with_capacity(err.len());
|
||||
let mut remaining = err;
|
||||
|
||||
loop {
|
||||
@@ -224,7 +227,7 @@ pub fn mask_err(err: &str) -> String {
|
||||
remaining = &remaining[url_end..];
|
||||
}
|
||||
|
||||
result
|
||||
result.into()
|
||||
}
|
||||
|
||||
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
@@ -235,7 +238,8 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
Some(
|
||||
percent_encoding::percent_decode_str(last_segment)
|
||||
.decode_utf8_lossy()
|
||||
.to_string(),
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,15 +281,15 @@ pub fn linux_elevator() -> String {
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
if let Ok(path) = std::str::from_utf8(&output.stdout) {
|
||||
path.trim().to_string()
|
||||
path.trim().into()
|
||||
} else {
|
||||
"sudo".to_string()
|
||||
"sudo".into()
|
||||
}
|
||||
} else {
|
||||
"sudo".to_string()
|
||||
"sudo".into()
|
||||
}
|
||||
}
|
||||
Err(_) => "sudo".to_string(),
|
||||
Err(_) => "sudo".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -354,6 +354,7 @@ pub(super) async fn init_dns_config() -> Result<()> {
|
||||
Value::String("*.arpa".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("ntp.*.com".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("+.market.xiaomi.com".into()),
|
||||
Value::String("localhost.ptlogin2.qq.com".into()),
|
||||
Value::String("*.msftncsi.com".into()),
|
||||
@@ -508,9 +509,11 @@ pub async fn init_resources() -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir()?;
|
||||
|
||||
// app_home is created by `ensure_directories` in `init_config`, which always runs first.
|
||||
if !app_dir.exists() {
|
||||
std::mem::drop(fs::create_dir_all(&app_dir).await);
|
||||
}
|
||||
if !res_dir.exists() {
|
||||
fs::create_dir_all(&res_dir).await?;
|
||||
std::mem::drop(fs::create_dir_all(&res_dir).await);
|
||||
}
|
||||
|
||||
let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> {
|
||||
let Some(path) = mimeapps_list_path() else {
|
||||
@@ -46,19 +47,19 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
|
||||
if trimmed.eq_ignore_ascii_case("[Default Applications]") {
|
||||
default_present = true;
|
||||
current_section = Some(SectionKind::DefaultApplications);
|
||||
output_lines.push("[Default Applications]".to_string());
|
||||
output_lines.push("[Default Applications]".into());
|
||||
continue;
|
||||
} else if trimmed.eq_ignore_ascii_case("[Added Associations]") {
|
||||
current_section = Some(SectionKind::AddedAssociations);
|
||||
output_lines.push("[Added Associations]".to_string());
|
||||
output_lines.push("[Added Associations]".into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if current_section.is_some() {
|
||||
section_buffer.push(line.to_string());
|
||||
section_buffer.push(line.into());
|
||||
} else {
|
||||
output_lines.push(line.to_string());
|
||||
output_lines.push(line.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +79,9 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
|
||||
if output_lines.last().is_some_and(|line| !line.is_empty()) {
|
||||
output_lines.push(String::new());
|
||||
}
|
||||
output_lines.push("[Default Applications]".to_string());
|
||||
output_lines.push("[Default Applications]".into());
|
||||
for &scheme in schemes {
|
||||
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
|
||||
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ fn flush_section(
|
||||
.split(';')
|
||||
.filter_map(|value| {
|
||||
let trimmed = value.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
(!trimmed.is_empty()).then(|| trimmed.into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -187,7 +188,7 @@ fn flush_section(
|
||||
.split(';')
|
||||
.filter_map(|value| {
|
||||
let trimmed = value.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
(!trimmed.is_empty()).then(|| trimmed.into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -203,10 +204,10 @@ fn flush_section(
|
||||
merged_values.insert(0, moved);
|
||||
}
|
||||
} else {
|
||||
merged_values.insert(0, desktop_file.to_string());
|
||||
merged_values.insert(0, desktop_file.into());
|
||||
}
|
||||
|
||||
let mut merged_line = format!("{existing_prefix}x-scheme-handler/{scheme}=");
|
||||
let mut merged_line: String = format!("{existing_prefix}x-scheme-handler/{scheme}=").into();
|
||||
merged_line.push_str(&merged_values.join(";"));
|
||||
merged_line.push(';');
|
||||
|
||||
@@ -222,16 +223,16 @@ fn flush_section(
|
||||
if let Some(pos) = values.iter().position(|value| value == desktop_file) {
|
||||
if pos != 0 {
|
||||
values.remove(pos);
|
||||
values.insert(0, desktop_file.to_string());
|
||||
values.insert(0, desktop_file.into());
|
||||
*changed = true;
|
||||
}
|
||||
} else {
|
||||
values.insert(0, desktop_file.to_string());
|
||||
values.insert(0, desktop_file.into());
|
||||
*changed = true;
|
||||
}
|
||||
|
||||
let prefix = line.chars().take_while(|c| c.is_whitespace()).collect::<String>();
|
||||
let mut new_line = format!("{prefix}x-scheme-handler/{scheme}=");
|
||||
let mut new_line: String = format!("{prefix}x-scheme-handler/{scheme}=").into();
|
||||
new_line.push_str(&values.join(";"));
|
||||
new_line.push(';');
|
||||
|
||||
@@ -253,7 +254,7 @@ fn flush_section(
|
||||
if ensure_all {
|
||||
for &scheme in schemes {
|
||||
if !seen.contains_key(scheme) {
|
||||
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
|
||||
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
|
||||
*changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ use reqwest::{
|
||||
Client, Proxy, StatusCode,
|
||||
header::{HeaderMap, HeaderValue, USER_AGENT},
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::Url;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HttpResponse {
|
||||
@@ -48,17 +48,6 @@ enum TlsRootMode {
|
||||
StaticWebpkiRoots,
|
||||
}
|
||||
|
||||
/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer.
|
||||
pub(crate) fn build_webpki_rooted_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct NetworkManager;
|
||||
|
||||
impl NetworkManager {
|
||||
@@ -68,7 +57,7 @@ impl NetworkManager {
|
||||
|
||||
fn build_client(
|
||||
&self,
|
||||
proxy_url: Option<std::string::String>,
|
||||
proxy_url: Option<String>,
|
||||
default_headers: HeaderMap,
|
||||
accept_invalid_certs: bool,
|
||||
timeout_secs: Option<u64>,
|
||||
@@ -87,7 +76,7 @@ impl NetworkManager {
|
||||
|
||||
// 设置代理
|
||||
if let Some(proxy_str) = proxy_url {
|
||||
let proxy = Proxy::all(proxy_str)?;
|
||||
let proxy = Proxy::all(proxy_str.as_str())?;
|
||||
builder = builder.proxy(proxy);
|
||||
} else {
|
||||
builder = builder.no_proxy();
|
||||
@@ -113,7 +102,13 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let mut config = build_webpki_rooted_tls_config()?;
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let mut config =
|
||||
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
|
||||
Ok(config)
|
||||
@@ -160,23 +155,6 @@ impl NetworkManager {
|
||||
detail.contains("protocolversion") || detail.contains("protocol version")
|
||||
}
|
||||
|
||||
/// `allow_fallback` stays false when the caller already disabled certificate verification.
|
||||
async fn request_with_static_webpki_fallback<T, F, Fut>(allow_fallback: bool, mut request: F) -> Result<T>
|
||||
where
|
||||
F: FnMut(TlsRootMode) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<T>>,
|
||||
{
|
||||
match request(TlsRootMode::PlatformVerifier).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => {
|
||||
request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
})
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_with_tls_mode(
|
||||
&self,
|
||||
url: &str,
|
||||
@@ -248,15 +226,15 @@ impl NetworkManager {
|
||||
accept_invalid_certs: bool,
|
||||
tls_root_mode: TlsRootMode,
|
||||
) -> Result<Client> {
|
||||
let proxy_url: Option<std::string::String> = match proxy_type {
|
||||
let proxy_url: Option<String> = match proxy_type {
|
||||
ProxyType::None => None,
|
||||
ProxyType::Localhost => {
|
||||
let port = MixedPort::desired().await;
|
||||
Some(format!("http://127.0.0.1:{port}"))
|
||||
Some(format!("http://127.0.0.1:{port}").into())
|
||||
}
|
||||
ProxyType::System => {
|
||||
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||
Some(format!("http://{}:{}", p.host, p.port))
|
||||
Some(format!("http://{}:{}", p.host, p.port).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -286,21 +264,34 @@ impl NetworkManager {
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<HttpResponse> {
|
||||
Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| {
|
||||
let user_agent = user_agent.clone();
|
||||
async move {
|
||||
self.get_with_tls_mode(
|
||||
let platform_result = self
|
||||
.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent.clone(),
|
||||
accept_invalid_certs,
|
||||
TlsRootMode::PlatformVerifier,
|
||||
)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent,
|
||||
accept_invalid_certs,
|
||||
tls_root_mode,
|
||||
TlsRootMode::StaticWebpkiRoots,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_bytes_with_tls_mode(
|
||||
@@ -349,10 +340,19 @@ impl NetworkManager {
|
||||
timeout_secs: Option<u64>,
|
||||
max_bytes: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move {
|
||||
self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode)
|
||||
let platform_result = self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(body) => Ok(body),
|
||||
Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{
|
||||
@@ -95,6 +97,18 @@ async fn resolve_setup() {
|
||||
resolve_done();
|
||||
}
|
||||
|
||||
pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
|
||||
CoreManager::global().stop_core().await?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use dns::restore_public_dns;
|
||||
restore_public_dns().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_scheme() {
|
||||
logging_error!(Type::Setup, init::init_scheme());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use anyhow::Result;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
use tauri::Url;
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::{
|
||||
config::{Config, PrfItem, profiles},
|
||||
@@ -39,7 +39,7 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option<String>)> {
|
||||
fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String>)> {
|
||||
if !matches!(link_parsed.scheme(), "clash" | "clash-verge") {
|
||||
return None;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String,
|
||||
Some((url, name))
|
||||
}
|
||||
|
||||
fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
|
||||
fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
|
||||
let query = link_parsed.query()?;
|
||||
let prefix = "url=";
|
||||
let pos = query.find(prefix)?;
|
||||
@@ -60,15 +60,15 @@ fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
|
||||
Some(decode_subscription_url(raw_url))
|
||||
}
|
||||
|
||||
fn decode_subscription_url(raw_url: &str) -> std::string::String {
|
||||
fn decode_subscription_url(raw_url: &str) -> String {
|
||||
// Avoid double-decoding nested subscription URLs; decode only when needed.
|
||||
if Url::parse(raw_url).is_ok() {
|
||||
return raw_url.to_string();
|
||||
return String::from(raw_url);
|
||||
}
|
||||
|
||||
let mut candidate = raw_url.to_string();
|
||||
let mut candidate = String::from(raw_url);
|
||||
for _ in 0..2 {
|
||||
let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string();
|
||||
let next = String::from(percent_decode_str(&candidate).decode_utf8_lossy());
|
||||
if next == candidate {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fs;
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use tristr::alias::String;
|
||||
use windows::Win32::Globalization::{GetACP, GetOEMCP, MULTI_BYTE_TO_WIDE_CHAR_FLAGS, MultiByteToWideChar};
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
@@ -72,11 +73,11 @@ fn get_task_user_id() -> Result<String> {
|
||||
if let Some(domain) = domain {
|
||||
let domain = domain.trim();
|
||||
if !domain.is_empty() {
|
||||
return Ok(format!("{domain}\\{username}"));
|
||||
return Ok(format!("{domain}\\{username}").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(username.to_string())
|
||||
Ok(username.into())
|
||||
}
|
||||
|
||||
fn get_startup_dir() -> Result<PathBuf> {
|
||||
@@ -118,7 +119,8 @@ fn task_xml_path(mode: TaskMode) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
fn xml_escape(value: &str) -> String {
|
||||
let mut escaped = String::with_capacity(value.len());
|
||||
// Per-char pushes must stay on the std String: TriStr growth re-materializes per push.
|
||||
let mut escaped = std::string::String::with_capacity(value.len());
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'&' => escaped.push_str("&"),
|
||||
@@ -129,7 +131,7 @@ fn xml_escape(value: &str) -> String {
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
escaped.into()
|
||||
}
|
||||
|
||||
fn build_task_xml(mode: TaskMode) -> Result<String> {
|
||||
@@ -182,7 +184,8 @@ fn build_task_xml(mode: TaskMode) -> Result<String> {
|
||||
user_id,
|
||||
mode.xml_run_level(),
|
||||
exe_path
|
||||
))
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
fn encode_utf16le_with_bom(content: &str) -> Vec<u8> {
|
||||
@@ -226,12 +229,13 @@ fn decode_with_code_page(bytes: &[u8], code_page: u32) -> Option<String> {
|
||||
}
|
||||
|
||||
wide.truncate(written as usize);
|
||||
Some(String::from_utf16_lossy(&wide))
|
||||
// from_utf16_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Some(std::string::String::from_utf16_lossy(&wide).into())
|
||||
}
|
||||
|
||||
fn decode_console_output(bytes: &[u8]) -> String {
|
||||
if let Ok(text) = std::str::from_utf8(bytes) {
|
||||
return text.to_string();
|
||||
return text.into();
|
||||
}
|
||||
|
||||
let oem = unsafe { GetOEMCP() };
|
||||
@@ -244,7 +248,8 @@ fn decode_console_output(bytes: &[u8]) -> String {
|
||||
return text;
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(bytes).to_string()
|
||||
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf8_lossy(bytes).into()
|
||||
}
|
||||
|
||||
fn output_message(output: &Output) -> String {
|
||||
@@ -254,10 +259,10 @@ fn output_message(output: &Output) -> String {
|
||||
let stderr = stderr.trim();
|
||||
|
||||
match (stdout.is_empty(), stderr.is_empty()) {
|
||||
(true, true) => "unknown error".to_string(),
|
||||
(false, true) => stdout.to_string(),
|
||||
(true, false) => stderr.to_string(),
|
||||
(false, false) => format!("{stdout} | {stderr}"),
|
||||
(true, true) => "unknown error".into(),
|
||||
(false, true) => stdout.into(),
|
||||
(true, false) => stderr.into(),
|
||||
(false, false) => format!("{stdout} | {stderr}").into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
use tristr::alias::String;
|
||||
use warp::Filter as _;
|
||||
|
||||
const INSTANCE_TOKEN_HEADER: &str = "x-instance-token";
|
||||
@@ -141,7 +142,7 @@ async fn notify_existing_instance(record: &InstanceRecord) -> bool {
|
||||
let request = client.get(format!("http://127.0.0.1:{}/commands/visible", record.port));
|
||||
|
||||
request
|
||||
.header(INSTANCE_TOKEN_HEADER, &record.token)
|
||||
.header(INSTANCE_TOKEN_HEADER, record.token.as_str())
|
||||
.send()
|
||||
.await
|
||||
.is_ok_and(|response| response.status().is_success())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::Serialize;
|
||||
use serde_yaml_ng::Result;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
|
||||
let yaml = serde_yaml_ng::to_string(data)?;
|
||||
@@ -7,7 +8,8 @@ pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
|
||||
}
|
||||
|
||||
fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
|
||||
let mut result = String::with_capacity(yaml.len());
|
||||
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
|
||||
let mut result = std::string::String::with_capacity(yaml.len());
|
||||
let mut in_fake_ip_filter = false;
|
||||
let mut fake_ip_filter_indent = 0;
|
||||
|
||||
@@ -39,7 +41,7 @@ fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
result
|
||||
result.into()
|
||||
}
|
||||
|
||||
fn leading_spaces(line: &str) -> usize {
|
||||
@@ -50,12 +52,12 @@ fn quote_wildcard_item(item: &str) -> String {
|
||||
if !item.starts_with('\"') && !item.starts_with('\'') && contains_wildcard(item) {
|
||||
quote_string(item)
|
||||
} else {
|
||||
item.to_string()
|
||||
item.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_string(s: &str) -> String {
|
||||
"\'".to_string() + s.replace('\'', "''").as_str() + "\'"
|
||||
("\'".to_string() + s.replace('\'', "''").as_str() + "\'").into()
|
||||
}
|
||||
|
||||
fn contains_wildcard(value: &str) -> bool {
|
||||
|
||||
@@ -4,19 +4,10 @@ import {
|
||||
VisibilityOffOutlined,
|
||||
VisibilityOutlined,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useEffect } from 'foxact/use-abortable-effect'
|
||||
import { useIntersection } from 'foxact/use-intersection'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import type { XOR } from 'foxts/ts-xor'
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -108,13 +99,9 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
||||
export const IpInfoCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showIp, setShowIp] = useState(false)
|
||||
const [autoRefresh, setAutoRefresh] = useLocalStorage(
|
||||
'ip-info-auto-refresh',
|
||||
true,
|
||||
)
|
||||
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
|
||||
|
||||
// Once intersected, the card stays eligible for refresh until unmount.
|
||||
// Once intersected, refreshes stay enabled until unmount.
|
||||
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
||||
rootMargin: '0px',
|
||||
})
|
||||
@@ -164,8 +151,6 @@ export const IpInfoCard = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return
|
||||
|
||||
let timer: number | null = null
|
||||
|
||||
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
|
||||
@@ -205,7 +190,7 @@ export const IpInfoCard = () => {
|
||||
if (timer != null) clearInterval(timer)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
}
|
||||
}, [autoRefresh, hasIntersected])
|
||||
}, [hasIntersected])
|
||||
|
||||
const toggleShowIp = useCallback(() => {
|
||||
setShowIp((prev) => !prev)
|
||||
@@ -379,25 +364,12 @@ export const IpInfoCard = () => {
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
sx={{ m: 0 }}
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={autoRefresh}
|
||||
onChange={(_, checked) => setAutoRefresh(checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="caption">
|
||||
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||
{autoRefresh &&
|
||||
(countdown.type === 'countdown'
|
||||
? `: ${countdown.remainingSeconds}s`
|
||||
: '...')}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||
{countdown.type === 'countdown'
|
||||
? `: ${countdown.remainingSeconds}s`
|
||||
: '...'}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
|
||||
@@ -397,9 +397,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq((obj?.prepend || []).filter((rule) => rule !== null))
|
||||
setAppendSeq((obj?.append || []).filter((rule) => rule !== null))
|
||||
setDeleteSeq((obj?.delete || []).filter((rule) => rule !== null))
|
||||
setPrependSeq(obj?.prepend || [])
|
||||
setAppendSeq(obj?.append || [])
|
||||
setDeleteSeq(obj?.delete || [])
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
}, [property])
|
||||
|
||||
@@ -417,9 +417,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
startTransition(() => {
|
||||
setPrependSeq((obj?.prepend ?? []).filter((rule) => rule !== null))
|
||||
setAppendSeq((obj?.append ?? []).filter((rule) => rule !== null))
|
||||
setDeleteSeq((obj?.delete ?? []).filter((rule) => rule !== null))
|
||||
setPrependSeq(obj?.prepend ?? [])
|
||||
setAppendSeq(obj?.append ?? [])
|
||||
setDeleteSeq(obj?.delete ?? [])
|
||||
})
|
||||
setVisualization(true)
|
||||
}
|
||||
|
||||
@@ -608,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
/>
|
||||
</DragDropProvider>
|
||||
|
||||
{(mode === 'rule' || mode === 'global') && (
|
||||
{mode === 'rule' && (
|
||||
<ProxyGroupNavigator
|
||||
proxyGroupNames={proxyGroupNames}
|
||||
onGroupLocation={handleGroupLocationByName}
|
||||
|
||||
@@ -211,7 +211,7 @@ export const useRenderList = (
|
||||
if (!proxyView) return []
|
||||
if (isChainMode)
|
||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||
return mode === 'rule' || mode === 'global'
|
||||
return mode === 'rule' || mode === 'script'
|
||||
? proxyView.groups.map(({ name }) => name)
|
||||
: proxyView.global
|
||||
? [proxyView.global.name]
|
||||
@@ -257,7 +257,7 @@ export const useRenderList = (
|
||||
}))
|
||||
}
|
||||
|
||||
const useRule = mode === 'rule' || mode === 'global'
|
||||
const useRule = mode === 'rule' || mode === 'script'
|
||||
const renderGroups = useRule
|
||||
? proxyView.groups
|
||||
: proxyView.global === null
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "غير مؤكد",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Unterstützt",
|
||||
"no": "Nicht unterstützt",
|
||||
"failed": "Test fehlgeschlagen",
|
||||
"risky": "Ungewiss",
|
||||
"completed": "Prüfung abgeschlossen",
|
||||
"disallowedIsp": "Nicht zugelassener Internetdienstanbieter",
|
||||
"originalsOnly": "Nur Original",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "Risky",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Soportado",
|
||||
"no": "No soportado",
|
||||
"failed": "Prueba fallida",
|
||||
"risky": "Dudoso",
|
||||
"completed": "Detección completada",
|
||||
"disallowedIsp": "Proveedor de servicios de Internet no permitido",
|
||||
"originalsOnly": "Solo originales",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "نامشخص",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "Tidak pasti",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user