mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-28 23:05:13 +08:00
update 2026-09-07 01:57:13
This commit is contained in:
1 parent
7b5d12b8ec
commit
4a4dfac1a2
27 files changed
+796
-102
No files matched your search
+2
-1
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
@@ -127,6 +127,7 @@ define Package/dae/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae $(1)/usr/bin/dae
|
||||
$(INSTALL_BIN) $(CURDIR)/files/dae-guard $(1)/usr/bin/dae-guard
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/dae
|
||||
$(INSTALL_CONF) $(PKG_BUILD_DIR)/example.dae $(1)/etc/dae/example.dae
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATE="/var/run/daede-dae.ready"
|
||||
LOG="/var/log/dae/dae.log"
|
||||
READY="Total startup time:"
|
||||
PID="$$"
|
||||
watcher_pid=
|
||||
|
||||
mark_state() {
|
||||
local value="$1" tmp="${STATE}.${PID}"
|
||||
printf '%s %s\n' "$PID" "$value" > "$tmp" && mv -f "$tmp" "$STATE"
|
||||
}
|
||||
|
||||
watch_ready() {
|
||||
local offset size
|
||||
offset="$(wc -c < "$LOG" 2>/dev/null || echo 0)"
|
||||
mark_state starting
|
||||
(
|
||||
while kill -0 "$PID" 2>/dev/null; do
|
||||
size="$(wc -c < "$LOG" 2>/dev/null || echo 0)"
|
||||
[ "$size" -lt "$offset" ] && offset=0
|
||||
if [ -r "$LOG" ] && tail -c "+$((offset + 1))" "$LOG" 2>/dev/null | grep -Fq "$READY"; then
|
||||
# Check before and after the atomic publish. If dae exits during this
|
||||
# window, remove the ready marker rather than leaving a false positive.
|
||||
[ "$(cat "$STATE" 2>/dev/null)" = "$PID starting" ] || exit 1
|
||||
kill -0 "$PID" 2>/dev/null || exit 1
|
||||
mark_state ready || exit 1
|
||||
kill -0 "$PID" 2>/dev/null || { rm -f "$STATE" "$STATE".*; exit 1; }
|
||||
[ "$(cat "$STATE" 2>/dev/null)" = "$PID ready" ] || exit 1
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
) >/dev/null 2>&1 &
|
||||
watcher_pid=$!
|
||||
}
|
||||
|
||||
watch_ready
|
||||
exec /usr/bin/dae "$@"
|
||||
+4
-4
@@ -7,7 +7,7 @@ START=99
|
||||
extra_command "hot_reload" "Hot-reload service"
|
||||
|
||||
CONF="dae"
|
||||
PROG="/usr/bin/dae"
|
||||
PROG="/usr/bin/dae-guard"
|
||||
LOG_DIR="/var/log/dae"
|
||||
|
||||
start_service() {
|
||||
@@ -20,12 +20,11 @@ start_service() {
|
||||
local config_file
|
||||
config_get config_file "config" "config_file" "/etc/dae/config.dae"
|
||||
|
||||
"$PROG" validate -c "$config_file" || return 1
|
||||
/usr/bin/dae validate -c "$config_file" || return 1
|
||||
|
||||
local log_maxbackups log_maxsize
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "1"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
@@ -58,6 +57,7 @@ stop_service() {
|
||||
fi
|
||||
# clean orphaned dae0
|
||||
ip link del dae0 2>/dev/null || true
|
||||
rm -f /var/run/daede-dae.ready /var/run/daede-dae.ready.*
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
@@ -65,5 +65,5 @@ service_triggers() {
|
||||
}
|
||||
|
||||
hot_reload() {
|
||||
/etc/init.d/dae running && "$PROG" reload "$(cat /var/run/dae.pid)"
|
||||
/etc/init.d/dae running && /usr/bin/dae reload "$(cat /var/run/dae.pid)"
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.08.28-73f01b995c34.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
|
||||
+66
-1
@@ -2,16 +2,68 @@
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
STATE="/var/run/daede-daed.ready"
|
||||
LOG="/var/log/daed/daed.log"
|
||||
READY="Reload: Finished"
|
||||
PID="$$"
|
||||
watcher_pid=
|
||||
|
||||
mark_state() {
|
||||
local value="$1" tmp="${STATE}.${PID}"
|
||||
printf '%s %s\n' "$PID" "$value" > "$tmp" && mv -f "$tmp" "$STATE"
|
||||
}
|
||||
|
||||
watch_ready() {
|
||||
local offset size
|
||||
offset="$(wc -c < "$LOG" 2>/dev/null || echo 0)"
|
||||
(
|
||||
while kill -0 "$PID" 2>/dev/null; do
|
||||
size="$(wc -c < "$LOG" 2>/dev/null || echo 0)"
|
||||
[ "$size" -lt "$offset" ] && offset=0
|
||||
if [ -r "$LOG" ] && tail -c "+$((offset + 1))" "$LOG" 2>/dev/null | grep -Fq "$READY"; then
|
||||
[ "$(cat "$STATE" 2>/dev/null)" = "$PID starting" ] && mark_state ready
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
) >/dev/null 2>&1 &
|
||||
watcher_pid=$!
|
||||
}
|
||||
|
||||
stop_watcher() {
|
||||
[ -n "$watcher_pid" ] || return 0
|
||||
kill "$watcher_pid" 2>/dev/null
|
||||
wait "$watcher_pid" 2>/dev/null
|
||||
watcher_pid=
|
||||
}
|
||||
|
||||
mark_state starting
|
||||
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: stale network state could not be removed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
watch_ready
|
||||
|
||||
# Keep daed as a child so a panic or unexpected exit is followed by an
|
||||
# immediate teardown of all kernel/runtime state before procd can respawn us.
|
||||
child_pid=
|
||||
term_requested=0
|
||||
cleanup_child() {
|
||||
[ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null
|
||||
term_requested=1
|
||||
[ -n "$child_pid" ] || return 0
|
||||
kill "$child_pid" 2>/dev/null
|
||||
# Do not let an interrupted wait leave a daed child behind. Four one-second
|
||||
# polls give a graceful TERM about four seconds before the bounded KILL.
|
||||
local attempt=0
|
||||
while kill -0 "$child_pid" 2>/dev/null && [ "$attempt" -lt 4 ]; do
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
if kill -0 "$child_pid" 2>/dev/null; then
|
||||
kill -9 "$child_pid" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
trap cleanup_child TERM INT
|
||||
|
||||
@@ -19,8 +71,21 @@ trap cleanup_child TERM INT
|
||||
child_pid=$!
|
||||
wait "$child_pid"
|
||||
status=$?
|
||||
# A signal can interrupt ash's wait even though the trap has already asked the
|
||||
# child to exit. Confirm the process is gone and reap it a second time before
|
||||
# publishing stopping or running cleanup.
|
||||
if [ "$term_requested" = 1 ]; then
|
||||
cleanup_child
|
||||
wait "$child_pid" 2>/dev/null
|
||||
reap_status=$?
|
||||
[ "$status" -eq 0 ] && status="$reap_status"
|
||||
fi
|
||||
child_pid=
|
||||
trap - TERM INT
|
||||
# The watcher may have observed the marker at the same time as the child
|
||||
# exited. Reap it before publishing stopping so it cannot overwrite that state.
|
||||
stop_watcher
|
||||
mark_state stopping
|
||||
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
|
||||
@@ -21,7 +21,6 @@ start_service() {
|
||||
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "5"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
@@ -45,6 +44,9 @@ start_service() {
|
||||
stop_service() {
|
||||
rm -f "$LOG"
|
||||
daed_cleanup_runtime
|
||||
local status=$?
|
||||
rm -f /var/run/daede-daed.ready /var/run/daede-daed.ready.*
|
||||
return "$status"
|
||||
}
|
||||
|
||||
restart() {
|
||||
|
||||
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=automount
|
||||
PKG_VERSION:=1
|
||||
PKG_RELEASE:=40
|
||||
PKG_RELEASE:=41
|
||||
PKG_ARCH:=all
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
blkdev=`dirname $DEVPATH`
|
||||
basename=`basename $blkdev`
|
||||
device=`basename $DEVPATH`
|
||||
|
||||
grep -qs "^/dev/$device " /proc/mounts && exit 0
|
||||
|
||||
skip=`block info | grep -vE '(f2fs|kernel|squashfs)' | sed 's/\(.*\): .*/\1/' | grep -q $device ; echo $?`
|
||||
path=$DEVPATH
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.14.7
|
||||
PKG_RELEASE:=25
|
||||
PKG_VERSION:=1.15
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -78,6 +78,7 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-backup.sh $(1)/usr/share/luci-app-daede/config-backup.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/snapshot-file.sh $(1)/usr/share/luci-app-daede/snapshot-file.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-defaults.sh $(1)/usr/share/luci-app-daede/config-defaults.sh
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede/defaults
|
||||
$(INSTALL_DATA) $(CURDIR)/../dae/files/dae.config $(1)/usr/share/luci-app-daede/defaults/dae
|
||||
|
||||
@@ -23,7 +23,9 @@ const BACKENDS = {
|
||||
pkg: 'daed',
|
||||
hasWebUI: true,
|
||||
useNetns: true,
|
||||
defaultListen: '0.0.0.0:2023'
|
||||
defaultListen: '0.0.0.0:2023',
|
||||
readyState: '/var/run/daede-daed.ready',
|
||||
readyPattern: /Reload: Finished/
|
||||
},
|
||||
dae: {
|
||||
name: 'dae',
|
||||
@@ -34,7 +36,9 @@ const BACKENDS = {
|
||||
pkg: 'dae',
|
||||
config: '/etc/dae/config.dae',
|
||||
hasWebUI: false,
|
||||
useNetns: false
|
||||
useNetns: false,
|
||||
readyState: '/var/run/daede-dae.ready',
|
||||
readyPattern: /Total startup time:/
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,6 +68,56 @@ function serviceStatus(name) {
|
||||
});
|
||||
}
|
||||
|
||||
function serviceLogOffset(be) {
|
||||
if (!be || !be.log)
|
||||
return Promise.resolve(0);
|
||||
|
||||
return L.resolveDefault(fs.read_direct(be.log, 'text'), '').then(function(content) {
|
||||
return String(content || '').length;
|
||||
});
|
||||
}
|
||||
|
||||
function serviceReady(be, afterOffset, pid) {
|
||||
if (!be || !be.log || !be.readyPattern)
|
||||
return Promise.resolve(false);
|
||||
|
||||
const expectedPid = Number(pid) || 0;
|
||||
const notFound = function(error) {
|
||||
const code = String(error && (error.code || error.errno || error.name) || '').toUpperCase();
|
||||
const message = String(error && (error.message || error) || '').toUpperCase();
|
||||
return code === 'ENOENT' || code === '-2' || code === 'NOTFOUNDERROR' ||
|
||||
/ENOENT|NO SUCH FILE|RESOURCE NOT FOUND/.test(message);
|
||||
};
|
||||
const readState = be.readyState
|
||||
? fs.stat(be.readyState).then(function(stat) {
|
||||
// A successful stat makes the state file authoritative. A subsequent
|
||||
// read error must fail closed instead of falling back to an old log line.
|
||||
if (!stat)
|
||||
return { present: true, error: true };
|
||||
return fs.read_direct(be.readyState, 'text').then(function(value) {
|
||||
return { present: true, value: String(value || '').trim() };
|
||||
}, function() {
|
||||
return { present: true, error: true };
|
||||
});
|
||||
}, function(error) {
|
||||
return notFound(error) ? { present: false } : { present: true, error: true };
|
||||
})
|
||||
: Promise.resolve({ present: true, error: true });
|
||||
|
||||
return readState.then(function(result) {
|
||||
if (result.present)
|
||||
return !result.error && expectedPid > 0 && result.value === expectedPid + ' ready';
|
||||
|
||||
// Compatibility fallback for a luci-app upgrade before the matching dae or
|
||||
// daed package. It is allowed only when stat explicitly reported ENOENT.
|
||||
return L.resolveDefault(fs.read_direct(be.log, 'text'), '').then(function(content) {
|
||||
const text = String(content || '');
|
||||
const offset = Number(afterOffset) || 0;
|
||||
return be.readyPattern.test(text.slice(text.length < offset ? 0 : offset));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function detectInstalledBackends() {
|
||||
return Promise.all([
|
||||
exists(BACKENDS.dae.initd),
|
||||
@@ -136,5 +190,7 @@ return baseclass.extend({
|
||||
detectRunning: detectRunning,
|
||||
detectBackend: detectBackend,
|
||||
setActiveBackend: setActiveBackend,
|
||||
serviceStatus: serviceStatus
|
||||
serviceStatus: serviceStatus,
|
||||
serviceLogOffset: serviceLogOffset,
|
||||
serviceReady: serviceReady
|
||||
});
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
|
||||
const GENERATOR = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
||||
const SNAPSHOT_HELPER = '/usr/share/luci-app-daede/snapshot-file.sh';
|
||||
const SUB_STAGE = '/tmp/daede-sub.txt';
|
||||
const FETCH_CHUNK_BYTES = 16384;
|
||||
|
||||
@@ -651,11 +652,19 @@ return view.extend({
|
||||
const name = airportName.value.trim();
|
||||
const groupName = airportSync.backendGroupName(name, 'daed', airportId);
|
||||
const endpoint = daedEndpoint();
|
||||
// daed can't read file:// — it HTTP-fetches a subscription link. Stage
|
||||
// the converted links (base64) and let daed pull them from the
|
||||
// loopback-only CGI, so the import lands as ONE subscription.
|
||||
const subToken = airportSync.backendId(airportId) + Date.now().toString(36);
|
||||
const stageFile = '/tmp/daede-daedsub-' + subToken;
|
||||
// daed can't read file:// — it HTTP-fetches a subscription link. Persist
|
||||
// the converted snapshot and let daed pull it from the loopback-only CGI,
|
||||
// so later daed refreshes and configuration backups keep working.
|
||||
// Keep the CGI token opaque, bounded, and unique across imports started in
|
||||
// the same millisecond. The final sanitization also protects the filename
|
||||
// and the CGI's allow-list if backendId ever changes.
|
||||
const tokenTime = Date.now().toString(36);
|
||||
const tokenRandom = Math.random().toString(36).slice(2, 12);
|
||||
const tokenPrefix = airportSync.backendId(airportId).replace(/[^A-Za-z0-9]/g, '')
|
||||
.slice(0, Math.max(0, 64 - tokenTime.length - tokenRandom.length));
|
||||
const subToken = (tokenPrefix + tokenTime + tokenRandom).slice(0, 64);
|
||||
const snapshotFile = '/etc/daed/daede-sub-' + subToken;
|
||||
const stageFile = '/etc/daed/.daede-sub-stage-' + subToken;
|
||||
const subUrl = 'http://127.0.0.1/cgi-bin/daede-sub?t=' + subToken;
|
||||
const b64 = btoa(items.map(function(item) { return item.link; }).join('\n') + '\n');
|
||||
|
||||
@@ -664,15 +673,156 @@ return view.extend({
|
||||
let createdGroupId = '';
|
||||
const createdNodeIds = [];
|
||||
let groupReady = false;
|
||||
let snapshotCommitted = false;
|
||||
let snapshotFinalToken = '';
|
||||
let reuseExistingSnapshot = false;
|
||||
const oldSubId = existingAirport ? existingAirport.subscription_id : '';
|
||||
const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
|
||||
|
||||
const dropStage = function() { return fs.exec('/bin/rm', [ '-f', stageFile ]).catch(function() {}); };
|
||||
const dropSnapshot = function(path) {
|
||||
if (!path)
|
||||
return Promise.resolve();
|
||||
return fs.remove(path).catch(function(error) {
|
||||
const message = String(error && (error.message || error) || '');
|
||||
if (/not found|no such file|enoent/i.test(message))
|
||||
return;
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
const appendWarning = function(error, warning) {
|
||||
const result = error instanceof Error ? error : new Error(String(error && (error.message || error) || error));
|
||||
result.message = result.message + '; ' + warning;
|
||||
return result;
|
||||
};
|
||||
const snapshotAction = function(args) {
|
||||
return fs.exec(SNAPSHOT_HELPER, args).then(function(res) {
|
||||
if (!res || res.code !== 0)
|
||||
throw new Error((res && (res.stderr || res.stdout)) || _('Snapshot operation failed'));
|
||||
return res;
|
||||
});
|
||||
};
|
||||
const cleanupStage = function() {
|
||||
return snapshotAction([ 'discard', subToken ]);
|
||||
};
|
||||
const querySubscriptions = function() {
|
||||
if (!token)
|
||||
return Promise.reject(new Error(_('daed subscription cleanup could not be verified')));
|
||||
return graphQL(endpoint, 'query SubscriptionLinks{subscriptions{id link}}', {}, token).then(function(value) {
|
||||
if (!value || !Array.isArray(value.subscriptions))
|
||||
throw new Error(_('daed returned an invalid subscription list'));
|
||||
return value.subscriptions;
|
||||
});
|
||||
};
|
||||
const dropSnapshotIfUnreferenced = function() {
|
||||
if (snapshotFinalToken !== subToken)
|
||||
return Promise.resolve();
|
||||
return querySubscriptions().then(function(subscriptions) {
|
||||
if (subscriptions.some(function(sub) { return String(sub.link || '') === subUrl; }))
|
||||
throw new Error(_('new daed subscription snapshot is still referenced; it was kept'));
|
||||
return dropSnapshot(snapshotFile);
|
||||
});
|
||||
};
|
||||
const finishWithSnapshotCleanup = function(result) {
|
||||
return dropSnapshotIfUnreferenced().then(function() {
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
const warning = String(error && (error.message || error) || error);
|
||||
result.warning = result.warning
|
||||
? result.warning + '; ' + warning
|
||||
: warning;
|
||||
return result;
|
||||
});
|
||||
};
|
||||
const updateExistingSubscription = function() {
|
||||
const old = (((before || {}).subscriptions) || []).find(function(sub) { return sub.id === oldSubId; });
|
||||
if (!old)
|
||||
return Promise.reject(new Error(_('Managed subscription no longer exists in daed')));
|
||||
|
||||
const oldLink = String(old.link || '');
|
||||
const localToken = oldLink.match(/^http:\/\/127\.0\.0\.1\/cgi-bin\/daede-sub\?t=([A-Za-z0-9]{1,64})$/);
|
||||
const refresh = function() {
|
||||
return graphQL(endpoint, 'mutation UpdateSub($id:ID!){updateSubscription(id:$id){id}}', { id: oldSubId }, token);
|
||||
};
|
||||
const currentLink = function() {
|
||||
return querySubscriptions().then(function(subscriptions) {
|
||||
const current = subscriptions.find(function(sub) { return sub.id === oldSubId; });
|
||||
return current ? String(current.link || '') : '';
|
||||
});
|
||||
};
|
||||
const restoreOldLink = function(originalError) {
|
||||
return graphQL(endpoint, 'mutation RestoreLink($id:ID!,$link:String!){updateSubscriptionLink(id:$id,link:$link)}', {
|
||||
id: oldSubId,
|
||||
link: oldLink
|
||||
}, token).then(refresh).then(currentLink).then(function(link) {
|
||||
if (link !== oldLink) {
|
||||
snapshotCommitted = true;
|
||||
throw appendWarning(originalError, _('rollback was not confirmed; both subscription snapshots were kept'));
|
||||
}
|
||||
throw originalError;
|
||||
}, function(rollbackError) {
|
||||
snapshotCommitted = true;
|
||||
throw appendWarning(originalError, _('rollback could not be confirmed; both subscription snapshots were kept: %s').format(String(rollbackError && (rollbackError.message || rollbackError) || rollbackError)));
|
||||
});
|
||||
};
|
||||
const finish = function() {
|
||||
const tag = old.tag !== groupName
|
||||
? graphQL(endpoint, 'mutation TagSub($id:ID!,$tag:String!){tagSubscription(id:$id,tag:$tag)}', {
|
||||
id: oldSubId,
|
||||
tag: groupName
|
||||
}, token).catch(function(error) {
|
||||
return String(error && (error.message || error) || error);
|
||||
})
|
||||
: Promise.resolve('');
|
||||
return tag.then(function(tagWarning) {
|
||||
writeAirportRecord(existingAirport, {
|
||||
id: airportId,
|
||||
backend: 'daed',
|
||||
name: name,
|
||||
sourceHash: state.sourceHash,
|
||||
groupId: existingAirport.group_id,
|
||||
subscriptionId: oldSubId,
|
||||
nodeIds: []
|
||||
});
|
||||
return applyUciChanges().then(function() {
|
||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||
return {
|
||||
added: items.length,
|
||||
duplicates: 0,
|
||||
failed: 0,
|
||||
warning: tagWarning
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (localToken && reuseExistingSnapshot) {
|
||||
return graphQL(endpoint, 'mutation UpdateSub($id:ID!){updateSubscription(id:$id){id}}', { id: oldSubId }, token)
|
||||
.then(currentLink).then(function(link) {
|
||||
if (link !== oldLink)
|
||||
throw new Error(_('daed did not confirm the existing subscription link after refresh'));
|
||||
}).catch(function(error) {
|
||||
throw appendWarning(error, _('daed subscription refresh failed; the new snapshot remains at the existing link and existing nodes were kept'));
|
||||
}).then(finish);
|
||||
}
|
||||
|
||||
return graphQL(endpoint, 'mutation SetLink($id:ID!,$link:String!){updateSubscriptionLink(id:$id,link:$link)}', {
|
||||
id: oldSubId,
|
||||
link: subUrl
|
||||
}, token).then(refresh).then(function() {
|
||||
return currentLink().then(function(link) {
|
||||
if (link !== subUrl)
|
||||
throw new Error(_('daed did not confirm the new subscription link'));
|
||||
});
|
||||
}).catch(restoreOldLink).then(function() {
|
||||
snapshotCommitted = true;
|
||||
return finish();
|
||||
});
|
||||
};
|
||||
const loadState = function(forceLogin) {
|
||||
return requestDaedToken(endpoint, forceLogin).then(function(auth) {
|
||||
token = auth.token;
|
||||
usedCachedToken = auth.cached;
|
||||
return graphQL(endpoint, 'query State{nodes(first:10000){edges{id link tag}} groups{id name nodes{id}}}', {}, token);
|
||||
return graphQL(endpoint, 'query State{nodes(first:10000){edges{id link tag}} groups{id name nodes{id}} subscriptions{id link tag}}', {}, token);
|
||||
});
|
||||
};
|
||||
const importDirectNodes = function() {
|
||||
@@ -797,7 +947,7 @@ return view.extend({
|
||||
});
|
||||
};
|
||||
|
||||
return fs.write(stageFile, b64).then(function() {
|
||||
return fs.write(stageFile, b64, 384).then(function() {
|
||||
return loadState(false).catch(function(error) {
|
||||
if (!usedCachedToken || !daedSession.isAccessDenied(error))
|
||||
throw error;
|
||||
@@ -806,42 +956,63 @@ return view.extend({
|
||||
});
|
||||
}).then(function(dataValue) {
|
||||
before = dataValue;
|
||||
// drop this airport's previous subscription first: the new one
|
||||
// reuses the same tag (group name) and daed enforces unique tags.
|
||||
if (!oldSubId)
|
||||
return null;
|
||||
return graphQL(endpoint, 'mutation RmSub($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ oldSubId ] }, token).catch(function() {});
|
||||
}).then(function() {
|
||||
// import the whole converted batch as one subscription
|
||||
return graphQL(endpoint,
|
||||
'mutation Import($a:ImportArgument!){importSubscription(rollbackError:false,arg:$a){sub{id} nodeImportResult{error}}}',
|
||||
{ a: { link: subUrl, tag: groupName } }, token).then(function(result) {
|
||||
const sub = result.importSubscription && result.importSubscription.sub;
|
||||
const rows = (result.importSubscription && result.importSubscription.nodeImportResult) || [];
|
||||
const usable = rows.length
|
||||
? rows.some(function(r) { return !r.error || r.error === 'node already exists'; })
|
||||
: !!(sub && sub.id);
|
||||
if (!sub || !sub.id || !usable) {
|
||||
const details = rows.map(function(r) { return r.error; }).filter(Boolean).join('; ');
|
||||
if (sub && sub.id)
|
||||
newSubId = sub.id;
|
||||
throw new Error(details || _('No usable nodes were imported'));
|
||||
}
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
if (daedSession.isAccessDenied(error))
|
||||
throw error;
|
||||
const cleanupSub = newSubId
|
||||
? graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token).catch(function() {})
|
||||
: Promise.resolve();
|
||||
newSubId = '';
|
||||
return cleanupSub.then(importDirectNodes).then(function(result) {
|
||||
return { directResult: result };
|
||||
});
|
||||
const old = (before.subscriptions || []).find(function(sub) { return sub.id === oldSubId; });
|
||||
const localToken = old && String(old.link || '').match(/^http:\/\/127\.0\.0\.1\/cgi-bin\/daede-sub\?t=([A-Za-z0-9]{1,64})$/);
|
||||
const oldLinkReferences = localToken
|
||||
? (before.subscriptions || []).filter(function(sub) { return String(sub.link || '') === String(old.link || ''); })
|
||||
: [];
|
||||
reuseExistingSnapshot = !!(localToken && oldLinkReferences.length === 1 && String(oldLinkReferences[0].id) === String(oldSubId));
|
||||
const publish = reuseExistingSnapshot
|
||||
? snapshotAction([ 'replace', localToken[1], subToken ]).then(function() {
|
||||
snapshotFinalToken = localToken[1];
|
||||
snapshotCommitted = true;
|
||||
})
|
||||
: snapshotAction([ 'publish', subToken ]).then(function() {
|
||||
snapshotFinalToken = subToken;
|
||||
});
|
||||
return publish.then(function() {
|
||||
if (oldSubId)
|
||||
return updateExistingSubscription().then(function(result) { return { existingResult: result }; });
|
||||
// import the whole converted batch as one subscription
|
||||
return graphQL(endpoint,
|
||||
'mutation Import($a:ImportArgument!){importSubscription(rollbackError:false,arg:$a){sub{id} nodeImportResult{error}}}',
|
||||
{ a: { link: subUrl, tag: groupName } }, token).then(function(result) {
|
||||
const sub = result.importSubscription && result.importSubscription.sub;
|
||||
const rows = (result.importSubscription && result.importSubscription.nodeImportResult) || [];
|
||||
const usable = rows.length
|
||||
? rows.some(function(r) { return !r.error || r.error === 'node already exists'; })
|
||||
: !!(sub && sub.id);
|
||||
if (!sub || !sub.id || !usable) {
|
||||
const details = rows.map(function(r) { return r.error; }).filter(Boolean).join('; ');
|
||||
if (sub && sub.id)
|
||||
newSubId = sub.id;
|
||||
throw new Error(details || _('No usable nodes were imported'));
|
||||
}
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
if (daedSession.isAccessDenied(error))
|
||||
throw error;
|
||||
const cleanupSub = newSubId
|
||||
? graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token).catch(function(cleanupError) {
|
||||
snapshotCommitted = true;
|
||||
throw appendWarning(error, _('rollback could not be confirmed; the new subscription snapshot was kept: %s').format(String(cleanupError && (cleanupError.message || cleanupError) || cleanupError)));
|
||||
})
|
||||
: Promise.resolve();
|
||||
newSubId = '';
|
||||
return cleanupSub.then(function() {
|
||||
if (oldSubId)
|
||||
throw error;
|
||||
return importDirectNodes();
|
||||
}).then(function(result) {
|
||||
return { directResult: result };
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(function(result) {
|
||||
if (result.existingResult)
|
||||
return result.existingResult;
|
||||
if (result.directResult)
|
||||
return result.directResult;
|
||||
return finishWithSnapshotCleanup(result.directResult);
|
||||
|
||||
const sub = result.importSubscription && result.importSubscription.sub;
|
||||
newSubId = sub.id;
|
||||
@@ -861,6 +1032,7 @@ return view.extend({
|
||||
return ensureGroup.then(function(groupId) {
|
||||
return graphQL(endpoint, 'mutation AddSubs($id:ID!,$ids:[ID!]!){groupAddSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: [ newSubId ] }, token).then(function() {
|
||||
groupReady = true;
|
||||
snapshotCommitted = true;
|
||||
const cleanup = [];
|
||||
// Migrate converter-managed airport groups to proxy, but never
|
||||
// disturb proxy's existing nodes or other subscriptions.
|
||||
@@ -880,7 +1052,12 @@ return view.extend({
|
||||
});
|
||||
return applyUciChanges().then(function() {
|
||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||
return { added: items.length, duplicates: 0, failed: failed };
|
||||
return {
|
||||
added: items.length,
|
||||
duplicates: 0,
|
||||
failed: failed,
|
||||
warning: ''
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -892,15 +1069,47 @@ return view.extend({
|
||||
throw error;
|
||||
const cleanup = [];
|
||||
if (newSubId)
|
||||
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token));
|
||||
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token).catch(function(cleanupError) {
|
||||
snapshotCommitted = true;
|
||||
throw cleanupError;
|
||||
}));
|
||||
if (createdNodeIds.length)
|
||||
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: createdNodeIds }, token));
|
||||
if (createdGroupId)
|
||||
cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token));
|
||||
return Promise.all(cleanup).catch(function() {}).then(function() { throw error; });
|
||||
}).finally(function() {
|
||||
token = '';
|
||||
return dropStage();
|
||||
return Promise.all(cleanup).then(function() {
|
||||
throw error;
|
||||
}, function(cleanupError) {
|
||||
snapshotCommitted = true;
|
||||
throw appendWarning(error, _('rollback could not be confirmed; the new subscription snapshot was kept: %s').format(String(cleanupError && (cleanupError.message || cleanupError) || cleanupError)));
|
||||
});
|
||||
}).then(function(result) {
|
||||
return cleanupStage().then(function() {
|
||||
token = '';
|
||||
return result;
|
||||
}, function(cleanupError) {
|
||||
token = '';
|
||||
result.warning = result.warning
|
||||
? result.warning + '; ' + String(cleanupError && (cleanupError.message || cleanupError) || cleanupError)
|
||||
: String(cleanupError && (cleanupError.message || cleanupError) || cleanupError);
|
||||
return result;
|
||||
});
|
||||
}, function(error) {
|
||||
let cleanupWarning = '';
|
||||
const removeSnapshot = snapshotCommitted
|
||||
? Promise.resolve()
|
||||
: dropSnapshotIfUnreferenced().catch(function(cleanupError) {
|
||||
cleanupWarning = _('rollback could not be confirmed; the new subscription snapshot was kept: %s').format(String(cleanupError && (cleanupError.message || cleanupError) || cleanupError));
|
||||
});
|
||||
return removeSnapshot.then(function() {
|
||||
return cleanupStage().catch(function(stageError) {
|
||||
const warning = _('snapshot stage cleanup failed: %s').format(String(stageError && (stageError.message || stageError) || stageError));
|
||||
cleanupWarning = cleanupWarning ? cleanupWarning + '; ' + warning : warning;
|
||||
});
|
||||
}).then(function() {
|
||||
token = '';
|
||||
throw cleanupWarning ? appendWarning(error, cleanupWarning) : error;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -924,8 +1133,11 @@ return view.extend({
|
||||
setImportStatus(_('Importing node group…'));
|
||||
const action = state.target === 'dae' ? importDae(items) : importDaed(items);
|
||||
action.then(function(result) {
|
||||
setImportStatus(_('Node group imported: added %d, reused %d, failed %d')
|
||||
.format(result.added, result.duplicates, result.failed), result.failed ? 'err' : 'ok');
|
||||
let message = _('Node group imported: added %d, reused %d, failed %d')
|
||||
.format(result.added, result.duplicates, result.failed);
|
||||
if (result.warning)
|
||||
message += ' ' + _('Completed with warning: %s').format(result.warning);
|
||||
setImportStatus(message, result.failed || result.warning ? 'err' : 'ok');
|
||||
renderResults();
|
||||
}).catch(function(e) {
|
||||
setImportStatus(_('Node group import failed: %s').format(e.message || e), 'err');
|
||||
|
||||
@@ -16,6 +16,7 @@ const CSS = [
|
||||
'.dd-status-row .dd-grow{flex:1 1 auto}',
|
||||
'.dd-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;font-size:10.5px;font-weight:700;letter-spacing:.3px;border:1px solid transparent;line-height:1.3}',
|
||||
'.dd-badge-run{color:#3da66a;border-color:rgba(61,166,106,.5)}',
|
||||
'.dd-badge-wait{color:#d6a23d;border-color:rgba(214,162,61,.55)}',
|
||||
'.dd-badge-stop{color:#d96d6d;border-color:rgba(217,109,109,.55)}',
|
||||
'.dd-badge-dot{width:6px;height:6px;border-radius:50%;background:currentColor;display:inline-block}',
|
||||
'.dd-meta{font-size:11.5px;opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}',
|
||||
|
||||
@@ -23,8 +23,7 @@ function execInit(be, action) {
|
||||
}
|
||||
|
||||
function restartService(be) {
|
||||
return execChecked(be.initd, ['restart'])
|
||||
.then(function() { return waitForService(be, true, 80); });
|
||||
return execChecked(be.initd, ['restart']);
|
||||
}
|
||||
|
||||
function rejectIfOtherRunning(be, running) {
|
||||
@@ -60,14 +59,26 @@ function toggleService(be, turnOn) {
|
||||
.then(function() { return execChecked(be.initd, [action]); });
|
||||
}
|
||||
|
||||
function waitForService(be, turnOn, attempts) {
|
||||
function waitForService(be, turnOn, attempts, readyAfter) {
|
||||
return backend.serviceStatus(be.name).then(function(state) {
|
||||
if (!!state.running === turnOn)
|
||||
if (!turnOn && !state.running)
|
||||
return state;
|
||||
if (turnOn && state.running)
|
||||
return backend.serviceReady(be, readyAfter, state.pid).then(function(ready) {
|
||||
if (ready) {
|
||||
state.ready = true;
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
}).then(function(state) {
|
||||
if (state)
|
||||
return state;
|
||||
if (attempts <= 0)
|
||||
throw new Error(turnOn ? _('Service did not start in time.') : _('Service did not stop in time.'));
|
||||
return new Promise(function(resolve) { setTimeout(resolve, 250); })
|
||||
.then(function() { return waitForService(be, turnOn, attempts - 1); });
|
||||
.then(function() { return waitForService(be, turnOn, attempts - 1, readyAfter); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +167,21 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
let busy = false;
|
||||
let lastError = '';
|
||||
let refreshGeneration = 0;
|
||||
const boundaryKey = 'daede.ready-boundary.' + window.location.host + '.' + be.name;
|
||||
let readyBoundary = null;
|
||||
try {
|
||||
const savedBoundary = window.localStorage.getItem(boundaryKey);
|
||||
if (savedBoundary !== null && /^\d+$/.test(savedBoundary))
|
||||
readyBoundary = Number(savedBoundary);
|
||||
} catch (e) {}
|
||||
const rememberBoundary = function(offset) {
|
||||
readyBoundary = Number(offset) || 0;
|
||||
try { window.localStorage.setItem(boundaryKey, String(readyBoundary)); } catch (e) {}
|
||||
};
|
||||
const clearBoundary = function() {
|
||||
readyBoundary = null;
|
||||
try { window.localStorage.removeItem(boundaryKey); } catch (e) {}
|
||||
};
|
||||
const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…')));
|
||||
const card = E('div', { 'class': 'dd-card dd-status-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Service Status')),
|
||||
@@ -165,7 +191,10 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
const render = function(state) {
|
||||
while (body.firstChild) body.removeChild(body.firstChild);
|
||||
|
||||
const badge = state.running
|
||||
const starting = !!state.starting;
|
||||
const badge = starting
|
||||
? E('span', { 'class': 'dd-badge dd-badge-wait' }, [ E('span', { 'class': 'dd-badge-dot' }), _('Starting…') ])
|
||||
: state.running
|
||||
? E('span', { 'class': 'dd-badge dd-badge-run' }, [ E('span', { 'class': 'dd-badge-dot' }), _('RUNNING') ])
|
||||
: E('span', { 'class': 'dd-badge dd-badge-stop' }, [ E('span', { 'class': 'dd-badge-dot' }), _('STOPPED') ]);
|
||||
|
||||
@@ -177,7 +206,12 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
meta.push(E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, 'PID'), state.pid ]));
|
||||
|
||||
const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': lastError ? '' : 'display:none' }, lastError);
|
||||
const sw = E('button', { 'class': 'dd-switch' + (state.running ? ' is-on' : ''), 'type': 'button', 'aria-label': _('Toggle service') }, [
|
||||
const sw = E('button', {
|
||||
'class': 'dd-switch' + (state.running ? ' is-on' : ''),
|
||||
'type': 'button',
|
||||
'aria-label': _('Toggle service'),
|
||||
'disabled': busy ? '' : null
|
||||
}, [
|
||||
E('span', { 'class': 'dd-switch-knob' })
|
||||
]);
|
||||
sw.addEventListener('click', function(ev) {
|
||||
@@ -185,21 +219,25 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
if (busy) return;
|
||||
refreshGeneration++;
|
||||
busy = true;
|
||||
sw.disabled = true;
|
||||
lastError = '';
|
||||
swErr.style.display = 'none';
|
||||
const turnOn = !state.running;
|
||||
/* instant optimistic feedback — the start/stop chain (esp. dae's eBPF
|
||||
load) takes a few seconds; flip the switch and show a pending label
|
||||
right away instead of looking frozen */
|
||||
sw.classList.toggle('is-on', turnOn);
|
||||
const lbl = sw.parentNode && sw.parentNode.querySelector('.dd-switch-label');
|
||||
if (lbl) lbl.textContent = '…';
|
||||
toggleService(be, turnOn)
|
||||
.then(function() { return waitForService(be, turnOn, 40); })
|
||||
.then(function() {
|
||||
busy = false;
|
||||
return refresh(true);
|
||||
/* Keep process liveness separate from data-plane readiness. The process
|
||||
appears in procd before dae has attached its rules, so show the real
|
||||
transition until the backend's final startup marker is logged. */
|
||||
render({ running: turnOn, pid: 0, starting: turnOn });
|
||||
if (!turnOn)
|
||||
clearBoundary();
|
||||
const boundary = turnOn ? backend.serviceLogOffset(be) : Promise.resolve(0);
|
||||
boundary.then(function(offset) {
|
||||
if (turnOn)
|
||||
rememberBoundary(offset);
|
||||
return toggleService(be, turnOn)
|
||||
.then(function() { return waitForService(be, turnOn, turnOn ? 240 : 40, offset); });
|
||||
})
|
||||
.then(function() {
|
||||
busy = false;
|
||||
clearBoundary();
|
||||
return refresh(true);
|
||||
})
|
||||
.catch(function(e) {
|
||||
busy = false;
|
||||
@@ -214,7 +252,7 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
E('span', { 'class': 'dd-grow' }),
|
||||
swErr,
|
||||
E('span', { 'class': 'dd-switch-wrap' }, [
|
||||
E('span', { 'class': 'dd-switch-label' }, state.running ? 'ON' : 'OFF'),
|
||||
E('span', { 'class': 'dd-switch-label' }, starting ? _('Starting…') : (state.running ? 'ON' : 'OFF')),
|
||||
sw
|
||||
])
|
||||
]);
|
||||
@@ -223,7 +261,7 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
body.appendChild(E('div', { 'class': 'dd-status-meta' }, meta));
|
||||
|
||||
const actions = [];
|
||||
if (be.hasWebUI && state.running) {
|
||||
if (be.hasWebUI && state.running && !starting) {
|
||||
const port = (listenAddr || be.defaultListen).split(':').slice(-1)[0];
|
||||
actions.push(E('a', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
@@ -232,19 +270,23 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
'rel': 'noreferrer noopener'
|
||||
}, _('Open WebUI')));
|
||||
}
|
||||
if (state.running) {
|
||||
if (state.running && !starting) {
|
||||
const restart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Restart'));
|
||||
restart.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
restart.disabled = true;
|
||||
lastError = '';
|
||||
swErr.style.display = 'none';
|
||||
restartService(be)
|
||||
.then(function() {
|
||||
busy = false;
|
||||
return refresh(true);
|
||||
render({ running: true, pid: 0, starting: true });
|
||||
backend.serviceLogOffset(be).then(function(offset) {
|
||||
rememberBoundary(offset);
|
||||
return restartService(be)
|
||||
.then(function() { return waitForService(be, true, 240, offset); });
|
||||
})
|
||||
.then(function() {
|
||||
busy = false;
|
||||
clearBoundary();
|
||||
return refresh(true);
|
||||
})
|
||||
.catch(function(e) {
|
||||
busy = false;
|
||||
@@ -254,7 +296,7 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
});
|
||||
actions.push(restart);
|
||||
}
|
||||
if (state.running && be.name === 'dae') {
|
||||
if (state.running && !starting && be.name === 'dae') {
|
||||
const hot = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Hot Reload'));
|
||||
hot.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
@@ -263,7 +305,7 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
});
|
||||
actions.push(hot);
|
||||
}
|
||||
if (state.running && be.name === 'dae') {
|
||||
if (state.running && !starting && be.name === 'dae') {
|
||||
const ckBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Test YouTube'));
|
||||
const ckRes = E('span', { 'class': 'dd-meta', style: 'margin-left:8px;display:none' }, '');
|
||||
ckBtn.addEventListener('click', function(ev) {
|
||||
@@ -308,7 +350,18 @@ function renderStatusCard(ctx, listenAddr) {
|
||||
const generation = refreshGeneration;
|
||||
return backend.serviceStatus(be.name).then(function(state) {
|
||||
if (generation !== refreshGeneration || (busy && !force)) return;
|
||||
render(state);
|
||||
if (!state.running) {
|
||||
clearBoundary();
|
||||
render(state);
|
||||
return;
|
||||
}
|
||||
return backend.serviceReady(be, readyBoundary, state.pid).then(function(ready) {
|
||||
if (generation !== refreshGeneration || (busy && !force)) return;
|
||||
state.starting = !ready;
|
||||
if (ready)
|
||||
clearBoundary();
|
||||
render(state);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -812,6 +812,12 @@ msgstr ""
|
||||
msgid "Node group imported: added %d, reused %d, failed %d"
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed with warning: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Managed subscription no longer exists in daed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Node group import failed: %s"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1004,6 +1004,12 @@ msgstr "正在导入节点组…"
|
||||
msgid "Node group imported: added %d, reused %d, failed %d"
|
||||
msgstr "节点组导入完成:新增 %d,复用 %d,失败 %d"
|
||||
|
||||
msgid "Completed with warning: %s"
|
||||
msgstr "已完成,但有警告:%s"
|
||||
|
||||
msgid "Managed subscription no longer exists in daed"
|
||||
msgstr "daed 中已不存在该托管订阅"
|
||||
|
||||
msgid "Node group import failed: %s"
|
||||
msgstr "节点组导入失败:%s"
|
||||
|
||||
|
||||
@@ -1004,6 +1004,12 @@ msgstr "正在导入节点组…"
|
||||
msgid "Node group imported: added %d, reused %d, failed %d"
|
||||
msgstr "节点组导入完成:新增 %d,复用 %d,失败 %d"
|
||||
|
||||
msgid "Completed with warning: %s"
|
||||
msgstr "已完成,但有警告:%s"
|
||||
|
||||
msgid "Managed subscription no longer exists in daed"
|
||||
msgstr "daed 中已不存在该托管订阅"
|
||||
|
||||
msgid "Node group import failed: %s"
|
||||
msgstr "节点组导入失败:%s"
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ paths() {
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac
|
||||
printf 'etc/dae/subscriptions/%s\n' "$name"
|
||||
done
|
||||
for sub in "$1"/etc/daed/daede-sub-*; do
|
||||
[ -e "$sub" ] || [ -L "$sub" ] || continue
|
||||
name="${sub##*/daede-sub-}"
|
||||
case "$name" in ''|*[!A-Za-z0-9]*) fail 'unexpected daed subscription filename' ;; esac
|
||||
printf 'etc/daed/daede-sub-%s\n' "$name"
|
||||
done
|
||||
}
|
||||
check_paths() {
|
||||
for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do
|
||||
@@ -42,6 +48,14 @@ check_paths() {
|
||||
[ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p"
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
cleanup_stages() {
|
||||
for stage in /etc/daed/.daede-sub-stage-*; do
|
||||
[ -e "$stage" ] || [ -L "$stage" ] || continue
|
||||
[ ! -L "$stage" ] || fail "refusing symlink: $stage"
|
||||
[ -f "$stage" ] || fail "not a regular file: $stage"
|
||||
rm -f "$stage" || fail "failed to remove stale snapshot stage: $stage"
|
||||
done
|
||||
}
|
||||
snapshot() {
|
||||
mkdir -p "$WORK/before"
|
||||
while IFS= read -r p; do
|
||||
@@ -139,6 +153,9 @@ validate_archive() {
|
||||
etc/dae/subscriptions/*.sub)
|
||||
name="${p#etc/dae/subscriptions/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'invalid subscription path' ;; esac ;;
|
||||
etc/daed/daede-sub-*)
|
||||
name="${p#etc/daed/daede-sub-}"
|
||||
case "$name" in ''|*[!A-Za-z0-9]*) fail 'invalid daed subscription path' ;; esac ;;
|
||||
*) fail 'unexpected archive entry' ;;
|
||||
esac
|
||||
done < "$WORK/entries"
|
||||
@@ -175,6 +192,7 @@ run_action() {
|
||||
fi
|
||||
fi
|
||||
stop_backends
|
||||
[ "$ACTION" = reset ] && cleanup_stages
|
||||
snapshot
|
||||
if [ "$ACTION" = export ]; then
|
||||
paths "$WORK/before" > "$WORK/candidates"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
# Publish converter snapshots without exposing a partially written final file.
|
||||
|
||||
set -eu
|
||||
|
||||
DIR=/etc/daed
|
||||
PREFIX=daede-sub
|
||||
STAGE_PREFIX=.daede-sub-stage
|
||||
|
||||
fail() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
valid_token() {
|
||||
[ "$#" -eq 1 ] || return 1
|
||||
case "$1" in
|
||||
''|*[!A-Za-z0-9]*) return 1 ;;
|
||||
esac
|
||||
[ "${#1}" -le 64 ]
|
||||
}
|
||||
|
||||
stage_path() { printf '%s/%s-%s\n' "$DIR" "$STAGE_PREFIX" "$1"; }
|
||||
final_path() { printf '%s/%s-%s\n' "$DIR" "$PREFIX" "$1"; }
|
||||
|
||||
regular_file() {
|
||||
[ -f "$1" ] && [ ! -L "$1" ]
|
||||
}
|
||||
|
||||
safe_dir() {
|
||||
[ -d "$DIR" ] && [ ! -L "$DIR" ] || fail "invalid snapshot directory"
|
||||
}
|
||||
|
||||
publish() {
|
||||
[ "$#" -eq 1 ] || fail 'publish expects one token'
|
||||
valid_token "$1" || fail 'invalid snapshot token'
|
||||
safe_dir
|
||||
stage=$(stage_path "$1")
|
||||
final=$(final_path "$1")
|
||||
regular_file "$stage" || fail 'snapshot stage is not a regular file'
|
||||
[ ! -e "$final" ] && [ ! -L "$final" ] || fail 'snapshot already exists'
|
||||
chmod 600 "$stage" || fail 'cannot secure snapshot stage'
|
||||
mv -f "$stage" "$final" || fail 'cannot publish snapshot'
|
||||
}
|
||||
|
||||
replace() {
|
||||
[ "$#" -eq 2 ] || fail 'replace expects existing and stage tokens'
|
||||
valid_token "$1" || fail 'invalid existing snapshot token'
|
||||
valid_token "$2" || fail 'invalid stage snapshot token'
|
||||
safe_dir
|
||||
[ "$1" != "$2" ] || fail 'existing and stage tokens must differ'
|
||||
stage=$(stage_path "$2")
|
||||
final=$(final_path "$1")
|
||||
regular_file "$stage" || fail 'snapshot stage is not a regular file'
|
||||
if [ -e "$final" ] || [ -L "$final" ]; then
|
||||
regular_file "$final" || fail 'existing snapshot is not a regular file'
|
||||
fi
|
||||
chmod 600 "$stage" || fail 'cannot secure snapshot stage'
|
||||
mv -f "$stage" "$final" || fail 'cannot replace snapshot'
|
||||
}
|
||||
|
||||
discard() {
|
||||
[ "$#" -eq 1 ] || fail 'discard expects one token'
|
||||
valid_token "$1" || fail 'invalid snapshot token'
|
||||
safe_dir
|
||||
stage=$(stage_path "$1")
|
||||
if [ -L "$stage" ]; then
|
||||
fail 'refusing symlink snapshot stage'
|
||||
fi
|
||||
if [ -e "$stage" ]; then
|
||||
[ -f "$stage" ] || fail 'snapshot stage is not a regular file'
|
||||
rm -f "$stage" || fail 'cannot discard snapshot stage'
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
publish) shift; publish "$@" ;;
|
||||
replace) shift; replace "$@" ;;
|
||||
discard) shift; discard "$@" ;;
|
||||
*) fail 'unknown snapshot action' ;;
|
||||
esac
|
||||
@@ -10,6 +10,8 @@
|
||||
"/etc/dae/example.dae": [ "read" ],
|
||||
"/var/log/dae/dae.log": [ "read" ],
|
||||
"/var/log/daed/daed.log": [ "read" ],
|
||||
"/var/run/daede-dae.ready": [ "read" ],
|
||||
"/var/run/daede-daed.ready": [ "read" ],
|
||||
"/usr/share/v2ray/geoip.dat": [ "read" ],
|
||||
"/usr/share/v2ray/geosite.dat": [ "read" ],
|
||||
"/sys/kernel/btf/vmlinux": [ "read" ],
|
||||
@@ -43,9 +45,12 @@
|
||||
"/tmp/dae-validate.dae": [ "write" ],
|
||||
"/tmp/daede-import.b64": [ "write" ],
|
||||
"/tmp/daede-sub.txt": [ "write" ],
|
||||
"/tmp/daede-daedsub-*": [ "write" ],
|
||||
"/bin/rm -f /tmp/daede-daedsub-*": [ "exec" ],
|
||||
"/etc/daed/.daede-sub-stage-*": [ "write" ],
|
||||
"/etc/daed/daede-sub-*": [ "write" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh publish *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh replace * *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh discard *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh write-sub *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh delete-sub *": [ "exec" ],
|
||||
"/etc/config/daede": [ "write" ],
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# daede-sub - serve a converter's staged node list to the local daed only.
|
||||
#
|
||||
# daed (dae-wing) can only build a subscription by HTTP-fetching a link, and it
|
||||
# does not read file:// like dae core. So the converter stages the base64 share
|
||||
# links in /tmp/daede-daedsub-<token> and points daed's importSubscription at
|
||||
# does not read file:// like dae core. So the converter stores the base64 share
|
||||
# links in /etc/daed/daede-sub-<token> and points daed's importSubscription at
|
||||
# http://127.0.0.1/cgi-bin/daede-sub?t=<token>. This CGI is the read side.
|
||||
#
|
||||
# Locked to loopback (REMOTE_ADDR), so even though uhttpd listens on the LAN no
|
||||
# remote client can pull the links. The frontend deletes the staged file after
|
||||
# the import (success or failure), so the exposure is one local fetch.
|
||||
# remote client can pull the links. The snapshot remains available for daed's
|
||||
# own refresh action and is included in daede configuration backups.
|
||||
|
||||
deny() { printf 'Status: 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nforbidden\n'; exit 0; }
|
||||
|
||||
@@ -18,7 +18,7 @@ deny() { printf 'Status: 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nforbid
|
||||
t=$(printf '%s' "$QUERY_STRING" | sed -n 's/^.*\bt=\([A-Za-z0-9]\{1,64\}\).*$/\1/p')
|
||||
[ -n "$t" ] || deny
|
||||
|
||||
f="/tmp/daede-daedsub-$t"
|
||||
f="/etc/daed/daede-sub-$t"
|
||||
[ -f "$f" ] || deny
|
||||
|
||||
printf 'Content-Type: text/plain\r\n\r\n'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (C) 2020 Gyj1109
|
||||
# 适配 OpenWrt 25.12 修改版
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-syncthing
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
LUCI_TITLE:=LuCI support for Syncthing
|
||||
LUCI_PKGARCH:=all
|
||||
# 适配 25.12:确保安装了 syncthing 后,LuCI 界面能正确拉起
|
||||
LUCI_DEPENDS:=+syncthing
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# 明确定义配置文件的归属,这对 25.12 的 apk 管理器非常重要
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/syncthing
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,2 @@
|
||||
# luci-app-syncthing
|
||||
在Potat0000源码基础上进行了汉化优化,官方openwrt23.05.3编译后可使用
|
||||
@@ -0,0 +1,17 @@
|
||||
module("luci.controller.syncthing", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/syncthing") then
|
||||
return
|
||||
end
|
||||
|
||||
entry({"admin", "services", "syncthing"}, cbi("syncthing"), _("文件同步"), 10).dependent = true
|
||||
entry({"admin", "services", "syncthing", "status"}, call("act_status")).leaf = true
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e = {}
|
||||
e.running = luci.sys.call("pgrep syncthing >/dev/null") == 0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
require("nixio.fs")
|
||||
|
||||
m = Map("syncthing", translate("Syncthing同步工具"))
|
||||
|
||||
m:section(SimpleSection).template = "syncthing/syncthing_status"
|
||||
|
||||
s = m:section(TypedSection, "syncthing")
|
||||
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(Flag, "enabled", translate("启用"))
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
|
||||
gui_address = s:option(Value, "gui_address", translate("GUI访问地址"))
|
||||
gui_address.description = translate("使用0.0.0.0以监控所有访问。")
|
||||
gui_address.default = "http://0.0.0.0:8384"
|
||||
gui_address.placeholder = "http://0.0.0.0:8384"
|
||||
gui_address.rmempty = false
|
||||
|
||||
home = s:option(Value, "home", translate("配置文件目录"))
|
||||
home.description = translate("只有保存在/etc/syncthing中的配置会自动备份!")
|
||||
home.default = "/etc/syncthing"
|
||||
home.placeholder = "/etc/syncthing"
|
||||
home.rmempty = false
|
||||
|
||||
user = s:option(ListValue, "user", translate("用户"))
|
||||
user.description = translate("默认是syncthing,但这可能会导致权限被拒绝。Syncthing官方不建议以root身份运行。")
|
||||
user:value("", translate("syncthing"))
|
||||
for u in luci.util.execi("cat /etc/passwd | cut -d ':' -f1") do
|
||||
user:value(u)
|
||||
end
|
||||
|
||||
macprocs = s:option(Value, "macprocs", translate("线程限制"))
|
||||
macprocs.description = translate("0表示匹配CPU数量(默认),>0表示显式指定并发数。")
|
||||
macprocs.default = "0"
|
||||
macprocs.placeholder = "0"
|
||||
macprocs.datatype = "range(0,32)"
|
||||
macprocs.rmempty = false
|
||||
|
||||
nice = s:option(Value, "nice", translate("优先级"))
|
||||
nice.description = translate("显式指定优先级值。0是最高,19是最低。(暂时不允许设置负值)")
|
||||
nice.default = "19"
|
||||
nice.placeholder = "19"
|
||||
nice.datatype = "range(0,19)"
|
||||
nice.rmempty = false
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,27 @@
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(1, '<%=url([[admin]], [[services]], [[syncthing]], [[status]])%>', null,
|
||||
function (x, data) {
|
||||
var tb = document.getElementById('syncthing_status');
|
||||
if (data && tb) {
|
||||
if (data.running) {
|
||||
var links = '<em><b><font color="green">Syncthing <%:运行中%></font></b></em><input class="btn cbi-button mar-10" type="button" value="<%:打开Syncthing页面%>" onclick="openwebui();" />';
|
||||
tb.innerHTML = links;
|
||||
} else {
|
||||
tb.innerHTML = '<em><b><font color="red">Syncthing <%:未运行%></font></b></em>';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function openwebui(){
|
||||
var url = window.location.host+":<%=luci.sys.exec("uci -q get syncthing.syncthing.gui_address"):match(":[0-9]+"):gsub(":", "")%>";
|
||||
window.open('http://'+url,'target','');
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
|
||||
<fieldset class="cbi-section">
|
||||
<p id="syncthing_status">
|
||||
<em><%:正在收集数据...%></em>
|
||||
</p>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
touch /etc/config/syncthing
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@syncthing[-1]
|
||||
add ucitrack syncthing
|
||||
set ucitrack.@syncthing[-1].exec='/etc/init.d/syncthing stop && /etc/init.d/syncthing start'
|
||||
commit ucitrack
|
||||
EOF
|
||||
# remove LuCI cache
|
||||
rm -f /tmp/luci*
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"luci-app-syncthing": {
|
||||
"description": "Grant UCI access for luci-app-syncthing",
|
||||
"read": {
|
||||
"uci": [ "syncthing" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "syncthing" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user