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: {}
|
expect: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callDropletJobStatus = rpc.declare({
|
||||||
|
object: 'luci.droplet',
|
||||||
|
method: 'job_status',
|
||||||
|
params: ['job_id'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -302,7 +309,11 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(function(result) {
|
.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>');
|
showResult('success', '✅ Published! <a href="' + result.url + '" target="_blank">' + result.url + '</a>');
|
||||||
setTimeout(function() { location.reload(); }, 2000);
|
setTimeout(function() { location.reload(); }, 2000);
|
||||||
} else {
|
} else {
|
||||||
@ -323,6 +334,47 @@ return view.extend({
|
|||||||
resultMsg.innerHTML = msg;
|
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
|
// Delete buttons
|
||||||
view.querySelectorAll('.btn-delete').forEach(function(btn) {
|
view.querySelectorAll('.btn-delete').forEach(function(btn) {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
|
|||||||
@ -5,10 +5,14 @@
|
|||||||
|
|
||||||
UPLOAD_DIR="/tmp/droplet-upload"
|
UPLOAD_DIR="/tmp/droplet-upload"
|
||||||
DEFAULT_DOMAIN="gk2.secubox.in"
|
DEFAULT_DOMAIN="gk2.secubox.in"
|
||||||
|
JOB_DIR="/tmp/droplet-jobs"
|
||||||
|
|
||||||
|
# Ensure job dir exists
|
||||||
|
mkdir -p "$JOB_DIR"
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -21,6 +25,25 @@ case "$1" in
|
|||||||
json_dump
|
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)
|
list)
|
||||||
json_init
|
json_init
|
||||||
json_add_array "droplets"
|
json_add_array "droplets"
|
||||||
@ -59,53 +82,57 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
upload)
|
upload)
|
||||||
# Read params
|
# Read params using jsonfilter for reliability
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
file=$(echo "$input" | jsonfilter -e '@.file' 2>/dev/null)
|
||||||
json_get_var file file
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
json_get_var name name
|
domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||||
json_get_var domain domain
|
|
||||||
|
|
||||||
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
||||||
[ -z "$file" ] && { echo '{"error":"File required"}'; exit 0; }
|
[ -z "$file" ] && { echo '{"error":"File required"}'; exit 0; }
|
||||||
[ -z "$domain" ] && domain="$DEFAULT_DOMAIN"
|
[ -z "$domain" ] && domain="$DEFAULT_DOMAIN"
|
||||||
|
|
||||||
# File should be in upload dir (set by cgi-io)
|
# 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
|
if [ ! -f "$upload_file" ]; then
|
||||||
# Try direct path
|
# Try direct path
|
||||||
upload_file="$file"
|
upload_file="$file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ ! -f "$upload_file" ] && { echo '{"error":"File not found"}'; exit 0; }
|
[ ! -f "$upload_file" ] && { echo '{"error":"File not found: '"$file"'"}'; exit 0; }
|
||||||
|
|
||||||
# Publish
|
# Generate job ID and run publish async
|
||||||
result=$(dropletctl publish "$upload_file" "$name" "$domain" 2>&1)
|
job_id="$(date +%s)_$$"
|
||||||
exit_code=$?
|
job_file="$JOB_DIR/$job_id"
|
||||||
|
|
||||||
# Extract vhost from result
|
# Initialize job status
|
||||||
vhost=$(echo "$result" | grep -oE '[a-z0-9_-]+\.[a-z0-9.-]+' | tail -1)
|
echo '{"status":"running","name":"'"$name"'","domain":"'"$domain"'"}' > "$job_file"
|
||||||
|
|
||||||
json_init
|
# Write background script - use file output to avoid pipe inheritance issues
|
||||||
if [ $exit_code -eq 0 ]; then
|
bg_script="/tmp/droplet-bg-$job_id.sh"
|
||||||
json_add_boolean "success" 1
|
result_file="/tmp/droplet-result-$job_id.txt"
|
||||||
json_add_string "vhost" "$vhost"
|
printf '#!/bin/sh\nupload_file="$1"\nname="$2"\ndomain="$3"\njob_file="$4"\nresult_file="$5"\n' > "$bg_script"
|
||||||
json_add_string "url" "https://$vhost/"
|
# Write output to file instead of capturing in variable (avoids pipe inheritance)
|
||||||
json_add_string "message" "Published successfully"
|
printf 'dropletctl publish "$upload_file" "$name" "$domain" > "$result_file" 2>&1\nexit_code=$?\n' >> "$bg_script"
|
||||||
else
|
# Check for success marker and extract vhost
|
||||||
json_add_boolean "success" 0
|
printf 'if grep -q "\\[OK\\] Published:" "$result_file"; then\n' >> "$bg_script"
|
||||||
json_add_string "error" "$result"
|
printf ' vhost=$(tail -1 "$result_file")\n' >> "$bg_script"
|
||||||
fi
|
printf ' printf '\''{"status":"complete","success":true,"vhost":"%%s","url":"https://%%s/","message":"Published successfully"}'\'' "$vhost" "$vhost" > "$job_file"\n' >> "$bg_script"
|
||||||
json_dump
|
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
|
# Run background script detached
|
||||||
rm -f "$upload_file"
|
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)
|
remove)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
json_get_var name name
|
|
||||||
|
|
||||||
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
[ -z "$name" ] && { echo '{"error":"Name required"}'; exit 0; }
|
||||||
|
|
||||||
@ -119,9 +146,8 @@ case "$1" in
|
|||||||
|
|
||||||
rename)
|
rename)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
old=$(echo "$input" | jsonfilter -e '@.old' 2>/dev/null)
|
||||||
json_get_var old old
|
new=$(echo "$input" | jsonfilter -e '@.new' 2>/dev/null)
|
||||||
json_get_var new new
|
|
||||||
|
|
||||||
[ -z "$old" ] || [ -z "$new" ] && { echo '{"error":"Old and new names required"}'; exit 0; }
|
[ -z "$old" ] || [ -z "$new" ] && { echo '{"error":"Old and new names required"}'; exit 0; }
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Droplet Publisher",
|
"description": "Droplet Publisher",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.droplet": ["status", "list"]
|
"luci.droplet": ["status", "list", "job_status"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user