openclacky 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/lib/clacky/agent.rb +11 -0
- data/lib/clacky/client.rb +8 -4
- data/lib/clacky/default_skills/browser-setup/SKILL.md +90 -0
- data/lib/clacky/default_skills/channel-manager/SKILL.md +1 -0
- data/lib/clacky/default_skills/channel-manager/weixin_setup.rb +28 -8
- data/lib/clacky/mcp/stdio_transport.rb +6 -1
- data/lib/clacky/providers.rb +5 -7
- data/lib/clacky/server/browser_manager.rb +16 -3
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +46 -1
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +38 -13
- data/lib/clacky/server/channel/channel_manager.rb +14 -4
- data/lib/clacky/server/channel/channel_ui_controller.rb +27 -11
- data/lib/clacky/server/http_server.rb +13 -3
- data/lib/clacky/tools/browser.rb +3 -3
- data/lib/clacky/tools/glob.rb +19 -4
- data/lib/clacky/tools/grep.rb +13 -1
- data/lib/clacky/tools/security.rb +1 -2
- data/lib/clacky/tools/terminal.rb +210 -14
- data/lib/clacky/ui2/ui_controller.rb +20 -2
- data/lib/clacky/utils/file_ignore_helper.rb +78 -5
- data/lib/clacky/utils/login_shell.rb +3 -1
- data/lib/clacky/utils/model_pricing.rb +28 -3
- data/lib/clacky/utils/scripts_manager.rb +1 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1 -0
- data/lib/clacky/web/settings.js +5 -0
- data/lib/clacky/web/weixin-qr.html +5 -4
- data/scripts/build/lib/apt.sh +71 -1
- data/scripts/build/src/install.sh.cc +1 -1
- data/scripts/build/src/install_rails_deps.sh.cc +4 -4
- data/scripts/build/src/install_system_deps.sh.cc +1 -1
- data/scripts/install.ps1 +44 -17
- data/scripts/install.sh +72 -2
- data/scripts/install_rails_deps.sh +75 -5
- data/scripts/install_system_deps.sh +72 -2
- data/scripts/wsl_network_doctor.ps1 +196 -0
- metadata +3 -2
|
@@ -116,21 +116,94 @@ module Clacky
|
|
|
116
116
|
CONFIG_FILE_PATTERNS.any? { |pattern| file.match?(pattern) }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Paths considered too broad to recursively walk by default. Searching from
|
|
120
|
+
# these would commonly traverse millions of files (system roots, $HOME with
|
|
121
|
+
# many workspaces, WSL Windows mounts). Tools should refuse such requests
|
|
122
|
+
# and ask for a narrower base_path.
|
|
123
|
+
def self.dangerous_root?(path)
|
|
124
|
+
return false if path.nil? || path.empty?
|
|
125
|
+
|
|
126
|
+
expanded = File.expand_path(path)
|
|
127
|
+
return true if expanded == "/"
|
|
128
|
+
|
|
129
|
+
system_roots = ["/root", "/home", "/Users", "/mnt", "/media", "/var", "/etc", "/usr", "/opt"]
|
|
130
|
+
return true if system_roots.include?(expanded)
|
|
131
|
+
|
|
132
|
+
["/Users/", "/home/"].each do |prefix|
|
|
133
|
+
next unless expanded.start_with?(prefix)
|
|
134
|
+
tail = expanded[prefix.length..]
|
|
135
|
+
return true if tail && !tail.empty? && !tail.include?("/")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return true if expanded =~ %r{\A/mnt/[a-zA-Z]\z}
|
|
139
|
+
|
|
140
|
+
home = ENV["HOME"]
|
|
141
|
+
return true if home && !home.empty? && expanded == File.expand_path(home)
|
|
142
|
+
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Hard ceiling on directories visited in a single walk. Prevents indefinite
|
|
147
|
+
# traversal across huge trees (e.g. /root, $HOME, /mnt/c on WSL).
|
|
148
|
+
MAX_DIRS_VISITED = 20_000
|
|
149
|
+
|
|
150
|
+
# Wall-clock budget for a single walk, in seconds.
|
|
151
|
+
WALK_TIMEOUT_SECONDS = 15
|
|
152
|
+
|
|
153
|
+
# Raised internally to abort a walk when a budget is exhausted.
|
|
154
|
+
class WalkBudgetExceeded < StandardError
|
|
155
|
+
attr_reader :reason
|
|
156
|
+
def initialize(reason)
|
|
157
|
+
@reason = reason
|
|
158
|
+
super(reason.to_s)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
119
162
|
# Walk a directory tree, pruning ignored directories early.
|
|
120
163
|
# Yields each non-ignored file path. Supports nested .gitignore files.
|
|
121
164
|
# @param skipped [Hash, nil] If provided, increments :ignored for each gitignore-skipped entry.
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
# @param status [Hash, nil] If provided, populated with :truncated and :truncation_reason
|
|
166
|
+
# when the walk is aborted due to dir-count or wall-clock budget.
|
|
167
|
+
def self.walk_files(base_path, gitignore: nil, skipped: nil, status: nil,
|
|
168
|
+
max_dirs_visited: MAX_DIRS_VISITED,
|
|
169
|
+
timeout_seconds: WALK_TIMEOUT_SECONDS,
|
|
170
|
+
&block)
|
|
171
|
+
unless block_given?
|
|
172
|
+
return enum_for(:walk_files, base_path,
|
|
173
|
+
gitignore: gitignore, skipped: skipped, status: status,
|
|
174
|
+
max_dirs_visited: max_dirs_visited, timeout_seconds: timeout_seconds)
|
|
175
|
+
end
|
|
124
176
|
|
|
125
177
|
root_gitignore = gitignore || begin
|
|
126
178
|
gi_path = find_gitignore(base_path)
|
|
127
179
|
gi_path ? Clacky::GitignoreParser.new(gi_path) : nil
|
|
128
180
|
end
|
|
129
181
|
|
|
130
|
-
|
|
182
|
+
budget = {
|
|
183
|
+
dirs_visited: 0,
|
|
184
|
+
max_dirs: max_dirs_visited,
|
|
185
|
+
deadline: timeout_seconds ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds) : nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
_walk_recursive(base_path, base_path, root_gitignore, skipped, budget, &block)
|
|
190
|
+
rescue WalkBudgetExceeded => e
|
|
191
|
+
if status
|
|
192
|
+
status[:truncated] = true
|
|
193
|
+
status[:truncation_reason] = e.reason.to_s
|
|
194
|
+
end
|
|
195
|
+
end
|
|
131
196
|
end
|
|
132
197
|
|
|
133
|
-
def self._walk_recursive(dir, base_path, gitignore, skipped, &block)
|
|
198
|
+
def self._walk_recursive(dir, base_path, gitignore, skipped, budget, &block)
|
|
199
|
+
budget[:dirs_visited] += 1
|
|
200
|
+
if budget[:dirs_visited] > budget[:max_dirs]
|
|
201
|
+
raise WalkBudgetExceeded.new(:max_dirs_visited)
|
|
202
|
+
end
|
|
203
|
+
if budget[:deadline] && Process.clock_gettime(Process::CLOCK_MONOTONIC) > budget[:deadline]
|
|
204
|
+
raise WalkBudgetExceeded.new(:timeout)
|
|
205
|
+
end
|
|
206
|
+
|
|
134
207
|
child_gitignore_path = File.join(dir, ".gitignore")
|
|
135
208
|
if dir != base_path && File.exist?(child_gitignore_path)
|
|
136
209
|
gitignore ||= Clacky::GitignoreParser.new(nil)
|
|
@@ -153,7 +226,7 @@ module Clacky
|
|
|
153
226
|
if gitignore&.ignored?("#{relative}/") || should_ignore_file?(full, base_path, gitignore)
|
|
154
227
|
next
|
|
155
228
|
end
|
|
156
|
-
_walk_recursive(full, base_path, gitignore, skipped, &block)
|
|
229
|
+
_walk_recursive(full, base_path, gitignore, skipped, budget, &block)
|
|
157
230
|
else
|
|
158
231
|
if !is_config_file?(full) && should_ignore_file?(full, base_path, gitignore)
|
|
159
232
|
skipped[:ignored] += 1 if skipped
|
|
@@ -51,7 +51,9 @@ module Clacky
|
|
|
51
51
|
|
|
52
52
|
# { rc_sources; } 1>&2 — send rc-time stdout to stderr so target's
|
|
53
53
|
# stdout is pristine. `exec` replaces the shell with target.
|
|
54
|
-
|
|
54
|
+
# PS1 trick: Ubuntu .bashrc has `[ -z "$PS1" ] && return` guard that
|
|
55
|
+
# skips the entire file in non-interactive shells. Setting PS1 defeats it.
|
|
56
|
+
script = "PS1='$ '; { #{rc_sources}; } 1>&2; exec #{command}"
|
|
55
57
|
[shell, "-c", script]
|
|
56
58
|
end
|
|
57
59
|
|
|
@@ -366,6 +366,24 @@ module Clacky
|
|
|
366
366
|
# surprising users with the explicit 25% surcharge).
|
|
367
367
|
# - We bill reads at 20% (implicit rate) — the conservative side; users on
|
|
368
368
|
# explicit caching will see real bills slightly *lower* than displayed.
|
|
369
|
+
"qwen3.7-max" => {
|
|
370
|
+
input: { default: 1.20, over_200k: 1.20 },
|
|
371
|
+
output: { default: 6.00, over_200k: 6.00 },
|
|
372
|
+
cache: { write: 1.20, read: 0.24 }
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
"qwen3.7-plus" => {
|
|
376
|
+
input: { default: 0.40, over_200k: 0.40 },
|
|
377
|
+
output: { default: 2.40, over_200k: 2.40 },
|
|
378
|
+
cache: { write: 0.40, read: 0.08 }
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
"qwen3.7-flash" => {
|
|
382
|
+
input: { default: 0.15, over_200k: 0.15 },
|
|
383
|
+
output: { default: 0.90, over_200k: 0.90 },
|
|
384
|
+
cache: { write: 0.15, read: 0.03 }
|
|
385
|
+
},
|
|
386
|
+
|
|
369
387
|
"qwen3.6-plus" => {
|
|
370
388
|
input: { default: 0.40, over_200k: 0.40 },
|
|
371
389
|
output: { default: 2.40, over_200k: 2.40 },
|
|
@@ -571,9 +589,16 @@ module Clacky
|
|
|
571
589
|
"minimax-m2.7"
|
|
572
590
|
|
|
573
591
|
# Qwen (Alibaba DashScope) — strict anchored match per registered
|
|
574
|
-
# model id in providers.rb. qwen3.
|
|
575
|
-
# qwen-plus-latest is the
|
|
576
|
-
# release; qwen-vl-* are
|
|
592
|
+
# model id in providers.rb. qwen3.7-* is the latest flagship line;
|
|
593
|
+
# qwen3.6-* are the previous generation; qwen-plus-latest is the
|
|
594
|
+
# rolling alias for the latest Qwen-Plus release; qwen-vl-* are
|
|
595
|
+
# the multimodal SKUs.
|
|
596
|
+
when /^qwen3\.7-max$/i
|
|
597
|
+
"qwen3.7-max"
|
|
598
|
+
when /^qwen3\.7-plus$/i
|
|
599
|
+
"qwen3.7-plus"
|
|
600
|
+
when /^qwen3\.7-flash$/i
|
|
601
|
+
"qwen3.7-flash"
|
|
577
602
|
when /^qwen3\.6-plus$/i
|
|
578
603
|
"qwen3.6-plus"
|
|
579
604
|
when /^qwen3\.6-max$/i
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
data/lib/clacky/web/settings.js
CHANGED
|
@@ -169,6 +169,11 @@ const Settings = (() => {
|
|
|
169
169
|
providerValue.classList.add("placeholder");
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// Reset save button
|
|
173
|
+
const saveBtn = document.getElementById("model-modal-save");
|
|
174
|
+
saveBtn.textContent = I18n.t("settings.models.btn.save");
|
|
175
|
+
saveBtn.disabled = false;
|
|
176
|
+
|
|
172
177
|
// Clear test result
|
|
173
178
|
document.getElementById("model-modal-test-result").textContent = "";
|
|
174
179
|
document.getElementById("model-modal-test-result").className = "model-test-result";
|
|
@@ -140,10 +140,11 @@
|
|
|
140
140
|
<script>
|
|
141
141
|
const params = new URLSearchParams(location.search);
|
|
142
142
|
const url = params.get("url");
|
|
143
|
-
// since:
|
|
144
|
-
// We only show success if token_updated_at
|
|
145
|
-
//
|
|
146
|
-
|
|
143
|
+
// since: the moment this page loaded, in Unix seconds.
|
|
144
|
+
// We only show success if token_updated_at >= since, so a pre-existing token
|
|
145
|
+
// never triggers the overlay.
|
|
146
|
+
// Intentionally NOT taken from the URL param — the page is the source of truth.
|
|
147
|
+
const since = Math.floor(Date.now() / 1000);
|
|
147
148
|
const el = document.getElementById("qrcode");
|
|
148
149
|
|
|
149
150
|
if (!url) {
|
data/scripts/build/lib/apt.sh
CHANGED
|
@@ -4,6 +4,76 @@
|
|
|
4
4
|
# Sets-Vars: (none)
|
|
5
5
|
# Include via: @include lib/apt.sh
|
|
6
6
|
|
|
7
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
8
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
9
|
+
# rather than checking file existence (the lock files are always present;
|
|
10
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
11
|
+
wait_apt_lock() {
|
|
12
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
13
|
+
|
|
14
|
+
local locks=(
|
|
15
|
+
"/var/lib/dpkg/lock-frontend"
|
|
16
|
+
"/var/lib/dpkg/lock"
|
|
17
|
+
"/var/lib/apt/lists/lock"
|
|
18
|
+
)
|
|
19
|
+
local max_wait="${1:-120}"
|
|
20
|
+
local waited=0
|
|
21
|
+
local announced=false
|
|
22
|
+
|
|
23
|
+
while :; do
|
|
24
|
+
local busy=false
|
|
25
|
+
for f in "${locks[@]}"; do
|
|
26
|
+
[ -e "$f" ] || continue
|
|
27
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
28
|
+
busy=true
|
|
29
|
+
break
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
[ "$busy" = false ] && break
|
|
34
|
+
|
|
35
|
+
if [ "$announced" = false ]; then
|
|
36
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
37
|
+
announced=true
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
41
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
42
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
43
|
+
return 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
sleep 3
|
|
47
|
+
waited=$((waited + 3))
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
55
|
+
# Usage: apt_get_run update [-qq]
|
|
56
|
+
# apt_get_run install -y pkg1 pkg2
|
|
57
|
+
apt_get_run() {
|
|
58
|
+
local attempts=3
|
|
59
|
+
local i=1
|
|
60
|
+
while [ "$i" -le "$attempts" ]; do
|
|
61
|
+
wait_apt_lock 120 || return 1
|
|
62
|
+
if sudo apt-get "$@"; then
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
local rc=$?
|
|
66
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
67
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
68
|
+
sleep 5
|
|
69
|
+
else
|
|
70
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
71
|
+
return "$rc"
|
|
72
|
+
fi
|
|
73
|
+
i=$((i + 1))
|
|
74
|
+
done
|
|
75
|
+
}
|
|
76
|
+
|
|
7
77
|
# Configure apt mirror for CN region and run apt-get update.
|
|
8
78
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
9
79
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -51,6 +121,6 @@ EOF
|
|
|
51
121
|
print_info "Region: global — using default apt sources"
|
|
52
122
|
fi
|
|
53
123
|
|
|
54
|
-
|
|
124
|
+
apt_get_run update -qq || return 1
|
|
55
125
|
print_success "apt updated"
|
|
56
126
|
}
|
|
@@ -34,7 +34,7 @@ ensure_ruby() {
|
|
|
34
34
|
|
|
35
35
|
if is_linux_apt; then
|
|
36
36
|
print_info "Installing Ruby via apt..."
|
|
37
|
-
|
|
37
|
+
apt_get_run install -y ruby ruby-dev 2>/dev/null && check_ruby && return 0
|
|
38
38
|
print_warning "apt Ruby install failed or version too old"
|
|
39
39
|
fi
|
|
40
40
|
|
|
@@ -31,8 +31,8 @@ install_ruby() {
|
|
|
31
31
|
if is_macos; then
|
|
32
32
|
brew install openssl@3 libyaml gmp
|
|
33
33
|
elif is_linux_apt; then
|
|
34
|
-
|
|
35
|
-
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
|
|
34
|
+
apt_get_run install -y \
|
|
35
|
+
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev || return 1
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
38
|
ensure_mise || return 1
|
|
@@ -75,8 +75,8 @@ install_postgres() {
|
|
|
75
75
|
|
|
76
76
|
elif is_linux_apt; then
|
|
77
77
|
setup_apt_mirror
|
|
78
|
-
|
|
79
|
-
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev
|
|
78
|
+
apt_get_run install -y postgresql libpq-dev \
|
|
79
|
+
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev || return 1
|
|
80
80
|
sudo systemctl enable --now postgresql || true
|
|
81
81
|
fi
|
|
82
82
|
|
data/scripts/install.ps1
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
# -BrandName Display name shown in prompts (default: OpenClacky)
|
|
12
12
|
# -CommandName CLI command name after install (default: openclacky)
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# the script
|
|
14
|
+
# WSL1 is preferred (shares Windows network stack — no mirrored networking needed).
|
|
15
|
+
# If WSL1 import fails, the script falls back to WSL2 with mirrored networking.
|
|
16
16
|
# If WSL is not installed at all, the script enables it and asks you to reboot.
|
|
17
17
|
# After rebooting, run the same command again to complete installation.
|
|
18
18
|
#
|
|
@@ -501,32 +501,59 @@ if ($installPhase -eq "wsl-pending" -and $wslCode -eq 1) {
|
|
|
501
501
|
# wslCode != 1 (0, -1, -444, 50, etc.): WSL is functional, continue.
|
|
502
502
|
Remove-InstallReg -Name "InstallPhase"
|
|
503
503
|
|
|
504
|
-
# Step 2: Install Ubuntu, preferring
|
|
504
|
+
# Step 2: Install Ubuntu, preferring WSL1 (shares Windows network — no mirrored needed).
|
|
505
|
+
# If WSL1 import fails, fall back to WSL2.
|
|
506
|
+
# If the distro already exists, keep whatever version was previously installed.
|
|
505
507
|
if (Test-UbuntuInstalled) {
|
|
506
508
|
Write-Info "Ubuntu (WSL) already installed — skipping import."
|
|
507
509
|
$wslVersion = Get-InstallReg -Name "WslVersion" -Default 2
|
|
508
510
|
} else {
|
|
509
511
|
$tarPath = Get-UbuntuRootfs
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
|
|
513
|
+
# Try WSL1 first
|
|
514
|
+
Write-Info "Attempting WSL1 import..."
|
|
515
|
+
$wsl1Ok = $false
|
|
516
|
+
try {
|
|
517
|
+
New-Item -ItemType Directory -Force -Path $UBUNTU_WSL_DIR | Out-Null
|
|
518
|
+
wsl.exe --import Ubuntu $UBUNTU_WSL_DIR $tarPath --version 1 >$null 2>$null
|
|
519
|
+
$wsl1Ok = ($LASTEXITCODE -eq 0)
|
|
520
|
+
} catch {
|
|
521
|
+
$wsl1Ok = $false
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if ($wsl1Ok) {
|
|
525
|
+
Write-Success "Ubuntu (WSL1) imported successfully."
|
|
526
|
+
$wslVersion = 1
|
|
514
527
|
} else {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
528
|
+
# Clean up failed WSL1 attempt
|
|
529
|
+
wsl.exe --unregister Ubuntu 2>$null | Out-Null
|
|
530
|
+
Remove-Item -Force -Recurse -ErrorAction SilentlyContinue $UBUNTU_WSL_DIR
|
|
531
|
+
|
|
532
|
+
Write-Info "WSL1 import failed, trying WSL2..."
|
|
533
|
+
if (Test-VirtualisationSupported -TarPath $tarPath) {
|
|
534
|
+
wsl.exe --set-default-version 2 >$null 2>$null
|
|
535
|
+
Install-UbuntuRootfs -WslVersion 2 -TarPath $tarPath
|
|
536
|
+
$wslVersion = 2
|
|
537
|
+
} else {
|
|
538
|
+
if ($wslFeaturesEnabled -ne "1") {
|
|
539
|
+
Write-Warn "Neither WSL1 nor WSL2 is available. Enabling WSL components..."
|
|
540
|
+
Enable-WslFeatures
|
|
541
|
+
# Always exits (prompts reboot)
|
|
542
|
+
}
|
|
543
|
+
Write-Fail "Failed to import Ubuntu into both WSL1 and WSL2."
|
|
544
|
+
Write-Fail "Please ensure Windows Subsystem for Linux is enabled and try again."
|
|
545
|
+
exit 1
|
|
520
546
|
}
|
|
521
|
-
Write-Info "[main] WSL2 unavailable, falling back to WSL1..."
|
|
522
|
-
Install-UbuntuRootfs -WslVersion 1 -TarPath $tarPath
|
|
523
|
-
$wslVersion = 1
|
|
524
547
|
}
|
|
525
548
|
}
|
|
526
549
|
|
|
527
|
-
if ($wslVersion -eq 2) { Set-Wsl2MirroredNetworking }
|
|
528
|
-
|
|
529
550
|
Write-Success "WSL is ready."
|
|
530
551
|
Run-InstallInWsl
|
|
552
|
+
|
|
553
|
+
# For WSL2, configure mirrored networking AFTER install.sh succeeds (NAT is more
|
|
554
|
+
# reliable for outbound traffic during installation). The shutdown here is safe
|
|
555
|
+
# because installation is already complete.
|
|
556
|
+
if ($wslVersion -eq 2) { Set-Wsl2MirroredNetworking }
|
|
557
|
+
|
|
531
558
|
Set-InstallReg -Name "WslVersion" -Value $wslVersion
|
|
532
559
|
Show-PostInstall -WslVersion $wslVersion
|
data/scripts/install.sh
CHANGED
|
@@ -271,6 +271,76 @@ detect_network_region() {
|
|
|
271
271
|
|
|
272
272
|
# ---[ @include lib/apt.sh ]---
|
|
273
273
|
|
|
274
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
275
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
276
|
+
# rather than checking file existence (the lock files are always present;
|
|
277
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
278
|
+
wait_apt_lock() {
|
|
279
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
280
|
+
|
|
281
|
+
local locks=(
|
|
282
|
+
"/var/lib/dpkg/lock-frontend"
|
|
283
|
+
"/var/lib/dpkg/lock"
|
|
284
|
+
"/var/lib/apt/lists/lock"
|
|
285
|
+
)
|
|
286
|
+
local max_wait="${1:-120}"
|
|
287
|
+
local waited=0
|
|
288
|
+
local announced=false
|
|
289
|
+
|
|
290
|
+
while :; do
|
|
291
|
+
local busy=false
|
|
292
|
+
for f in "${locks[@]}"; do
|
|
293
|
+
[ -e "$f" ] || continue
|
|
294
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
295
|
+
busy=true
|
|
296
|
+
break
|
|
297
|
+
fi
|
|
298
|
+
done
|
|
299
|
+
|
|
300
|
+
[ "$busy" = false ] && break
|
|
301
|
+
|
|
302
|
+
if [ "$announced" = false ]; then
|
|
303
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
304
|
+
announced=true
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
308
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
309
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
310
|
+
return 1
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
sleep 3
|
|
314
|
+
waited=$((waited + 3))
|
|
315
|
+
done
|
|
316
|
+
|
|
317
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
318
|
+
return 0
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
322
|
+
# Usage: apt_get_run update [-qq]
|
|
323
|
+
# apt_get_run install -y pkg1 pkg2
|
|
324
|
+
apt_get_run() {
|
|
325
|
+
local attempts=3
|
|
326
|
+
local i=1
|
|
327
|
+
while [ "$i" -le "$attempts" ]; do
|
|
328
|
+
wait_apt_lock 120 || return 1
|
|
329
|
+
if sudo apt-get "$@"; then
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
local rc=$?
|
|
333
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
334
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
335
|
+
sleep 5
|
|
336
|
+
else
|
|
337
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
338
|
+
return "$rc"
|
|
339
|
+
fi
|
|
340
|
+
i=$((i + 1))
|
|
341
|
+
done
|
|
342
|
+
}
|
|
343
|
+
|
|
274
344
|
# Configure apt mirror for CN region and run apt-get update.
|
|
275
345
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
276
346
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -318,7 +388,7 @@ EOF
|
|
|
318
388
|
print_info "Region: global — using default apt sources"
|
|
319
389
|
fi
|
|
320
390
|
|
|
321
|
-
|
|
391
|
+
apt_get_run update -qq || return 1
|
|
322
392
|
print_success "apt updated"
|
|
323
393
|
}
|
|
324
394
|
|
|
@@ -427,7 +497,7 @@ ensure_ruby() {
|
|
|
427
497
|
|
|
428
498
|
if is_linux_apt; then
|
|
429
499
|
print_info "Installing Ruby via apt..."
|
|
430
|
-
|
|
500
|
+
apt_get_run install -y ruby ruby-dev 2>/dev/null && check_ruby && return 0
|
|
431
501
|
print_warning "apt Ruby install failed or version too old"
|
|
432
502
|
fi
|
|
433
503
|
|
|
@@ -275,6 +275,76 @@ detect_network_region() {
|
|
|
275
275
|
|
|
276
276
|
# ---[ @include lib/apt.sh ]---
|
|
277
277
|
|
|
278
|
+
# Wait until apt/dpkg lock files are no longer held (e.g. by apt-daily on
|
|
279
|
+
# freshly-booted WSL/Ubuntu). Uses flock(1) — the same mechanism apt uses —
|
|
280
|
+
# rather than checking file existence (the lock files are always present;
|
|
281
|
+
# advisory locks live in the kernel, not the filesystem).
|
|
282
|
+
wait_apt_lock() {
|
|
283
|
+
[ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || return 0
|
|
284
|
+
|
|
285
|
+
local locks=(
|
|
286
|
+
"/var/lib/dpkg/lock-frontend"
|
|
287
|
+
"/var/lib/dpkg/lock"
|
|
288
|
+
"/var/lib/apt/lists/lock"
|
|
289
|
+
)
|
|
290
|
+
local max_wait="${1:-120}"
|
|
291
|
+
local waited=0
|
|
292
|
+
local announced=false
|
|
293
|
+
|
|
294
|
+
while :; do
|
|
295
|
+
local busy=false
|
|
296
|
+
for f in "${locks[@]}"; do
|
|
297
|
+
[ -e "$f" ] || continue
|
|
298
|
+
if ! sudo flock -n "$f" -c true 2>/dev/null; then
|
|
299
|
+
busy=true
|
|
300
|
+
break
|
|
301
|
+
fi
|
|
302
|
+
done
|
|
303
|
+
|
|
304
|
+
[ "$busy" = false ] && break
|
|
305
|
+
|
|
306
|
+
if [ "$announced" = false ]; then
|
|
307
|
+
print_info "Waiting for system apt/dpkg to finish (up to ${max_wait}s)..."
|
|
308
|
+
announced=true
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
if [ "$waited" -ge "$max_wait" ]; then
|
|
312
|
+
print_error "apt is still locked after ${max_wait}s."
|
|
313
|
+
print_info "On WSL try: 'wsl --shutdown' from PowerShell, then rerun the installer."
|
|
314
|
+
return 1
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
sleep 3
|
|
318
|
+
waited=$((waited + 3))
|
|
319
|
+
done
|
|
320
|
+
|
|
321
|
+
[ "$announced" = true ] && print_success "apt lock released"
|
|
322
|
+
return 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Run an apt-get subcommand with lock-wait + transient-failure retry.
|
|
326
|
+
# Usage: apt_get_run update [-qq]
|
|
327
|
+
# apt_get_run install -y pkg1 pkg2
|
|
328
|
+
apt_get_run() {
|
|
329
|
+
local attempts=3
|
|
330
|
+
local i=1
|
|
331
|
+
while [ "$i" -le "$attempts" ]; do
|
|
332
|
+
wait_apt_lock 120 || return 1
|
|
333
|
+
if sudo apt-get "$@"; then
|
|
334
|
+
return 0
|
|
335
|
+
fi
|
|
336
|
+
local rc=$?
|
|
337
|
+
if [ "$i" -lt "$attempts" ]; then
|
|
338
|
+
print_warning "apt-get $1 failed (exit $rc), retrying ($i/$((attempts-1)))..."
|
|
339
|
+
sleep 5
|
|
340
|
+
else
|
|
341
|
+
print_error "apt-get $1 failed after $attempts attempts."
|
|
342
|
+
return "$rc"
|
|
343
|
+
fi
|
|
344
|
+
i=$((i + 1))
|
|
345
|
+
done
|
|
346
|
+
}
|
|
347
|
+
|
|
278
348
|
# Configure apt mirror for CN region and run apt-get update.
|
|
279
349
|
# Guards: only runs on ubuntu/debian ($DISTRO).
|
|
280
350
|
# Relies on $USE_CN_MIRRORS set by detect_network_region (network.sh).
|
|
@@ -322,7 +392,7 @@ EOF
|
|
|
322
392
|
print_info "Region: global — using default apt sources"
|
|
323
393
|
fi
|
|
324
394
|
|
|
325
|
-
|
|
395
|
+
apt_get_run update -qq || return 1
|
|
326
396
|
print_success "apt updated"
|
|
327
397
|
}
|
|
328
398
|
|
|
@@ -632,8 +702,8 @@ install_ruby() {
|
|
|
632
702
|
if is_macos; then
|
|
633
703
|
brew install openssl@3 libyaml gmp
|
|
634
704
|
elif is_linux_apt; then
|
|
635
|
-
|
|
636
|
-
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
|
|
705
|
+
apt_get_run install -y \
|
|
706
|
+
rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev || return 1
|
|
637
707
|
fi
|
|
638
708
|
|
|
639
709
|
ensure_mise || return 1
|
|
@@ -676,8 +746,8 @@ install_postgres() {
|
|
|
676
746
|
|
|
677
747
|
elif is_linux_apt; then
|
|
678
748
|
setup_apt_mirror
|
|
679
|
-
|
|
680
|
-
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev
|
|
749
|
+
apt_get_run install -y postgresql libpq-dev \
|
|
750
|
+
libssl-dev libreadline-dev zlib1g-dev libyaml-dev libffi-dev || return 1
|
|
681
751
|
sudo systemctl enable --now postgresql || true
|
|
682
752
|
fi
|
|
683
753
|
|