Skip to content

Commit 482dc61

Browse files
liplus-lin-layliplus-lin-layclaude
authored
fix(check): remove check gate — DNS verify runs on every timer tick (#39)
* docs: add update procedure * fix(check): remove check gate — DNS verify runs on every timer tick (issue #38) checkGate used UpdateTime (1440 min default), so DNS verification only fired once a day. External DNS changes (e.g. manual record edits) went undetected until the gate elapsed. Remove the gate entirely. The systemd timer (every 5 min) is the effective rate limit. Check now detects mismatches within one timer interval and forces an immediate DDNS update. Closes #37, #38 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(acceptance): replace gate test with no-providers check test TestBinary_Check_GatedExitZero relied on gate_check being pre-seeded to skip execution. Since the check gate was removed in the check.go fix (issue #38), that test no longer represents real behaviour and was causing CI to fail by attempting a real DNS lookup with IPV4=on. Replace with TestBinary_Check_NoProviders_ExitZero which uses the default IPV4=off / IPV6=off config — no DNS lookup is attempted and check exits 0 cleanly, matching the expected behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: liplus-lin-lay <lin-lay@liplus-project.github> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0fca028 commit 482dc61

4 files changed

Lines changed: 145 additions & 72 deletions

File tree

Installation.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Installation
2+
3+
## 必要環境
4+
5+
| 項目 | 要件 |
6+
|------|------|
7+
| OS | AlmaLinux 9 / 10(他の systemd Linux でも動作可能) |
8+
| Go | 1.23 以上(ビルド時のみ) |
9+
| git | リポジトリのクローンに必要 |
10+
| 権限 | インストールスクリプトは root 必要 |
11+
12+
### 依存パッケージのインストール(AlmaLinux)
13+
14+
```bash
15+
# git
16+
sudo dnf install -y git
17+
18+
# Go 1.23+(dnf のバージョンが古い場合は公式バイナリを使用)
19+
sudo dnf install -y golang
20+
go version # 1.23 未満の場合は以下の手順で公式バイナリを導入
21+
22+
# Go 公式バイナリ(dnf で 1.23+ が入らない場合)
23+
curl -LO https://go.dev/dl/go1.23.6.linux-amd64.tar.gz
24+
sudo tar -C /usr/local -xzf go1.23.6.linux-amd64.tar.gz
25+
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
26+
source /etc/profile.d/go.sh
27+
go version
28+
```
29+
30+
---
31+
32+
## ビルド
33+
34+
```bash
35+
git clone https://github.com/Liplus-Project/dipper_ai.git
36+
cd dipper_ai
37+
go build -o dipper_ai ./cmd/dipper_ai
38+
```
39+
40+
クロスコンパイル例(Linux AMD64 向け):
41+
42+
```bash
43+
GOOS=linux GOARCH=amd64 go build -o dipper_ai ./cmd/dipper_ai
44+
```
45+
46+
---
47+
48+
## インストール
49+
50+
`scripts/install.sh` を root で実行します。
51+
以下のファイルが配置され、systemd タイマーが有効化されます。
52+
53+
```bash
54+
sudo bash scripts/install.sh
55+
```
56+
57+
### 配置先
58+
59+
| ファイル | 配置先 |
60+
|----------|--------|
61+
| `dipper_ai` バイナリ | `/usr/bin/dipper_ai` |
62+
| 設定サンプル | `/etc/dipper_ai/user.conf.example` |
63+
| systemd サービス | `/etc/systemd/system/dipper_ai.service` |
64+
| systemd タイマー | `/etc/systemd/system/dipper_ai.timer` |
65+
| 状態ファイル | `/etc/dipper_ai/state/`(実行時に自動生成) |
66+
67+
### インストール後の手順
68+
69+
```bash
70+
# 設定ファイルを作成して編集
71+
sudo cp /etc/dipper_ai/user.conf.example /etc/dipper_ai/user.conf
72+
sudo vi /etc/dipper_ai/user.conf
73+
74+
# タイマーの状態確認
75+
systemctl status dipper_ai.timer
76+
77+
# 手動で update を実行してテスト
78+
sudo dipper_ai update
79+
```
80+
81+
---
82+
83+
## アップデート
84+
85+
新しいバージョンが出たときはリポジトリのディレクトリで以下を実行するだけです。
86+
87+
```bash
88+
cd dipper_ai
89+
git pull
90+
go build -o dipper_ai ./cmd/dipper_ai
91+
sudo bash scripts/install.sh
92+
```
93+
94+
4 ステップの意味:
95+
96+
| ステップ | 内容 |
97+
|----------|------|
98+
| `git pull` | 最新のソースコードを取得 |
99+
| `go build ...` | バイナリを再ビルド |
100+
| `sudo bash scripts/install.sh` | バイナリと systemd unit を上書きインストール・タイマー再起動 |
101+
102+
> **Note:** 設定ファイル (`/etc/dipper_ai/user.conf`) はインストールスクリプトで上書きされません。設定は引き継がれます。
103+
104+
---
105+
106+
## アンインストール
107+
108+
```bash
109+
sudo bash scripts/uninstall.sh
110+
```
111+
112+
サービス・タイマーの停止と無効化、バイナリの削除を行います。
113+
設定ファイル (`/etc/dipper_ai/`) は保持されます。手動で削除してください。
114+
115+
---
116+
117+
## 設定ファイルのパス
118+
119+
デフォルトは `/etc/dipper_ai/user.conf` ですが、環境変数 `DIPPER_AI_CONFIG` で上書きできます。
120+
121+
```bash
122+
DIPPER_AI_CONFIG=/path/to/custom.conf dipper_ai update
123+
```
124+
125+
---
126+
127+
## テスト実行
128+
129+
```bash
130+
go test ./...
131+
```
132+
133+
全パッケージのユニットテスト・受け入れテストが実行されます(外部 API 接続不要)。

internal/acceptance/binary_test.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"path/filepath"
1010
"strings"
1111
"testing"
12-
"time"
1312
)
1413

1514
var binaryPath string
@@ -78,20 +77,15 @@ func TestBinary_Update_ExitZero(t *testing.T) {
7877
}
7978
}
8079

