-
Notifications
You must be signed in to change notification settings - Fork 18
/
wstat.go
309 lines (274 loc) · 8.47 KB
/
wstat.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package styx
import (
"fmt"
"math"
"os"
"sync/atomic"
"time"
"context"
"aqwari.net/net/styx/internal/styxfile"
"aqwari.net/net/styx/styxproto"
)
// In the plan 9 manual, stat(5) has this to say about modifying
// fields via Twstat:
//
// A wstat request can avoid modifying some properties of the
// file by providing explicit ``don't touch'' values in the
// stat data that is sent: zero-length strings for text values
// and the maximum unsigned value of appropriate size for inte-
// gral values.
//
// This keeps the protocol simpler by allowing a single message
// to modify multiple file attributes. However, it shifts the burden to
// the server to determine what fields are being modified and what
// fields should be untouched. The styx package will attempt to hide
// this complexity from the user, in a similar way to how it hides the
// complexity of the walk transaction; by generating multiple fake
// requests for each attribute to be changed, and assembling the
// responses. If any one of the responses are succesful, an Rwstat
// is returned.
//
// Note that for certain synthetic messages, there will be some overlap
// with certain 9P2000.u or 9P2000.L extensions (such as Trename).
// This is OK, we'll make the type generic enough for both.
type twstat struct {
status chan error
filled []int32
index int
reqInfo
}
// Call Rerror to provide a descriptive error message explaining
// why a file attribute could not be updated.
func (t twstat) Rerror(format string, args ...interface{}) {
t.respond(fmt.Errorf(format, args...))
}
func (t twstat) respond(err error) {
p := &t.filled[t.index]
if atomic.CompareAndSwapInt32(p, 0, 1) {
t.status <- err
}
}
func (t twstat) handled() bool {
return atomic.LoadInt32(&t.filled[t.index]) == 1
}
func (s *Session) handleTwstat(ctx context.Context, msg styxproto.Twstat, file file) bool {
// mode, atime+mtime, length, name, uid+gid, sync
// we will ignore muid
const numMutable = 6
// By convention, sending a Twstat message with a stat structure consisting
// entirely of "don't touch" values indicates that the client wants the server
// to sync the file to disk.
var haveChanges bool
var messages int
stat := msg.Stat()
// We buffer the channel so that the response
// methods for each attribute do not block.
status := make(chan error, numMutable)
info := newReqInfo(ctx, s, msg, file.name)
filled := make([]int32, numMutable)
atime, mtime := stat.Atime(), stat.Mtime()
if atime != math.MaxUint32 || mtime != math.MaxUint32 {
haveChanges = true
s.requests <- Tutimes{
Atime: time.Unix(int64(atime), 0),
Mtime: time.Unix(int64(mtime), 0),
twstat: twstat{status, filled, messages, info},
}
messages++
}
if uid, gid := string(stat.Uid()), string(stat.Gid()); uid != "" || gid != "" {
haveChanges = true
s.requests <- Tchown{
User: uid,
Group: gid,
twstat: twstat{status, filled, messages, info},
}
messages++
}
if name := string(stat.Name()); name != "" && name != file.name {
haveChanges = true
s.requests <- Trename{
OldPath: file.name,
NewPath: name,
twstat: twstat{status, filled, messages, info},
}
messages++
}
if length := stat.Length(); length != -1 {
haveChanges = true
s.requests <- Ttruncate{
Size: length,
twstat: twstat{status, filled, messages, info},
}
messages++
}
if stat.Mode() != math.MaxUint32 {
haveChanges = true
s.requests <- Tchmod{
Mode: styxfile.ModeOS(stat.Mode()),
twstat: twstat{status, filled, messages, info},
}
messages++
}
if len(stat.Muid()) != 0 {
// even though we won't respond to this field, we don't
// want to needlessly stimulate a sync request
haveChanges = true
}
if !haveChanges {
s.requests <- Tsync{
twstat: twstat{status, filled, messages, info},
}
messages++
}
go func() {
var (
success bool
err error
)
for i := 0; i < messages; i++ {
if e, ok := <-status; !ok {
panic("closed Twstat channel prematurely")
} else if e != nil {
err = e
} else {
success = true
}
}
if !s.conn.clearTag(msg.Tag()) {
return
}
if success {
s.conn.Rwstat(msg.Tag())
} else {
s.conn.Rerror(msg.Tag(), "%s", err)
}
s.conn.Flush()
}()
return true
}
// A Trename message is sent by the client to change the name of
// an existing file. Use the Rrename method to indicate success.
//
// The default response for a Trename request is an Rerror message
// saying "permission denied"
type Trename struct {
OldPath, NewPath string
twstat
}
func (t Trename) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// The Path method of a Trename request returns the current path
// to the file, before a rename has taken place.
func (t Trename) Path() string {
return t.OldPath
}
// Rrename indicates to the server that the rename was succesful. Once
// Rrename is called with a nil error, future stat requests should
// reflect the updated name.
func (t Trename) Rrename(err error) {
// BUG(droyo) renaming a file with one fid will break Twalk
// requests that attempt to clone another fid pointing to the
// same file.
if err == nil {
t.session.qidpool.Do(func(m map[interface{}]interface{}) {
if qid, ok := m[t.OldPath]; ok {
m[t.NewPath] = qid
}
})
}
t.respond(err)
}
// A Tchmod message is sent by the client to change the permissions
// of an existing file. The client must have write access to the file's
// containing directory. Use the Rchmod method to indicate success.
//
// The default response for a Tchmod request is an Rerror saying
// "permission denied"
type Tchmod struct {
Mode os.FileMode
twstat
}
func (t Tchmod) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// Rchmod, when called with a nil error, indicates that the permissions
// of the file were updated. Future stat requests should reflect the new
// file mode.
func (t Tchmod) Rchmod(err error) { t.respond(err) }
// A Tutimes message is sent by the client to change the modification
// time of a file. Use the Rutime method to indicate success.
//
// The default response to a Tutimes message is an Rerror message
// saying "permission denied"
type Tutimes struct {
Atime, Mtime time.Time
twstat
}
func (t Tutimes) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// Rutimes, when called with a nil error, indicates that the file
// times were succesfully updated. Future stat requests should reflect
// the new access and modification times.
func (t Tutimes) Rutimes(err error) { t.respond(err) }
// A Tchown message is sent by the client to change the user and group
// associated with a file. Use the Rchown method to indicate success.
//
// The default response to a Tchown message is an Rerror message
// saying "permission denied".
type Tchown struct {
User, Group string
// These will only be set if using the 9P2000.u or 9P2000.L
// extensions, and will be -1 otherwise.
Uid, Gid int
twstat
}
func (t Tchown) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// Rchown, when called with a nil error, indicates that file and group
// ownership attributes were updated for the given file. Future stat
// requests for the same file should reflect the changes.
func (t Tchown) Rchown(err error) { t.respond(err) }
// A Ttruncate requests for the size of a file to be changed. Use the Rtruncate
// method to indicate success.
//
// The default response to a Ttruncate message is an Rerror message
// saying "permission denied".
type Ttruncate struct {
Size int64
twstat
}
func (t Ttruncate) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// Rtruncate, when called with a nil error, indicates that the file has been
// updated to reflect Size. Future reads, writes and stats should reflect
// the new file length.
func (t Ttruncate) Rtruncate(err error) { t.respond(err) }
// A Tsync request is made by the client to indicate that the client would
// like any changes made to the file to be flushed to durable storage. Use
// the Rsync method to indicate success.
//
// The default response to a Tsync message is an Rerror message saying
// "not supported".
type Tsync struct {
twstat
}
func (t Tsync) WithContext(ctx context.Context) Request {
t.ctx = ctx
return t
}
// Rsync, when called with a nil error, indicates that the file has
// been flushed to durable storage. Note that different servers will
// have different definitions of what "durable" means, and provide
// different consistency guarantees.
func (t Tsync) Rsync(err error) { t.respond(err) }
func (t Tsync) defaultResponse() { t.Rerror("not supported") }