feat(droplet): Implement async publish to prevent UI timeout
- RPCD handler returns immediately with job_id (~0.04s) - Background script uses file output to avoid pipe inheritance issues - LuCI JS polls job_status every 2s until completion - Uses setsid for proper process detachment - jsonfilter for reliable parameter parsing Fixes "Failed to publish" error caused by ubus timeout during 40+ second publish operations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ddf480e6ed
commit
e1f2a0e885
@ -30,6 +30,13 @@ var callDropletRemove = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDropletJobStatus = rpc.declare({
|
||||
object: 'luci.droplet',
|
||||
method: 'job_status',
|
||||
params: ['job_id'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
@ -302,7 +309,11 @@ return view.extend({
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.success) {
|
||||
// Handle async job response
|
||||
if (result.status === 'started' && result.job_id) {
|
||||
showResult('success', '⏳ Publishing ' + result.name + '...');
|
||||
return pollJobStatus(result.job_id);
|
||||
} else if (result.success) {
|
||||
showResult('success', '✅ Published! <a href="' + result.url + '" target="_blank">' + result.url + '</a>');
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
} else {
|
||||
@ -323,6 +334,47 @@ return view.extend({
|
||||
resultMsg.innerHTML = msg;
|
||||
}
|
||||
|
||||
function pollJobStatus(jobId) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 60; // 60 * 2s = 2 minutes max
|
||||
|
||||
function check() {
|
||||
callDropletJobStatus(jobId).then(function(status) {
|
||||
if (status.status === 'complete') {
|
||||
if (status.success) {
|
||||
showResult('success', '✅ Published! <a href="' + status.url + '" target="_blank">' + status.url + '</a>');
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
} else {
|
||||
showResult('error', '❌ ' + (status.error || 'Failed to publish'));
|
||||
}
|
||||
resolve(status);
|
||||
} else if (status.status === 'running') {
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(check, 2000);
|
||||
} else {
|
||||
showResult('error', '❌ Publish timed out');
|
||||
reject(new Error('Timeout'));
|
||||
}
|
||||
} else {
|
||||
showResult('error', '❌ Job not found');
|
||||
reject(new Error('Job not found'));
|
||||
}
|
||||
}).catch(function(err) {
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(check, 2000);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// Delete buttons
|
||||
view.querySelectorAll('.btn-delete').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
|
||||
@ -5,10 +5,14 @@
|
||||
|
||||
UPLOAD_DIR="/tmp/droplet-upload"
|
||||
DEFAULT_DOMAIN="gk2.secubox.in"
|
||||
JOB_DIR="/tmp/droplet-jobs"
|
||||
|
||||
# Ensure job dir exists
|
||||
mkdir -p "$JOB_DIR"
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"publish":{},"upload":{"file":"string","name":"string","domain":"string"},"list":{},"remove":{"name":"string"},"rename":{"old":"string","new":"string"},"status":{}}'
|
||||
echo '{"publish":{},"upload":{"file":"string","name":"string","domain":"string"},"list":{},"remove":{"name":"string"},"rename":{"old":"string","new":"string"},"status":{},"job_status":{"job_id":"string"}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
@ -21,6 +25,25 @@ case "$1" in
|
||||
json_dump
|
||||
;;
|
||||
|
||||
job_status)
|
||||
# Check status of an async publish job
|
||||
read -r input
|
||||
job_id=$(echo "$input" | jsonfilter -e '@.job_id' 2>/dev/null)
|
||||
|
||||
if [ -z "$job_id" ]; then
|
||||
echo '{"error":"Job ID required"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
job_file="$JOB_DIR/$job_id"
|
||||
if [ ! -f "$job_file" ]; then
|
||||
echo '{"status":"not_found"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat "$job_file"
|
||||
;;
|
||||
|
||||
list)
|
||||
json_init
|
||||
json_add_array "droplets"
|
||||
@ -59,53 +82,57 @@ case "$1" in
|
||||
;;
|
||||
|
||||
upload)
|
||||
# Read params
|
||||
# Read params using jsonfilter for reliability
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var file file
|
||||
json_get_var name name
|
||||
json_get_var domain domain
|
||||
file=$(echo "$input" | jsonfilter -e '@.file' 2>/dev/null)
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||
|
||||
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
||||
[ -z "$file" ] && { echo '{"error":"File required"}'; exit 0; }
|
||||
[ -z "$domain" ] && domain="$DEFAULT_DOMAIN"
|
||||
|
||||
# File should be in upload dir (set by cgi-io)
|
||||
local upload_file="$UPLOAD_DIR/$file"
|
||||
upload_file="$UPLOAD_DIR/$file"
|
||||
if [ ! -f "$upload_file" ]; then
|
||||
# Try direct path
|
||||
upload_file="$file"
|
||||
fi
|
||||
|
||||
[ ! -f "$upload_file" ] && { echo '{"error":"File not found"}'; exit 0; }
|
||||
[ ! -f "$upload_file" ] && { echo '{"error":"File not found: '"$file"'"}'; exit 0; }
|
||||
|
||||
# Publish
|
||||
result=$(dropletctl publish "$upload_file" "$name" "$domain" 2>&1)
|
||||
exit_code=$?
|
||||
# Generate job ID and run publish async
|
||||
job_id="$(date +%s)_$$"
|
||||
job_file="$JOB_DIR/$job_id"
|
||||
|
||||
# Extract vhost from result
|
||||
vhost=$(echo "$result" | grep -oE '[a-z0-9_-]+\.[a-z0-9.-]+' | tail -1)
|
||||
# Initialize job status
|
||||
echo '{"status":"running","name":"'"$name"'","domain":"'"$domain"'"}' > "$job_file"
|
||||
|
||||
json_init
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "vhost" "$vhost"
|
||||
json_add_string "url" "https://$vhost/"
|
||||
json_add_string "message" "Published successfully"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
# Write background script - use file output to avoid pipe inheritance issues
|
||||
bg_script="/tmp/droplet-bg-$job_id.sh"
|
||||
result_file="/tmp/droplet-result-$job_id.txt"
|
||||
printf '#!/bin/sh\nupload_file="$1"\nname="$2"\ndomain="$3"\njob_file="$4"\nresult_file="$5"\n' > "$bg_script"
|
||||
# Write output to file instead of capturing in variable (avoids pipe inheritance)
|
||||
printf 'dropletctl publish "$upload_file" "$name" "$domain" > "$result_file" 2>&1\nexit_code=$?\n' >> "$bg_script"
|
||||
# Check for success marker and extract vhost
|
||||
printf 'if grep -q "\\[OK\\] Published:" "$result_file"; then\n' >> "$bg_script"
|
||||
printf ' vhost=$(tail -1 "$result_file")\n' >> "$bg_script"
|
||||
printf ' printf '\''{"status":"complete","success":true,"vhost":"%%s","url":"https://%%s/","message":"Published successfully"}'\'' "$vhost" "$vhost" > "$job_file"\n' >> "$bg_script"
|
||||
printf 'else\n error_msg=$(tail -10 "$result_file" | tr '\''\\n'\'' '\'' '\'' | sed '\''s/"/\\\\"/g'\'')\n' >> "$bg_script"
|
||||
printf ' printf '\''{"status":"complete","success":false,"error":"%%s"}'\'' "$error_msg" > "$job_file"\nfi\n' >> "$bg_script"
|
||||
printf 'rm -f "$upload_file" "$result_file" "$0"\nsleep 300\nrm -f "$job_file"\n' >> "$bg_script"
|
||||
chmod +x "$bg_script"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$upload_file"
|
||||
# Run background script detached
|
||||
setsid "$bg_script" "$upload_file" "$name" "$domain" "$job_file" "$result_file" </dev/null >/dev/null 2>&1 &
|
||||
|
||||
# Return immediately with job ID
|
||||
echo '{"status":"started","job_id":"'"$job_id"'","name":"'"$name"'","domain":"'"$domain"'"}'
|
||||
;;
|
||||
|
||||
remove)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var name name
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
|
||||
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
||||
|
||||
@ -119,9 +146,8 @@ case "$1" in
|
||||
|
||||
rename)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var old old
|
||||
json_get_var new new
|
||||
old=$(echo "$input" | jsonfilter -e '@.old' 2>/dev/null)
|
||||
new=$(echo "$input" | jsonfilter -e '@.new' 2>/dev/null)
|
||||
|
||||
[ -z "$old" ] || [ -z "$new" ] && { echo '{"error":"Old and new names required"}'; exit 0; }
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Droplet Publisher",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.droplet": ["status", "list"]
|
||||
"luci.droplet": ["status", "list", "job_status"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user