81-
func TestBinary_Check_GatedExitZero(t *testing.T) {
80+
func TestBinary_Check_NoProviders_ExitZero(t *testing.T) {
8281
stateDir := t.TempDir()
83-
// check uses UpdateTime as its gate duration (default 1440 min).
84-
conf := writeConf(t, stateDir, "IPV4=on\n")
85-
86-
// Pre-seed gate_check as "just touched" so check skips without network.
87-
ts := time.Now().Format(time.RFC3339)
88-
if err := os.WriteFile(filepath.Join(stateDir, "gate_check"), []byte(ts), 0644); err != nil {
89-
t.Fatal(err)
90-
}
82+
// IPV4=off, IPV6=off (default from writeConf) — no providers configured.
83+
// check should exit 0 immediately without attempting any DNS lookup.
84+
conf := writeConf(t, stateDir, "")
9185

9286
_, _, code := runBinary(t, conf, "check")
9387
if code != 0 {
94-
t.Errorf("expected exit 0 for gated check, got %d", code)
88+
t.Errorf("expected exit 0 for check with no providers, got %d", code)
9589
}
9690
}
9791

internal/mode/check.go

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import (
44
"fmt"
55
"net"
66
"os"
7-
"time"
87

98
"github.com/Liplus-Project/dipper_ai/internal/config"
109
"github.com/Liplus-Project/dipper_ai/internal/state"
11-
"github.com/Liplus-Project/dipper_ai/internal/timegate"
1210
)
1311

1412
// Package-level DNS lookup functions — overridable in tests.
@@ -19,19 +17,15 @@ var (
1917

2018
// Check resolves the DNS-registered IP for each configured DDNS domain and
2119
// compares it with the current external IP. If any domain has a stale or
22-
// wrong registration, Check resets the IP cache and forces an immediate DDNS
23-
// update via Update().
20+
// wrong registration, Check resets the per-domain cache and forces an
21+
// immediate DDNS update via Update().
22+
//
23+
// No internal gate: the systemd timer (every 5 min) is the effective rate
24+
// limit. Running check on every tick means external DNS changes (e.g. manual
25+
// record edits) are detected and corrected within one timer interval.
2426
//
2527
// Equivalent to `dipper check`.
2628
func Check(cfg *config.Config) error {
27-
// Check runs on its own schedule — use UpdateTime so it does not fire on
28-
// every timer tick (which would cause spurious DNS lookups and race
29-
// conditions immediately after a fresh update).
30-
checkGate := timegate.New(cfg.StateDir, "check", time.Duration(cfg.UpdateTime)*time.Minute)
31-
if !checkGate.ShouldRun() {
32-
return nil
33-
}
34-
3529
wantV4 := cfg.IPv4 && cfg.IPv4DDNS
3630
wantV6 := cfg.IPv6 && cfg.IPv6DDNS
3731

@@ -113,7 +107,6 @@ func Check(cfg *config.Config) error {
113107

114108
if !mismatch {
115109
fmt.Fprintln(os.Stderr, "dipper_ai check: all domains match current IP")
116-
_ = checkGate.Touch()
117110
return nil
118111
}
119112

@@ -150,9 +143,7 @@ func Check(cfg *config.Config) error {
150143
// Remove DDNS_TIME gate so Update() is not rate-limited on this forced run.
151144
_ = os.Remove(cfg.StateDir + "/gate_ddns")
152145

153-
updateErr := Update(cfg)
154-
_ = checkGate.Touch()
155-
return updateErr
146+
return Update(cfg)
156147
}
157148

158149
// lookupARecord resolves the IPv4 A record for domain.

internal/mode/check_test.go

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import (
1212
"github.com/Liplus-Project/dipper_ai/internal/state"
1313
)
1414

15-
// resetCheckGate removes the check gate file so the next Check() call runs.
16-
func resetCheckGate(dir string) {
17-
_ = os.Remove(dir + "/gate_check")
18-
}
19-
2015
// fakeFetchIP returns a fixed IPv4.
2116
func fakeFetchIP(addr string) func(bool, bool) (*ip.Result, error) {
2217
return func(v4, v6 bool) (*ip.Result, error) {
@@ -164,46 +159,6 @@ func TestCheck_DNSError_ForcesUpdate(t *testing.T) {
164159
}
165160
}
166161

167-
// TestCheck_Gate_SkipsWhenRecent verifies that the check gate prevents
168-
// re-running before UpdateTime minutes have elapsed.
169-
func TestCheck_Gate_SkipsWhenRecent(t *testing.T) {
170-
dir := t.TempDir()
171-
cfg := &config.Config{
172-
StateDir: dir,
173-
IPv4: true,
174-
IPv4DDNS: true,
175-
UpdateTime: 1440,
176-
DDNSTime: 1,
177-
MyDNS: []config.MyDNSEntry{
178-
{ID: "u", Pass: "p", Domain: "home.example.com", IPv4: true},
179-
},
180-
}
181-
182-
origFetch := ipFetch
183-
fetchCount := 0
184-
ipFetch = func(v4, v6 bool) (*ip.Result, error) {
185-
fetchCount++
186-
return &ip.Result{IPv4: "1.2.3.4"}, nil
187-
}
188-
t.Cleanup(func() { ipFetch = origFetch })
189-
190-
origDNS := dnsLookupA
191-
dnsLookupA = func(domain string) (string, error) { return "1.2.3.4", nil }
192-
t.Cleanup(func() { dnsLookupA = origDNS })
193-
194-
// First run: gate passes, fetch occurs.
195-
if err := Check(cfg); err != nil {
196-
t.Fatalf("first run: %v", err)
197-
}
198-
// Second run: gate active (not enough time has passed) → skips.
199-
if err := Check(cfg); err != nil {
200-
t.Fatalf("second run: %v", err)
201-
}
202-
if fetchCount > 1 {
203-
t.Errorf("expected 1 fetch (gate should block second run), got %d", fetchCount)
204-
}
205-
}
206-
207162
// TestUpdate_PerDomain_OnlyChangedEntry verifies that only the entry whose
208163
// per-domain cache differs from the current IP is updated; unchanged entries
209164
// are skipped even when the timer fires.

0 commit comments

Comments
 (0)