@@ -2,71 +2,16 @@ package main
2
2
3
3
import (
4
4
"context"
5
- "encoding/binary"
6
5
"errors"
7
6
"fmt"
8
7
"github.com/urfave/cli/v3"
9
8
"log"
10
- "net"
11
9
"os"
10
+ "strings"
11
+ "sync"
12
12
"time"
13
13
)
14
14
15
- // Calls an NTP server according to [RFC 5905](https://datatracker.ietf.org/doc/html/rfc5905), and returns the current time.
16
- func callNTPAddress (address string , quiet bool ) * time.Time {
17
- if ! quiet {
18
- fmt .Printf ("Calling NTP server at %s...\n " , address )
19
- }
20
-
21
- packet := make ([]byte , 48 )
22
-
23
- // leap 0, version 4, mode 3 (client)
24
- // 0 4 3 -> 00 100 011 -> 0x23
25
- packet [0 ] = 0x23
26
-
27
- connection , err := net .Dial ("udp" , address )
28
-
29
- if err != nil {
30
- fmt .Println ("An error occurred: " , err )
31
- return nil
32
- }
33
-
34
- defer connection .Close ()
35
-
36
- // Set a deadline for the connection
37
- if deadlineExceededError := connection .SetDeadline (time .Now ().Add (3 * time .Second )); deadlineExceededError != nil {
38
- fmt .Println ("Deadline exceeded: " , deadlineExceededError )
39
- return nil
40
- }
41
-
42
- // Send the packet to the server
43
- if _ , writeError := connection .Write (packet ); writeError != nil {
44
- fmt .Println ("Writing the packet failed: " , writeError )
45
- return nil
46
- }
47
-
48
- responsePacket := make ([]byte , 48 )
49
-
50
- // Read the response from the server
51
- if _ , readError := connection .Read (responsePacket ); readError != nil {
52
- fmt .Println ("Reading the response failed: " , readError )
53
- return nil
54
- }
55
-
56
- ntpSeconds := binary .BigEndian .Uint32 (responsePacket [40 :44 ])
57
-
58
- // RFC 868: https://datatracker.ietf.org/doc/rfc868/
59
- unixSeconds := int64 (ntpSeconds ) - 2208988800
60
-
61
- unixTime := time .Unix (unixSeconds , 0 )
62
-
63
- return & unixTime
64
- }
65
-
66
- func callNTP (hostname string , port int , quiet bool ) * time.Time {
67
- return callNTPAddress (fmt .Sprintf ("%s:%d" , hostname , port ), quiet )
68
- }
69
-
70
15
func main () {
71
16
timeFormats := map [string ]string {
72
17
"Layout" : time .Layout ,
@@ -105,6 +50,9 @@ func main() {
105
50
106
51
layout := time .UnixDate
107
52
53
+ var parallels []string
54
+ var fallbacks []string
55
+
108
56
command := & cli.Command {
109
57
Name : "ntp" ,
110
58
Usage : "Get the current time from an NTP server" ,
@@ -161,42 +109,81 @@ func main() {
161
109
layout = dateLayout
162
110
}
163
111
112
+ return nil
113
+ },
114
+ },
115
+ & cli.StringFlag {
116
+ Name : "parallel" ,
117
+ Usage : "Servers to call in parallel, alongside the main server. Separated by commas." ,
118
+ Action : func (_ context.Context , _ * cli.Command , serversString string ) error {
119
+ parallels = strings .Split (serversString , "," )
120
+
121
+ return nil
122
+ },
123
+ },
124
+ & cli.StringFlag {
125
+ Name : "fallback" ,
126
+ Usage : "Fallback servers to call if the main server fails. Separated by commas." ,
127
+ Action : func (_ context.Context , _ * cli.Command , serversString string ) error {
128
+ fallbacks = strings .Split (serversString , "," )
129
+
164
130
return nil
165
131
},
166
132
},
167
133
},
168
134
169
135
Action : func (context context.Context , command * cli.Command ) error {
170
- var currentTime * time.Time
136
+ var group sync.WaitGroup
137
+ group .Add (1 )
138
+
139
+ channel := make (chan * time.Time , 1 )
171
140
172
141
switch {
173
- case ! addressPresent && hostnamePresent : // hostname is present, address is not (use default port)
174
- currentTime = callNTP ( hostname , int ( port ), quiet )
142
+ case ! addressPresent && ( hostnamePresent || ( ! hostnamePresent && ! portPresent )):
143
+ go CallNTPAsync ( fmt . Sprintf ( "%s:%d" , hostname , port ), quiet , channel , & group )
175
144
case addressPresent && ! hostnamePresent && ! portPresent : // address is present, hostname and port are not
176
- currentTime = callNTPAddress (address , quiet )
177
- case ! addressPresent && ! hostnamePresent && ! portPresent : // neither address nor hostname nor port are present (use defaults)
178
- currentTime = callNTP (hostname , int (port ), quiet )
145
+ go CallNTPAsync (address , quiet , channel , & group )
179
146
default :
180
147
return errors .New ("invalid arguments: you can either specify an address or a hostname, but not both" )
181
148
}
182
149
183
- if currentTime != nil {
184
- var timeString string
185
-
186
- switch layout {
187
- case "Seconds1970" :
188
- timeString = fmt .Sprintf ("%d" , currentTime .Unix ())
189
- case "Seconds1900" :
190
- timeString = fmt .Sprintf ("%d" , currentTime .Unix ()+ 2208988800 )
191
- default :
192
- timeString = currentTime .Format (layout )
150
+ if parallelsLen , fallbacksLen := len (parallels ), len (fallbacks ); parallelsLen > 0 && fallbacksLen > 0 {
151
+ return errors .New ("you can either specify parallel servers or fallback servers, but not both" )
152
+ } else if parallelsLen > 0 {
153
+ for _ , address := range parallels {
154
+ go CallNTPAsync (address , quiet , channel , & group )
193
155
}
156
+ } else if fallbacksLen > 0 {
157
+ group .Wait () // wait for the primary server to respond
158
+
159
+ for _ , address := range fallbacks {
160
+ select {
161
+ case <- channel : // if the last server has already responded
162
+ break
163
+ default :
164
+ }
165
+
166
+ if result := CallNTP (address , quiet ); result != nil {
167
+ channel <- result
168
+ }
169
+ }
170
+ }
171
+
172
+ group .Wait ()
173
+
174
+ close (channel )
175
+
176
+ currentTime := <- channel
177
+
178
+ if currentTime != nil {
179
+ timeString := FormatTime (currentTime , layout )
194
180
195
181
if ! quiet {
196
- fmt .Printf ("Current time: %s\n " , timeString )
197
- } else {
198
- fmt .Printf ("%s\n " , timeString )
182
+ fmt .Printf ("Current time: " )
199
183
}
184
+
185
+ fmt .Printf ("%s\n " , timeString )
186
+
200
187
} else {
201
188
return errors .New ("failed to get the current time" )
202
189
}
0 commit comments