diff --git a/daed/Makefile b/daed/Makefile index b63419d0..77ea80e4 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed PKG_VERSION:=2026.07.20 -PKG_RELEASE:=26 +PKG_RELEASE:=27 PKG_SOURCE:=daed-src-2026.07.20-42db711ac8ce.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src diff --git a/daed/patches/0006-daed-strictly-reconcile-subscription-updates.patch b/daed/patches/0006-daed-strictly-reconcile-subscription-updates.patch new file mode 100644 index 00000000..e25462cf --- /dev/null +++ b/daed/patches/0006-daed-strictly-reconcile-subscription-updates.patch @@ -0,0 +1,619 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kenzok8 +Date: Tue, 21 Jul 2026 02:53:42 +0800 +Subject: [PATCH] daed: strictly reconcile subscription updates +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +订阅更新按节点链接精确对账,普通组里的失效节点删除,fixed 组固定节点转成独立节点保留。 + +数据库提交后按运行状态应用配置,补齐并发地址校验、导入失败回滚和 reload 超时恢复。 +--- + cmd/run.go | 6 +- + graphql/mutation.go | 9 +- + graphql/root_schema.go | 2 +- + graphql/service/config/mutation_utils.go | 59 ++++- + .../service/subscription/mutation_utils.go | 181 +++++++++++---- + .../subscription/mutation_utils_test.go | 212 ++++++++++++++++++ + 6 files changed, 408 insertions(+), 61 deletions(-) + create mode 100644 graphql/service/subscription/mutation_utils_test.go + +diff --git a/cmd/run.go b/cmd/run.go +index 7dce577..978f46c 100644 +--- a/cmd/run.go ++++ b/cmd/run.go +@@ -161,11 +161,8 @@ func restoreRunningState() (err error) { + if !reload { + return nil + } +- tx := db.BeginTx(context.TODO()) + // Reload. +- if _, err = config.Run(tx, false); err != nil { +- tx.Rollback() +- ++ if _, err = config.Run(context.TODO(), false); err != nil { + // Another tx. + // Set running = false. + tx2 := db.BeginTx(context.TODO()) +@@ -183,7 +180,6 @@ func restoreRunningState() (err error) { + tx2.Commit() + return err + } +- tx.Commit() + return nil + } + +diff --git a/graphql/mutation.go b/graphql/mutation.go +index fcbf440..adb747c 100644 +--- a/graphql/mutation.go ++++ b/graphql/mutation.go +@@ -256,14 +256,7 @@ func (r *MutationResolver) SelectConfig(args *struct { + func (r *MutationResolver) Run(args *struct { + Dry bool + }) (int32, error) { +- tx := db.BeginTx(context.TODO()) +- ret, err := config.Run(tx, args.Dry) +- if err != nil { +- tx.Rollback() +- return 0, err +- } +- tx.Commit() +- return ret, nil ++ return config.Run(context.TODO(), args.Dry) + } + + func (r *MutationResolver) CreateDns(args *struct { +diff --git a/graphql/root_schema.go b/graphql/root_schema.go +index 8b82db6..a96cace 100644 +--- a/graphql/root_schema.go ++++ b/graphql/root_schema.go +@@ -121,7 +121,7 @@ type Mutation { + # tagSubscription is to give the subscription a new tag. + tagSubscription(id: ID!, tag: String!): Int! @hasRole(role: ADMIN) + +- # updateSubscription is to re-fetch subscription and resolve subscription into nodes. Old nodes that independently belong to any groups will not be removed. ++ # updateSubscription re-fetches and strictly reconciles subscription nodes. Stale nodes pinned by fixed groups become independent nodes. + updateSubscription(id: ID!): Subscription! @hasRole(role: ADMIN) + + # updateSubscriptionLink is to update the subscription link without re-fetching nodes. +diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go +index fd61554..feb94d3 100644 +--- a/graphql/service/config/mutation_utils.go ++++ b/graphql/service/config/mutation_utils.go +@@ -21,6 +21,7 @@ import ( + "github.com/daeuniverse/dae-wing/dae" + "github.com/daeuniverse/dae-wing/db" + "github.com/daeuniverse/dae-wing/graphql/service/config/global" ++ "github.com/daeuniverse/dae-wing/graphql/service/general" + daeConfig "github.com/daeuniverse/dae/config" + "github.com/daeuniverse/dae/pkg/config_parser" + "github.com/graph-gophers/graphql-go" +@@ -269,21 +270,69 @@ func restartAfterReloadTimeout() { + }() + } + +-func Run(d *gorm.DB, noLoad bool) (n int32, err error) { ++func runTransaction(ctx context.Context, noLoad bool) (n int32, err error) { ++ tx := db.BeginTx(ctx) ++ if tx.Error != nil { ++ return 0, tx.Error ++ } ++ n, err = runLocked(tx, noLoad) ++ if err != nil { ++ tx.Rollback() ++ return 0, err ++ } ++ if err = tx.Commit().Error; err != nil { ++ return 0, err ++ } ++ return n, nil ++} ++ ++func Run(ctx context.Context, noLoad bool) (n int32, err error) { + if ok := runLock.TryLock(); !ok { + return 0, fmt.Errorf("the last request didn't complete; make a cup of tea and take a break") + } + defer runLock.Unlock() ++ return runTransaction(ctx, noLoad) ++} ++ ++func ApplyIfRunning(ctx context.Context) error { ++ runLock.Lock() ++ defer runLock.Unlock() ++ ++ daeResolver := general.DaeResolver{Ctx: ctx} ++ running, err := daeResolver.Running() ++ if err != nil || !running { ++ return err ++ } ++ modified, err := daeResolver.Modified() ++ if err != nil || !modified { ++ return err ++ } ++ _, err = runTransaction(ctx, false) ++ return err ++} ++ ++func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) { + //// Dry run. + if noLoad { + ch := make(chan error) +- dae.ChReloadConfigs <- &dae.ReloadMessage{ ++ reloadMsg := &dae.ReloadMessage{ + Config: dae.EmptyConfig, + Callback: ch, + } +- err = <-ch +- if err != nil { +- return 0, fmt.Errorf("failed to dryrun: %w; see more in log and report bugs", err) ++ select { ++ case dae.ChReloadConfigs <- reloadMsg: ++ case <-time.After(reloadWaitTimeout): ++ restartAfterReloadTimeout() ++ return 0, fmt.Errorf("failed to submit dryrun within %s; restarting daed to recover runtime", reloadWaitTimeout) ++ } ++ select { ++ case err = <-ch: ++ if err != nil { ++ return 0, fmt.Errorf("failed to dryrun: %w; see more in log and report bugs", err) ++ } ++ case <-time.After(reloadWaitTimeout): ++ restartAfterReloadTimeout() ++ return 0, fmt.Errorf("dryrun timed out after %s; restarting daed to recover runtime", reloadWaitTimeout) + } + + // Running -> false +diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go +index e55e65b..e01a342 100644 +--- a/graphql/service/subscription/mutation_utils.go ++++ b/graphql/service/subscription/mutation_utils.go +@@ -18,6 +18,7 @@ import ( + "github.com/daeuniverse/dae-wing/dae" + "github.com/daeuniverse/dae-wing/db" + "github.com/daeuniverse/dae-wing/graphql/internal" ++ "github.com/daeuniverse/dae-wing/graphql/service/config" + "github.com/daeuniverse/dae-wing/graphql/service/node" + "github.com/daeuniverse/dae/common/subscription" + "github.com/go-co-op/gocron" +@@ -171,8 +172,9 @@ func AutoUpdateVersionByIds(d *gorm.DB, ids []uint) (err error) { + } + + var ( +- schedulerCache = make(map[uint]*gocron.Scheduler) +- schedulerMu sync.RWMutex ++ schedulerCache = make(map[uint]*gocron.Scheduler) ++ schedulerMu sync.RWMutex ++ subscriptionUpdateMu sync.Mutex + ) + + func UpdateAll(ctx context.Context) { +@@ -258,68 +260,161 @@ func Update(ctx context.Context, _id graphql.ID) (r *Resolver, err error) { + return &Resolver{Subscription: m}, nil + } + +-func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) { +- // Fetch node links. +- var m db.Subscription +- if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil { +- return nil, err ++func reconcileSubscriptionNodes(tx *gorm.DB, subId uint, links []string) error { ++ var existing []db.Node ++ if err := tx.Where("subscription_id = ?", subId).Find(&existing).Error; err != nil { ++ return err + } +- links, err := fetchLinks(m.Link) +- if err != nil { +- return nil, err ++ ++ incoming := make(map[string]struct{}, len(links)) ++ for _, link := range links { ++ incoming[link] = struct{}{} + } + +- tx := db.BeginTx(ctx) +- defer func() { +- if err == nil { +- tx.Commit() ++ current := make(map[string]struct{}, len(existing)) ++ var staleIds []uint ++ for _, n := range existing { ++ if _, ok := incoming[n.Link]; ok { ++ current[n.Link] = struct{}{} ++ continue ++ } ++ staleIds = append(staleIds, n.ID) ++ } ++ ++ fixed := make(map[uint]struct{}) ++ if len(staleIds) > 0 { ++ var fixedIds []uint ++ if err := tx.Table("group_nodes"). ++ Distinct("group_nodes.node_id"). ++ Joins("INNER JOIN groups ON groups.id = group_nodes.group_id"). ++ Where("groups.policy = ?", "fixed"). ++ Where("group_nodes.node_id IN ?", staleIds). ++ Pluck("group_nodes.node_id", &fixedIds).Error; err != nil { ++ return err ++ } ++ for _, id := range fixedIds { ++ fixed[id] = struct{}{} ++ } ++ } ++ ++ var detachIds, deleteIds []uint ++ for _, id := range staleIds { ++ if _, ok := fixed[id]; ok { ++ detachIds = append(detachIds, id) + } else { +- tx.Rollback() ++ deleteIds = append(deleteIds, id) ++ } ++ } ++ ++ if len(staleIds) > 0 { ++ if err := node.AutoUpdateVersionByIds(tx, staleIds); err != nil { ++ return err ++ } ++ } ++ if len(detachIds) > 0 { ++ if err := tx.Model(&db.Node{}). ++ Where("id IN ?", detachIds). ++ Update("subscription_id", nil).Error; err != nil { ++ return err ++ } ++ } ++ if len(deleteIds) > 0 { ++ if err := tx.Exec("DELETE FROM group_nodes WHERE node_id IN ?", deleteIds).Error; err != nil { ++ return err ++ } ++ if err := tx.Where("id IN ?", deleteIds).Delete(&db.Node{}).Error; err != nil { ++ return err + } +- }() +- // Remove those nodes whose subscription are independent from any groups. +- subQuery := tx.Raw(`select nodes.id as id +- from nodes +- inner join group_nodes on group_nodes.node_id = nodes.id +- where subscription_id = ?`, subId) +- +- if err = tx.Where("subscription_id = ?", subId). +- Where("id not in (?)", subQuery). +- Select(clause.Associations). +- Delete(&db.Node{}).Error; err != nil { +- return nil, err + } +- // Import node links. ++ ++ seen := make(map[string]struct{}, len(links)) + var args []*internal.ImportArgument + for _, link := range links { ++ if _, ok := seen[link]; ok { ++ continue ++ } ++ seen[link] = struct{}{} ++ if _, ok := current[link]; ok { ++ continue ++ } + args = append(args, &internal.ImportArgument{Link: link}) + } +- result, err := node.Import(tx, false, &subId, args) ++ result, err := node.Import(tx, true, &subId, args) + if err != nil { +- return nil, err ++ return errors.New("failed to import subscription node") + } +- hasAnyCandidate := false + for _, r := range result { +- if r.Error == nil { +- hasAnyCandidate = true +- break ++ if r.Error != nil { ++ return errors.New("failed to import subscription node") + } + } +- if !hasAnyCandidate { +- return nil, fmt.Errorf("interrupt to update subscription: no any valid node can be imported") ++ ++ var count int64 ++ if err := tx.Model(&db.Node{}).Where("subscription_id = ?", subId).Count(&count).Error; err != nil { ++ return err + } +- // Update updated_at and return the latest version. +- if err = tx.Model(&m). ++ if count == 0 { ++ return fmt.Errorf("interrupt to update subscription: no any valid node can be imported") ++ } ++ return nil ++} ++ ++func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, links []string) (err error) { ++ subscriptionUpdateMu.Lock() ++ defer subscriptionUpdateMu.Unlock() ++ ++ tx := db.BeginTx(ctx) ++ if tx.Error != nil { ++ return tx.Error ++ } ++ defer func() { ++ if err != nil { ++ tx.Rollback() ++ } ++ }() ++ var current db.Subscription ++ if err = tx.Where(&db.Subscription{ID: subId}).First(¤t).Error; err != nil { ++ return err ++ } ++ if current.Link != m.Link { ++ return fmt.Errorf("subscription changed during update; retry") ++ } ++ *m = current ++ ++ if err = reconcileSubscriptionNodes(tx, subId, links); err != nil { ++ return err ++ } ++ q := tx.Model(m). + Clauses(clause.Returning{}). + Where(&db.Subscription{ID: subId}). +- Update("updated_at", time.Now()).Error; err != nil { +- return nil, err ++ Update("updated_at", time.Now()) ++ if q.Error != nil { ++ return q.Error ++ } ++ if q.RowsAffected == 0 { ++ return fmt.Errorf("no such subscription") + } +- +- // Update modified if subscription is referenced by running config. + if err = AutoUpdateVersionByIds(tx, []uint{subId}); err != nil { ++ return err ++ } ++ return tx.Commit().Error ++} ++ ++func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) { ++ var m db.Subscription ++ if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil { ++ return nil, err ++ } ++ links, err := fetchLinks(m.Link) ++ if err != nil { + return nil, err + } ++ if err = updateSubscriptionTx(ctx, &m, subId, links); err != nil { ++ return nil, err ++ } ++ if err = config.ApplyIfRunning(ctx); err != nil { ++ return nil, fmt.Errorf("subscription updated but failed to apply runtime: %w", err) ++ } + return &m, nil + } + +@@ -399,6 +494,8 @@ func UpdateLink(ctx context.Context, _id graphql.ID, link string) (r *Resolver, + if err != nil { + return nil, err + } ++ subscriptionUpdateMu.Lock() ++ defer subscriptionUpdateMu.Unlock() + + tx := db.BeginTx(ctx) + defer func() { +diff --git a/graphql/service/subscription/mutation_utils_test.go b/graphql/service/subscription/mutation_utils_test.go +new file mode 100644 +index 0000000..18ff9b9 +--- /dev/null ++++ b/graphql/service/subscription/mutation_utils_test.go +@@ -0,0 +1,212 @@ ++/* ++ * SPDX-License-Identifier: AGPL-3.0-only ++ * Copyright (c) 2023, daeuniverse Organization ++ */ ++ ++package subscription ++ ++import ( ++ "context" ++ "fmt" ++ "testing" ++ "time" ++ ++ "github.com/daeuniverse/dae-wing/db" ++) ++ ++func mustNoError(t *testing.T, err error) { ++ t.Helper() ++ if err != nil { ++ t.Fatal(err) ++ } ++} ++ ++func assertError(t *testing.T, err error) { ++ t.Helper() ++ if err == nil { ++ t.Fatal("expected error") ++ } ++} ++ ++func assertEqualValues(t *testing.T, want, got interface{}) { ++ t.Helper() ++ if fmt.Sprint(want) != fmt.Sprint(got) { ++ t.Fatalf("want %v, got %v", want, got) ++ } ++} ++ ++func assertNotNil(t *testing.T, value *uint) { ++ t.Helper() ++ if value == nil { ++ t.Fatal("expected non-nil value") ++ } ++} ++ ++func assertNil(t *testing.T, value *uint) { ++ t.Helper() ++ if value != nil { ++ t.Fatalf("expected nil, got %v", *value) ++ } ++} ++ ++func assertZero(t *testing.T, value int64) { ++ t.Helper() ++ if value != 0 { ++ t.Fatalf("expected zero, got %d", value) ++ } ++} ++ ++func initReconcileTestDB(t *testing.T) context.Context { ++ t.Helper() ++ mustNoError(t, db.InitDatabase(t.TempDir())) ++ return context.Background() ++} ++ ++func createTestSubscription(t *testing.T, ctx context.Context, links ...string) (db.Subscription, []db.Node) { ++ t.Helper() ++ sub := db.Subscription{ ++ UpdatedAt: time.Now(), ++ Link: "https://example.invalid/subscription", ++ Status: "", ++ Info: "", ++ } ++ mustNoError(t, db.DB(ctx).Create(&sub).Error) ++ ++ nodes := make([]db.Node, 0, len(links)) ++ for i, link := range links { ++ node := db.Node{ ++ Link: link, ++ Name: "node", ++ Address: "192.0.2.1", ++ Protocol: "ss", ++ SubscriptionID: &sub.ID, ++ } ++ node.Name += string(rune('A' + i)) ++ mustNoError(t, db.DB(ctx).Create(&node).Error) ++ nodes = append(nodes, node) ++ } ++ return sub, nodes ++} ++ ++func createTestGroup(t *testing.T, ctx context.Context, policy string, systemID *uint, nodes ...db.Node) db.Group { ++ t.Helper() ++ group := db.Group{Name: policy + time.Now().Format("150405.000000000"), Policy: policy, SystemID: systemID} ++ mustNoError(t, db.DB(ctx).Create(&group).Error) ++ mustNoError(t, db.DB(ctx).Model(&group).Association("Node").Append(nodes)) ++ return group ++} ++ ++func countGroupNode(t *testing.T, ctx context.Context, groupID, nodeID uint) int64 { ++ t.Helper() ++ var count int64 ++ mustNoError(t, db.DB(ctx).Table("group_nodes"). ++ Where("group_id = ? AND node_id = ?", groupID, nodeID). ++ Count(&count).Error) ++ return count ++} ++ ++func TestReconcileKeepsCurrentReferencedNode(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current") ++ group := createTestGroup(t, ctx, "min_moving_avg", nil, nodes[0]) ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"})) ++ ++ var got db.Node ++ mustNoError(t, db.DB(ctx).First(&got, nodes[0].ID).Error) ++ assertNotNil(t, got.SubscriptionID) ++ assertEqualValues(t, sub.ID, *got.SubscriptionID) ++ assertEqualValues(t, 1, countGroupNode(t, ctx, group.ID, got.ID)) ++} ++ ++func TestReconcileDeletesStaleUnreferencedNode(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale") ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"})) ++ ++ var count int64 ++ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("id = ?", nodes[1].ID).Count(&count).Error) ++ assertZero(t, count) ++} ++ ++func TestReconcileDeletesStaleNormalGroupNode(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ system := db.System{Running: true} ++ mustNoError(t, db.DB(ctx).Create(&system).Error) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale") ++ group := createTestGroup(t, ctx, "min_moving_avg", &system.ID, nodes...) ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"})) ++ ++ var gotGroup db.Group ++ mustNoError(t, db.DB(ctx).First(&gotGroup, group.ID).Error) ++ assertEqualValues(t, 1, gotGroup.Version) ++ assertEqualValues(t, 0, countGroupNode(t, ctx, group.ID, nodes[1].ID)) ++} ++ ++func TestReconcileDetachesStaleFixedNode(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://fixed-stale") ++ group := createTestGroup(t, ctx, "fixed", nil, nodes[1]) ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"})) ++ ++ var got db.Node ++ mustNoError(t, db.DB(ctx).First(&got, nodes[1].ID).Error) ++ assertNil(t, got.SubscriptionID) ++ assertEqualValues(t, 1, countGroupNode(t, ctx, group.ID, got.ID)) ++} ++ ++func TestReconcilePreservesNodeReferencedByFixedAndNormalGroups(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://shared-stale") ++ fixed := createTestGroup(t, ctx, "fixed", nil, nodes[1]) ++ normal := createTestGroup(t, ctx, "min", nil, nodes[1]) ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"})) ++ ++ var got db.Node ++ mustNoError(t, db.DB(ctx).First(&got, nodes[1].ID).Error) ++ assertNil(t, got.SubscriptionID) ++ assertEqualValues(t, 1, countGroupNode(t, ctx, fixed.ID, got.ID)) ++ assertEqualValues(t, 1, countGroupNode(t, ctx, normal.ID, got.ID)) ++} ++ ++func TestReconcileAllLinksAlreadyExistSucceeds(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://one", "ss://two") ++ ++ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://one", "ss://two", "ss://two"})) ++ ++ var count int64 ++ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("subscription_id = ?", sub.ID).Count(&count).Error) ++ assertEqualValues(t, len(nodes), count) ++} ++ ++func TestReconcileRollsBackWhenAnyIncomingNodeIsInvalid(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale") ++ tx := db.BeginTx(ctx) ++ ++ assertError(t, reconcileSubscriptionNodes(tx, sub.ID, []string{"ss://current", "not-a-node-link"})) ++ mustNoError(t, tx.Rollback().Error) ++ ++ var count int64 ++ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("id IN ?", []uint{nodes[0].ID, nodes[1].ID}).Count(&count).Error) ++ assertEqualValues(t, 2, count) ++} ++ ++func TestUpdateSubscriptionRejectsChangedLink(t *testing.T) { ++ ctx := initReconcileTestDB(t) ++ sub, nodes := createTestSubscription(t, ctx, "ss://current") ++ snapshot := sub ++ mustNoError(t, db.DB(ctx).Model(&sub).Update("link", "https://example.invalid/changed").Error) ++ ++ assertError(t, updateSubscriptionTx(ctx, &snapshot, sub.ID, []string{"ss://replacement"})) ++ ++ var got db.Node ++ mustNoError(t, db.DB(ctx).First(&got, nodes[0].ID).Error) ++ assertNotNil(t, got.SubscriptionID) ++ assertEqualValues(t, sub.ID, *got.SubscriptionID) ++} diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index 6a38d93a..94e92ac4 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.14.7 -PKG_RELEASE:=34 +PKG_RELEASE:=35 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh b/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh index ee1e2bb7..da6396af 100755 --- a/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh +++ b/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh @@ -121,12 +121,27 @@ if [ "$fail" -ne 0 ]; then fi if [ "$count" -gt 0 ] && [ "$ok" -gt 0 ] && /bin/pidof daed >/dev/null 2>&1; then + state_body="$TMPDIR/state.json" + state_resp="$TMPDIR/state.out" + printf '%s' '{"query":"query General{general{dae{running modified}}}","variables":{}}' > "$state_body" + if ! post_graphql "$state_body" "$state_resp" "$token" || grep -q '"errors"' "$state_resp"; then + fail 8 "failed to query daed runtime state: $(cat "$state_resp" 2>/dev/null)" + fi + if ! grep -q '"running"[[:space:]]*:[[:space:]]*true' "$state_resp"; then + log "daed proxy is stopped; skipping runtime apply" + exit 0 + fi + if ! grep -q '"modified"[[:space:]]*:[[:space:]]*true' "$state_resp"; then + log "daed runtime is already up to date" + exit 0 + fi + run_body="$TMPDIR/run.json" run_resp="$TMPDIR/run.out" printf '{"query":"mutation Run($dry:Boolean!){run(dry:$dry)}","variables":{"dry":false}}' > "$run_body" log "applying updated subscriptions" if ! post_graphql "$run_body" "$run_resp" "$token" || grep -q '"errors"' "$run_resp"; then - fail 8 "failed to apply updated subscriptions: $(cat "$run_resp" 2>/dev/null)" + fail 9 "failed to apply updated subscriptions: $(cat "$run_resp" 2>/dev/null)" fi fi diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index f35c8acb..8e736086 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.7.16 -PKG_RELEASE:=188 +PKG_RELEASE:=189 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua index 979d4fe4..38c96631 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua @@ -24,23 +24,26 @@ o:value("https://cdn.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/ o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat")) o:value("https://cdn.jsdelivr.net/gh/Loukky/gfwlist-by-loukky/gfwlist.txt", translate("Loukky/gfwlist-by-loukky")) o:value("https://cdn.jsdelivr.net/gh/gfwlist/gfwlist/gfwlist.txt", translate("gfwlist/gfwlist")) +o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/gfwlist/gfwlist.txt", translate("pexcn/gfwlist")) o.default = o.keylist[2] ----chnroute URL o = s:option(DynamicList, "chnroute_url", translate("China IPs(chnroute) Update URL")) o:depends("geo2rule", false) -o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("gaoyifan/china-operator-ip/china")) o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang.CN")) +o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("gaoyifan/china-operator-ip/china")) o:value("https://cdn.jsdelivr.net/gh/soffchen/GeoIP2-CN@release/CN-ip-cidr.txt", translate("soffchen/GeoIP2-CN")) o:value("https://cdn.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us/GeoIP2-CN")) o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_IP_No_IPv6.txt", translate("ios_rule_script/ChinaMax_IP_No_IPv6")) +o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chnroute/chnroute.txt", translate("pexcn/chnroute")) ----chnroute6 URL o = s:option(DynamicList, "chnroute6_url", translate("China IPv6s(chnroute6) Update URL")) o:depends("geo2rule", false) -o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china6.txt", translate("gaoyifan/china-operator-ip/china6")) o:value("https://ispip.clang.cn/all_cn_ipv6.txt", translate("Clang.CN.IPv6")) +o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china6.txt", translate("gaoyifan/china-operator-ip/china6")) o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_IP.txt", translate("ios_rule_script/ChinaMax_IP")) +o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chnroute/chnroute6.txt", translate("pexcn/chnroute6")) ----chnlist URL o = s:option(DynamicList, "chnlist_url", translate("China List(Chnlist) Update URL")) @@ -52,6 +55,7 @@ o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/china- o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/apple-cn.txt", translate("Loyalsoldier/apple-cn")) o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/google-cn.txt", translate("Loyalsoldier/google-cn")) o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_Domain.txt", translate("ios_rule_script/ChinaMax_Domain")) +o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chinalist/chinalist.txt", translate("pexcn/chinalist")) if has_xray or has_singbox then o = s:option(Value, "geoip_url", translate("GeoIP Update URL")) diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 2f11e363..6c2ae8a5 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -154,8 +154,7 @@ function gen_outbound(flag, node, tag, proxy_table) MaxConcurrentTry = 4 } or nil }, - network = (api.compare_versions(xray_version, "<", "26.7.11")) and node.transport or nil, -- Todo: Remove - method = (api.compare_versions(xray_version, ">=", "26.7.11")) and node.transport or nil, -- Todo: Remove version check + [(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network" security = node.stream_security, tlsSettings = (node.stream_security == "tls") and { serverName = node.tls_serverName, @@ -646,8 +645,7 @@ function gen_config_server(node) protocol = node.protocol, settings = settings, streamSettings = { - network = (api.compare_versions(xray_version, "<", "26.7.11")) and node.transport or nil, -- Todo: Remove - method = (api.compare_versions(xray_version, ">=", "26.7.11")) and node.transport or nil, -- Todo: Remove version check + [(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network" security = "none", tlsSettings = ("1" == node.tls) and { disableSystemRoot = false,