lockf.rb 0.13.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.clang-format +11 -0
- data/.rubocop.yml +5 -1
- data/Gemfile +0 -1
- data/README.md +41 -68
- data/Rakefile.rb +8 -18
- data/bin/test-runner +6 -0
- data/lib/lock/file/constants.rb +15 -0
- data/lib/lock/file/ffi.rb +36 -0
- data/lib/lock/file/version.rb +5 -0
- data/lib/lock/file.rb +114 -0
- data/lib/lockf.rb +1 -139
- data/lib/lockfile.rb +1 -0
- data/lockf.rb.gemspec +8 -11
- data/share/lockf.rb/examples/1_lockfile_blocking_variant.rb +6 -5
- data/share/lockf.rb/examples/2_lockfile_nonblocking_variant.rb +6 -5
- data/share/lockf.rb/examples/3_ffilockf_blocking_variant.rb +6 -4
- data/share/lockf.rb/examples/4_ffilockf_nonblocking_variant.rb +6 -4
- data/test/lock_file_test.rb +13 -11
- data/test/readme_test.rb +19 -19
- data/test/setup.rb +1 -1
- metadata +28 -68
- data/ext/lockf.rb/extconf.rb +0 -12
- data/ext/lockf.rb/lockf.c +0 -34
- data/ext/lockf.rb/lockf.h +0 -1
- data/lib/lockf/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a49042a4d79251c0a85dbe626fefaa323862ea30a45426012a1d8bab08f63fb
|
4
|
+
data.tar.gz: 57f2c700f963a299ce4502e3a7856cc75283ceb80d1c27864ea08438950047d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d5c94dbaa6ad0b2eff1fe114e902d7c49014f2bb0ce69b2c5512ae0d107274d7c3c8b6a2d688672faeb85c7b48510689952efd9c64dc4cc78616e3e566f2e8e
|
7
|
+
data.tar.gz: 27c55d59bbab2f8e76b5370b010b3541d65a346b88279361e7f64110b3e6a1fe3f1ddaf4677fbefcec7f2e6409d70483937298f47cda1e3d729c209122d821df
|
data/.clang-format
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
BasedOnStyle: LLVM
|
2
|
+
IndentWidth: 2
|
3
|
+
SortIncludes: false
|
4
|
+
UseTab: Never
|
5
|
+
BreakBeforeBraces: Allman
|
6
|
+
AllowShortFunctionsOnASingleLine: Inline
|
7
|
+
AlwaysBreakAfterDefinitionReturnType: TopLevel
|
8
|
+
BreakBeforeBinaryOperators: All
|
9
|
+
BinPackArguments: false
|
10
|
+
AlignConsecutiveAssignments: true
|
11
|
+
AlwaysBreakAfterReturnType: None
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,48 +2,36 @@
|
|
2
2
|
|
3
3
|
lockf.rb provides Ruby bindings for
|
4
4
|
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3).
|
5
|
-
The
|
6
|
-
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
7
|
-
function implements an advisory-mode lock that can be placed on select
|
8
|
-
regions of a file, or on the entire contents of a file.
|
9
|
-
|
10
|
-
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
11
|
-
can synchronize access to a file between multiple processes, or
|
12
|
-
synchronize access to a shared resource being accessed by multiple
|
13
|
-
processes at the same time. When used with a shared resource,
|
14
|
-
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
15
|
-
can provide something similar to a mutex that works across multiple
|
16
|
-
processes rather than multiple threads.
|
17
5
|
|
18
6
|
## Examples
|
19
7
|
|
20
|
-
###
|
21
|
-
|
22
|
-
The
|
23
|
-
[`LockFile`](https://0x1eef.github.io/x/lockf.rb/LockFile.html)
|
24
|
-
class provides an abstract, Ruby-oriented interface to
|
25
|
-
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3).
|
8
|
+
### Lock::File
|
26
9
|
|
27
|
-
|
10
|
+
__Blocking__
|
28
11
|
|
29
|
-
|
30
|
-
|
12
|
+
[Lock::File#lock](http://0x1eef.github.io/x/lockf.rb/Lock/File.html#lock-instance_method)
|
13
|
+
can be used to acquire a lock.
|
14
|
+
[Lock::File.temporary_file](http://0x1eef.github.io/x/lockf.rb/Lock/File.html#temporary_file-class_method)
|
15
|
+
returns a lock for an unlinked temporary file.
|
16
|
+
[Lock::File#lock](http://0x1eef.github.io/x/lockf.rb/Lock/File.html#lock-instance_method)
|
17
|
+
will block when another
|
18
|
+
process has acquired a lock beforehand:
|
31
19
|
|
32
20
|
```ruby
|
33
|
-
|
21
|
+
#!/usr/bin/env ruby
|
22
|
+
require "lock/file"
|
34
23
|
|
35
|
-
lockf =
|
24
|
+
lockf = Lock::File.temporary_file
|
36
25
|
lockf.lock
|
37
26
|
print "Lock acquired by parent process (#{Time.now.utc})", "\n"
|
38
|
-
|
27
|
+
fork do
|
39
28
|
print "Child process waiting on lock (#{Time.now.utc})", "\n"
|
40
29
|
lockf.lock
|
41
30
|
print "Lock acquired by child process (#{Time.now.utc})", "\n"
|
42
31
|
end
|
43
32
|
sleep(3)
|
44
33
|
lockf.release
|
45
|
-
Process.wait
|
46
|
-
lockf.file.close
|
34
|
+
Process.wait
|
47
35
|
|
48
36
|
##
|
49
37
|
# Lock acquired by parent process (2023-02-11 16:43:15 UTC)
|
@@ -51,31 +39,31 @@ lockf.file.close
|
|
51
39
|
# Lock acquired by child process (2023-02-11 16:43:18 UTC)
|
52
40
|
```
|
53
41
|
|
54
|
-
__Non-
|
42
|
+
__Non-blocking__
|
55
43
|
|
56
|
-
|
57
|
-
without blocking. When it is found
|
58
|
-
the method will raise an
|
59
|
-
instead:
|
44
|
+
[Lock::File#lock_nonblock](http://0x1eef.github.io/x/lockf.rb/Lock/File.html#lock_nonblock-instance_method)
|
45
|
+
can be used to acquire a lock without blocking. When it is found
|
46
|
+
that acquiring a lock would block the method will raise an
|
47
|
+
exception (`Errno::EAGAIN` / `Errno::EWOULDBLOCK`) instead:
|
60
48
|
|
61
49
|
```ruby
|
62
|
-
|
50
|
+
#!/usr/bin/env ruby
|
51
|
+
require "lock/file"
|
63
52
|
|
64
|
-
lockf =
|
53
|
+
lockf = Lock::File.temporary_file
|
65
54
|
lockf.lock_nonblock
|
66
55
|
print "Lock acquired by parent process (#{Time.now.utc})", "\n"
|
67
|
-
|
56
|
+
fork do
|
68
57
|
lockf.lock_nonblock
|
69
58
|
print "Lock acquired by child process (#{Time.now.utc})", "\n"
|
70
59
|
rescue Errno::EWOULDBLOCK
|
71
|
-
print "Lock would block", "\n"
|
72
60
|
sleep 1
|
61
|
+
print "Lock would block", "\n"
|
73
62
|
retry
|
74
63
|
end
|
75
64
|
sleep 3
|
76
65
|
lockf.release
|
77
|
-
Process.wait
|
78
|
-
lockf.file.close
|
66
|
+
Process.wait
|
79
67
|
|
80
68
|
##
|
81
69
|
# Lock acquired by parent process (2023-02-11 19:03:05 UTC)
|
@@ -85,25 +73,25 @@ lockf.file.close
|
|
85
73
|
# Lock acquired by child process (2023-02-11 19:03:08 UTC)
|
86
74
|
```
|
87
75
|
|
88
|
-
###
|
76
|
+
### Lock::File::FFI
|
77
|
+
|
78
|
+
__lockf__
|
89
79
|
|
90
|
-
|
91
|
-
|
92
|
-
method provides a direct interface to
|
80
|
+
[Lock::File::FFI.lockf](http://0x1eef.freebsd.home.network/x/lockf.rb/Lock/File/FFI.html#lockf-instance_method)
|
81
|
+
provides a direct interface to
|
93
82
|
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
94
83
|
that is more or less equivalent to how the function would be called
|
95
|
-
from C
|
96
|
-
|
97
|
-
__Blocking lock__
|
84
|
+
from C:
|
98
85
|
|
99
86
|
```ruby
|
100
|
-
|
87
|
+
#!/usr/bin/env ruby
|
88
|
+
require "lock/file"
|
101
89
|
require "tempfile"
|
102
90
|
|
103
91
|
file = Tempfile.new("lockf-ffi").tap(&:unlink)
|
104
|
-
|
92
|
+
Lock::File::FFI.lockf(file, Lock::File::F_LOCK, 0)
|
105
93
|
print "Lock acquired", "\n"
|
106
|
-
|
94
|
+
Lock::File::FFI.lockf(file, Lock::File::F_ULOCK, 0)
|
107
95
|
print "Lock released", "\n"
|
108
96
|
file.close
|
109
97
|
|
@@ -112,29 +100,14 @@ file.close
|
|
112
100
|
# Lock released
|
113
101
|
```
|
114
102
|
|
115
|
-
|
103
|
+
## Documentation
|
116
104
|
|
117
|
-
|
118
|
-
|
119
|
-
require "tempfile"
|
120
|
-
|
121
|
-
file = Tempfile.new("lockf-ffi").tap(&:unlink)
|
122
|
-
LockFile.lockf(file.fileno, LockFile::F_TLOCK, 0)
|
123
|
-
print "Lock acquired", "\n"
|
124
|
-
LockFile.lockf(file.fileno, LockFile::F_ULOCK, 0)
|
125
|
-
print "Lock released", "\n"
|
126
|
-
file.close
|
127
|
-
|
128
|
-
##
|
129
|
-
# Lock acquired
|
130
|
-
# Lock released
|
131
|
-
```
|
105
|
+
A complete API reference is available at
|
106
|
+
[0x1eef.github.io/x/lockf.rb](https://0x1eef.github.io/x/lockf.rb)
|
132
107
|
|
133
108
|
## Install
|
134
109
|
|
135
|
-
|
136
|
-
|
137
|
-
lockf.rb can be installed via rubygems.org.
|
110
|
+
lockf.rb can be installed via rubygems.org:
|
138
111
|
|
139
112
|
gem install lockf.rb
|
140
113
|
|
@@ -145,7 +118,7 @@ lockf.rb can be installed via rubygems.org.
|
|
145
118
|
|
146
119
|
## License
|
147
120
|
|
148
|
-
[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
121
|
+
[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
149
122
|
<br>
|
150
|
-
See [LICENSE](./LICENSE)
|
123
|
+
See [LICENSE](./LICENSE)
|
151
124
|
|
data/Rakefile.rb
CHANGED
@@ -1,25 +1,15 @@
|
|
1
1
|
require "bundler/setup"
|
2
|
-
require "rake/extensiontask"
|
3
|
-
require "rake/testtask"
|
4
2
|
|
5
|
-
namespace :
|
6
|
-
desc "Run
|
7
|
-
task :c do
|
8
|
-
sh "uncrustify -c .uncrustify.cfg --no-backup --replace ext/lockf.rb/*.c"
|
9
|
-
end
|
10
|
-
|
11
|
-
desc "Run the Ruby linter"
|
3
|
+
namespace :format do
|
4
|
+
desc "Run rubocop"
|
12
5
|
task :ruby do
|
13
|
-
sh "bundle exec rubocop -A
|
6
|
+
sh "bundle exec rubocop -A"
|
14
7
|
end
|
15
8
|
end
|
16
|
-
task
|
17
|
-
|
18
|
-
Rake::ExtensionTask.new("lockf.rb")
|
9
|
+
task format: %w[format:ruby]
|
19
10
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
t.warning = false
|
11
|
+
desc "Run tests"
|
12
|
+
task :test do
|
13
|
+
sh "bin/test-runner"
|
24
14
|
end
|
25
|
-
task default: %w[
|
15
|
+
task default: %w[test]
|
data/bin/test-runner
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lock::File
|
4
|
+
# The constants found in this module are defined
|
5
|
+
# by unistd.h. Their documentation can be found in
|
6
|
+
# the
|
7
|
+
# [lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
8
|
+
# man page
|
9
|
+
module Constants
|
10
|
+
F_ULOCK = 0x0
|
11
|
+
F_LOCK = 0x1
|
12
|
+
F_TLOCK = 0x2
|
13
|
+
F_TEST = 0x3
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lock::File
|
4
|
+
module FFI
|
5
|
+
require "fiddle"
|
6
|
+
include Fiddle::Types
|
7
|
+
extend self
|
8
|
+
|
9
|
+
##
|
10
|
+
# Provides a Ruby interface for lockf(3)
|
11
|
+
#
|
12
|
+
# @see https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3 lockf(3)
|
13
|
+
# @param [Integer, #fileno] fd
|
14
|
+
# @param [Integer] function
|
15
|
+
# @param [Integer] size
|
16
|
+
# @raise [SystemCallError]
|
17
|
+
# Might raise a subclass of SystemCallError
|
18
|
+
# @return [Boolean]
|
19
|
+
# Returns true when successful
|
20
|
+
def lockf(fd, function, size = 0)
|
21
|
+
fileno = fd.respond_to?(:fileno) ? fd.fileno : fd
|
22
|
+
Fiddle::Function.new(
|
23
|
+
libc["lockf"],
|
24
|
+
[INT, INT, INT],
|
25
|
+
INT
|
26
|
+
).call(fileno, function, size)
|
27
|
+
.zero? || raise(SystemCallError.new("lockf", Fiddle.last_error))
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def libc
|
33
|
+
@libc ||= Fiddle.dlopen Dir["/lib/libc.*"].first
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/lock/file.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module Lock
|
2
|
+
end unless defined?(Lock)
|
3
|
+
|
4
|
+
##
|
5
|
+
# {Lock::File Lock::File} provides an object-oriented
|
6
|
+
# [lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)
|
7
|
+
# interface
|
8
|
+
class Lock::File
|
9
|
+
require "tmpdir"
|
10
|
+
require_relative "file/version"
|
11
|
+
require_relative "file/ffi"
|
12
|
+
require_relative "file/constants"
|
13
|
+
|
14
|
+
include FFI
|
15
|
+
include Constants
|
16
|
+
|
17
|
+
##
|
18
|
+
# @example
|
19
|
+
# lockf = Lock::File.temporary_file
|
20
|
+
# lockf.lock
|
21
|
+
# # ...
|
22
|
+
#
|
23
|
+
# @param [String] basename
|
24
|
+
# @param [String] tmpdir
|
25
|
+
# @return [Lock::File]
|
26
|
+
# Returns a {Lock::File Lock::File} for a random,
|
27
|
+
# unlinked temporary file
|
28
|
+
def self.from_temporary_file(basename: "lockf", tmpdir: Dir.tmpdir)
|
29
|
+
require "tempfile" unless defined?(Tempfile)
|
30
|
+
file = Tempfile.new(basename, tmpdir:).tap(&:unlink)
|
31
|
+
Lock::File.new(file)
|
32
|
+
end
|
33
|
+
class << self
|
34
|
+
alias_method :temporary_file, :from_temporary_file
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# @return [<#fileno>]
|
39
|
+
# Returns a file handle
|
40
|
+
attr_reader :file
|
41
|
+
|
42
|
+
##
|
43
|
+
# @param [<#fileno>] file
|
44
|
+
# @param [Integer] size
|
45
|
+
# @return [Lock::File]
|
46
|
+
# Returns an instance of {Lock::File Lock::File}
|
47
|
+
def initialize(file, size = 0)
|
48
|
+
@file = file
|
49
|
+
@size = size
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Acquire lock (blocking)
|
54
|
+
#
|
55
|
+
# @see https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3 lockf(3)
|
56
|
+
# @raise [SystemCallError]
|
57
|
+
# Might raise a subclass of SystemCallError
|
58
|
+
# @return [Boolean]
|
59
|
+
# Returns true when successful
|
60
|
+
def lock
|
61
|
+
tries ||= 0
|
62
|
+
lockf(@file, F_LOCK, @size)
|
63
|
+
rescue Errno::EINTR => ex
|
64
|
+
tries += 1
|
65
|
+
tries == 3 ? raise(ex) : retry
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Acquire lock (non-blocking)
|
70
|
+
#
|
71
|
+
# @see https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3 lockf(3)
|
72
|
+
# @raise [SystemCallError]
|
73
|
+
# Might raise a subclass of SystemCallError
|
74
|
+
# @return [Boolean]
|
75
|
+
# Returns true when successful
|
76
|
+
def lock_nonblock
|
77
|
+
lockf(@file, F_TLOCK, @size)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Release lock
|
82
|
+
#
|
83
|
+
# @see https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3 lockf(3)
|
84
|
+
# @raise [SystemCallError]
|
85
|
+
# Might raise a subclass of SystemCallError
|
86
|
+
# @return [Boolean]
|
87
|
+
# Returns true when successful
|
88
|
+
def release
|
89
|
+
lockf(@file, F_ULOCK, @size)
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# @return [Boolean]
|
94
|
+
# Returns true when lock is held by another process
|
95
|
+
def locked?
|
96
|
+
lockf(@file, F_TEST, @size)
|
97
|
+
false
|
98
|
+
rescue Errno::EACCES, Errno::EAGAIN
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Closes {Lock::File#file Lock::File#file}
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
# # Equivalent to:
|
107
|
+
# lockf = Lock::File.temporary_file
|
108
|
+
# lockf.file.close
|
109
|
+
# @return [void]
|
110
|
+
def close
|
111
|
+
return unless @file.respond_to?(:close)
|
112
|
+
@file.close
|
113
|
+
end
|
114
|
+
end
|
data/lib/lockf.rb
CHANGED
@@ -1,139 +1 @@
|
|
1
|
-
|
2
|
-
# The
|
3
|
-
# [`LockFile`](https://0x1eef.github.io/x/lockf.rb/LockFile.html)
|
4
|
-
# class provides a Ruby-oriented interface to the C function
|
5
|
-
# [lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3).
|
6
|
-
class LockFile
|
7
|
-
require "tmpdir"
|
8
|
-
require_relative "lockf.rb.so"
|
9
|
-
|
10
|
-
##
|
11
|
-
# @!method self.lockf(fd, cmd, len)
|
12
|
-
# @example
|
13
|
-
# LockFile.lockf(5, LockFile::F_LOCK, 0)
|
14
|
-
#
|
15
|
-
# @param [Integer] fd
|
16
|
-
# A number that represents a file descriptor.
|
17
|
-
#
|
18
|
-
# @param [Integer] cmd
|
19
|
-
# {LockFile::F_LOCK}, {LockFile::F_TLOCK}, {LockFile::F_ULOCK}, or
|
20
|
-
# {LockFile::F_TEST}.
|
21
|
-
#
|
22
|
-
# @param [Integer] len
|
23
|
-
# The number of bytes to place a lock on.
|
24
|
-
# A value of "0" covers the entire file.
|
25
|
-
#
|
26
|
-
# @raise [SystemCallError]
|
27
|
-
# Might raise a number of Errno exception classes.
|
28
|
-
#
|
29
|
-
# @return [Integer]
|
30
|
-
# Returns 0 on success.
|
31
|
-
#
|
32
|
-
# @see (https://man7.org/linux/man-pages/man3/lockf.3.html) lockf man page (Linux)
|
33
|
-
# @see (https://man.openbsd.org/lockf.3) lockf man page (OpenBSD)
|
34
|
-
# @see (https://www.freebsd.org/cgi/man.cgi?query=lockf) lockf man page (FreeBSD)
|
35
|
-
|
36
|
-
##
|
37
|
-
# @example
|
38
|
-
# lockf = LockFile.temporary_file
|
39
|
-
# lockf.lock
|
40
|
-
# lockf.release
|
41
|
-
# lockf.file.close
|
42
|
-
#
|
43
|
-
# @param [String] basename
|
44
|
-
# The basename of the temporary file.
|
45
|
-
#
|
46
|
-
# @param [String] tmpdir
|
47
|
-
# The path to the parent directory of the temporary file.
|
48
|
-
#
|
49
|
-
# @return [LockFile]
|
50
|
-
# Returns an instance of {LockFile LockFile} backed by an
|
51
|
-
# unlinked instance of Tempfile.
|
52
|
-
def self.from_temporary_file(basename: "lockf", tmpdir: Dir.tmpdir)
|
53
|
-
require "tempfile" unless defined?(Tempfile)
|
54
|
-
file = Tempfile.new(basename, tmpdir:).tap(&:unlink)
|
55
|
-
LockFile.new(file)
|
56
|
-
end
|
57
|
-
class << self
|
58
|
-
alias_method :temporary_file, :from_temporary_file
|
59
|
-
end
|
60
|
-
|
61
|
-
##
|
62
|
-
# @return [<File, Tempfile, #fileno>]
|
63
|
-
# Returns a file object.
|
64
|
-
attr_reader :file
|
65
|
-
|
66
|
-
##
|
67
|
-
# @param [<File, TempFile, String, #fileno>] file
|
68
|
-
# The file to place a lock on.
|
69
|
-
#
|
70
|
-
# @param [Integer] len
|
71
|
-
# The number of bytes to place a lock on.
|
72
|
-
# A value of "0" covers the entire file.
|
73
|
-
#
|
74
|
-
# @return [LockFile]
|
75
|
-
# Returns an instance of {LockFile LockFile}.
|
76
|
-
def initialize(file, len = 0)
|
77
|
-
@file = String === file ? File.open(file, "r+") : file
|
78
|
-
@len = len
|
79
|
-
end
|
80
|
-
|
81
|
-
##
|
82
|
-
# Acquire a lock (blocking)
|
83
|
-
# @raise [Errno::EBADF]
|
84
|
-
# @raise [Errno::EDEADLK]
|
85
|
-
# @raise [Errno::EINTR]
|
86
|
-
# @raise [Errno::ENOLCK]
|
87
|
-
# @return [Integer]
|
88
|
-
def lock
|
89
|
-
attempts ||= 0
|
90
|
-
LockFile.lockf(@file.fileno, F_LOCK, @len)
|
91
|
-
rescue Errno::EINTR => ex
|
92
|
-
attempts += 1
|
93
|
-
attempts == 3 ? raise(ex) : retry
|
94
|
-
end
|
95
|
-
|
96
|
-
##
|
97
|
-
# Acquire a lock (non-blocking)
|
98
|
-
# @raise [Errno::EAGAIN]
|
99
|
-
# @raise [Errno::EBADF]
|
100
|
-
# @raise [Errno::ENOLCK]
|
101
|
-
# @raise [Errno::EINVAL]
|
102
|
-
# @return [Integer]
|
103
|
-
def lock_nonblock
|
104
|
-
LockFile.lockf(@file.fileno, F_TLOCK, @len)
|
105
|
-
end
|
106
|
-
|
107
|
-
##
|
108
|
-
# Release a lock
|
109
|
-
# @raise [Errno::EBADF]
|
110
|
-
# @raise [Errno::ENOLCK]
|
111
|
-
# @return [Integer]
|
112
|
-
def release
|
113
|
-
LockFile.lockf(@file.fileno, F_ULOCK, @len)
|
114
|
-
end
|
115
|
-
|
116
|
-
##
|
117
|
-
# @return [Boolean]
|
118
|
-
# Returns true when a lock has been acquired by another process.
|
119
|
-
def locked?
|
120
|
-
LockFile.lockf(@file.fileno, F_TEST, @len)
|
121
|
-
false
|
122
|
-
rescue Errno::EACCES, Errno::EAGAIN
|
123
|
-
true
|
124
|
-
end
|
125
|
-
|
126
|
-
##
|
127
|
-
# Closes {LockFile#file LockFile#file}.
|
128
|
-
#
|
129
|
-
# @example
|
130
|
-
# # Equivalent to:
|
131
|
-
# lockf = LockFile.temporary_file
|
132
|
-
# lockf.file.close
|
133
|
-
#
|
134
|
-
# @return [void]
|
135
|
-
def close
|
136
|
-
return unless @file.respond_to?(:close)
|
137
|
-
@file.close
|
138
|
-
end
|
139
|
-
end
|
1
|
+
require_relative "lock/file"
|
data/lib/lockfile.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "lock/file"
|
data/lockf.rb.gemspec
CHANGED
@@ -1,24 +1,21 @@
|
|
1
|
-
|
1
|
+
require_relative "lib/lockfile"
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
4
|
gem.name = "lockf.rb"
|
5
5
|
gem.authors = ["0x1eef"]
|
6
6
|
gem.email = ["0x1eef@protonmail.com"]
|
7
7
|
gem.homepage = "https://github.com/0x1eef/lockf.rb#readme"
|
8
|
-
gem.version =
|
8
|
+
gem.version = Lock::File::VERSION
|
9
9
|
gem.licenses = ["0BSD"]
|
10
10
|
gem.files = `git ls-files`.split($/)
|
11
11
|
gem.require_paths = ["lib"]
|
12
|
-
gem.extensions = %w[ext/lockf.rb/extconf.rb]
|
13
12
|
gem.summary = "Ruby bindings for lockf(3)"
|
14
13
|
gem.description = gem.summary
|
14
|
+
|
15
|
+
gem.add_runtime_dependency "fiddle", "~> 1.1"
|
15
16
|
gem.add_development_dependency "yard", "~> 0.9"
|
16
|
-
gem.add_development_dependency "
|
17
|
-
gem.add_development_dependency "
|
18
|
-
gem.add_development_dependency "
|
19
|
-
gem.add_development_dependency "test-
|
20
|
-
gem.add_development_dependency "rake-compiler", "= 1.2.0"
|
21
|
-
gem.add_development_dependency "rack", "~> 3.0"
|
22
|
-
gem.add_development_dependency "rackup", "~> 2.1"
|
23
|
-
gem.add_development_dependency "test-cmd.rb", "~> 0.4"
|
17
|
+
gem.add_development_dependency "standard", "~> 1.12"
|
18
|
+
gem.add_development_dependency "rubocop", "~> 1.29"
|
19
|
+
gem.add_development_dependency "test-unit", "~> 3.5"
|
20
|
+
gem.add_development_dependency "test-cmd.rb", "~> 0.12"
|
24
21
|
end
|
@@ -1,17 +1,18 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
|
4
|
+
require "lock/file"
|
5
|
+
lockf = Lock::File.temporary_file
|
4
6
|
lockf.lock
|
5
7
|
print "Lock acquired by parent process (#{Time.now.utc})", "\n"
|
6
|
-
|
8
|
+
fork do
|
7
9
|
print "Child process waiting on lock (#{Time.now.utc})", "\n"
|
8
10
|
lockf.lock
|
9
11
|
print "Lock acquired by child process (#{Time.now.utc})", "\n"
|
10
12
|
end
|
11
13
|
sleep(3)
|
12
14
|
lockf.release
|
13
|
-
Process.wait
|
14
|
-
lockf.file.close
|
15
|
+
Process.wait
|
15
16
|
|
16
17
|
##
|
17
18
|
# Lock acquired by parent process (2023-02-11 16:43:15 UTC)
|
@@ -1,9 +1,11 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
|
4
|
+
require "lock/file"
|
5
|
+
lockf = Lock::File.temporary_file
|
4
6
|
lockf.lock_nonblock
|
5
7
|
print "Lock acquired by parent process (#{Time.now.utc})", "\n"
|
6
|
-
|
8
|
+
fork do
|
7
9
|
lockf.lock_nonblock
|
8
10
|
print "Lock acquired by child process (#{Time.now.utc})", "\n"
|
9
11
|
rescue Errno::EWOULDBLOCK
|
@@ -13,8 +15,7 @@ rescue Errno::EWOULDBLOCK
|
|
13
15
|
end
|
14
16
|
sleep 3
|
15
17
|
lockf.release
|
16
|
-
Process.wait
|
17
|
-
lockf.file.close
|
18
|
+
Process.wait
|
18
19
|
|
19
20
|
##
|
20
21
|
# Lock acquired by parent process (2023-02-11 19:03:05 UTC)
|
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "lock/file"
|
5
|
+
require "tempfile"
|
4
6
|
file = Tempfile.new("lockf-ffi").tap(&:unlink)
|
5
|
-
|
7
|
+
Lock::File::FFI.lockf(file, Lock::File::F_LOCK, 0)
|
6
8
|
print "Lock acquired", "\n"
|
7
|
-
|
9
|
+
Lock::File::FFI.lockf(file, Lock::File::F_ULOCK, 0)
|
8
10
|
print "Lock released", "\n"
|
9
11
|
file.close
|
10
12
|
|
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "lock/file"
|
5
|
+
require "tempfile"
|
4
6
|
file = Tempfile.new("lockf-ffi").tap(&:unlink)
|
5
|
-
|
7
|
+
Lock::File::FFI.lockf(file, Lock::File::F_TLOCK, 0)
|
6
8
|
print "Lock acquired", "\n"
|
7
|
-
|
9
|
+
Lock::File::FFI.lockf(file, Lock::File::F_ULOCK, 0)
|
8
10
|
print "Lock released", "\n"
|
9
11
|
file.close
|
10
12
|
|
data/test/lock_file_test.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require_relative "setup"
|
2
|
-
|
3
|
+
|
4
|
+
class Lock::File::Test < Test::Unit::TestCase
|
3
5
|
attr_reader :file
|
4
6
|
attr_reader :lockf
|
5
7
|
|
6
8
|
def setup
|
7
|
-
@file
|
8
|
-
@lockf =
|
9
|
+
@file = Tempfile.new("lockf-test").tap(&:unlink)
|
10
|
+
@lockf = Lock::File.new(file)
|
9
11
|
end
|
10
12
|
|
11
13
|
def teardown
|
@@ -13,9 +15,9 @@ class LockFile::Test < Test::Unit::TestCase
|
|
13
15
|
end
|
14
16
|
|
15
17
|
##
|
16
|
-
#
|
18
|
+
# LockFile#lock
|
17
19
|
def test_lock
|
18
|
-
assert_equal
|
20
|
+
assert_equal true, lockf.lock
|
19
21
|
ensure
|
20
22
|
lockf.release
|
21
23
|
end
|
@@ -30,9 +32,9 @@ class LockFile::Test < Test::Unit::TestCase
|
|
30
32
|
end
|
31
33
|
|
32
34
|
##
|
33
|
-
#
|
35
|
+
# LockFile#lock_nonblock
|
34
36
|
def test_lock_nonblock
|
35
|
-
assert_equal
|
37
|
+
assert_equal true, lockf.lock_nonblock
|
36
38
|
ensure
|
37
39
|
lockf.release
|
38
40
|
end
|
@@ -47,7 +49,7 @@ class LockFile::Test < Test::Unit::TestCase
|
|
47
49
|
end
|
48
50
|
|
49
51
|
##
|
50
|
-
#
|
52
|
+
# LockFile#locked?
|
51
53
|
def test_locked?
|
52
54
|
pid = fork_sleep { lockf.lock }
|
53
55
|
sleep(0.1)
|
@@ -60,9 +62,9 @@ class LockFile::Test < Test::Unit::TestCase
|
|
60
62
|
##
|
61
63
|
# LockFile.temporary_file
|
62
64
|
def test_temporary_file
|
63
|
-
lockf =
|
64
|
-
assert_equal
|
65
|
-
assert_equal
|
65
|
+
lockf = Lock::File.temporary_file
|
66
|
+
assert_equal true, lockf.lock
|
67
|
+
assert_equal true, lockf.release
|
66
68
|
ensure
|
67
69
|
lockf.file.close
|
68
70
|
end
|
data/test/readme_test.rb
CHANGED
@@ -3,40 +3,40 @@
|
|
3
3
|
require_relative "setup"
|
4
4
|
require "test/cmd"
|
5
5
|
|
6
|
-
class
|
7
|
-
include Test::Cmd
|
8
|
-
|
6
|
+
class Lock::File::ReadmeTest < Test::Unit::TestCase
|
9
7
|
def test_lockfile_blocking_variant
|
10
|
-
r =
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
r = ruby(readme_example("1_lockfile_blocking_variant.rb"))
|
9
|
+
["Lock acquired by parent process \(.+\)\n",
|
10
|
+
"Child process waiting on lock \(.+\)\n",
|
11
|
+
"Lock acquired by child process \(.+\)\n"
|
12
|
+
].each { assert_match Regexp.new(_1), r.stdout }
|
15
13
|
end
|
16
14
|
|
17
15
|
def test_lockfile_nonblocking_variant
|
18
|
-
r =
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
r = ruby(readme_example("2_lockfile_nonblocking_variant.rb"))
|
17
|
+
["Lock acquired by parent process \(.+\)\n",
|
18
|
+
"(Lock would block\n){3,4}",
|
19
|
+
"Lock acquired by child process \(.+\)\n"
|
20
|
+
].each { assert_match Regexp.new(_1), r.stdout }
|
23
21
|
end
|
24
22
|
|
25
23
|
def test_ffi_lockf_blocking_variant
|
26
24
|
assert_equal "Lock acquired\nLock released\n",
|
27
|
-
readme_example("3_ffilockf_blocking_variant.rb").stdout
|
25
|
+
ruby(readme_example("3_ffilockf_blocking_variant.rb")).stdout
|
28
26
|
end
|
29
27
|
|
30
28
|
def test_ffi_lockf_nonblocking_variant
|
31
29
|
assert_equal "Lock acquired\nLock released\n",
|
32
|
-
readme_example("4_ffilockf_nonblocking_variant.rb").stdout
|
30
|
+
ruby(readme_example("4_ffilockf_nonblocking_variant.rb")).stdout
|
33
31
|
end
|
34
32
|
|
35
33
|
private
|
36
34
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
def ruby(*argv)
|
36
|
+
cmd("ruby", *argv)
|
37
|
+
end
|
38
|
+
|
39
|
+
def readme_example(example_name)
|
40
|
+
File.join(__dir__, "..", "share", "lockf.rb", "examples", example_name)
|
41
41
|
end
|
42
42
|
end
|
data/test/setup.rb
CHANGED
metadata
CHANGED
@@ -1,150 +1,108 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockf.rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- '0x1eef'
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: fiddle
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
type: :
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '1.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: yard
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0.9'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0.9'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: standard
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - '='
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 1.12.1
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - '='
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 1.12.1
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rubocop
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - '='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.29.1
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - '='
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 1.29.1
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: test-unit
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
72
44
|
requirements:
|
73
45
|
- - "~>"
|
74
46
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
47
|
+
version: '1.12'
|
76
48
|
type: :development
|
77
49
|
prerelease: false
|
78
50
|
version_requirements: !ruby/object:Gem::Requirement
|
79
51
|
requirements:
|
80
52
|
- - "~>"
|
81
53
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
54
|
+
version: '1.12'
|
83
55
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - '='
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: 1.2.0
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - '='
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 1.2.0
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rack
|
56
|
+
name: rubocop
|
99
57
|
requirement: !ruby/object:Gem::Requirement
|
100
58
|
requirements:
|
101
59
|
- - "~>"
|
102
60
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
61
|
+
version: '1.29'
|
104
62
|
type: :development
|
105
63
|
prerelease: false
|
106
64
|
version_requirements: !ruby/object:Gem::Requirement
|
107
65
|
requirements:
|
108
66
|
- - "~>"
|
109
67
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
68
|
+
version: '1.29'
|
111
69
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
70
|
+
name: test-unit
|
113
71
|
requirement: !ruby/object:Gem::Requirement
|
114
72
|
requirements:
|
115
73
|
- - "~>"
|
116
74
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
75
|
+
version: '3.5'
|
118
76
|
type: :development
|
119
77
|
prerelease: false
|
120
78
|
version_requirements: !ruby/object:Gem::Requirement
|
121
79
|
requirements:
|
122
80
|
- - "~>"
|
123
81
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
82
|
+
version: '3.5'
|
125
83
|
- !ruby/object:Gem::Dependency
|
126
84
|
name: test-cmd.rb
|
127
85
|
requirement: !ruby/object:Gem::Requirement
|
128
86
|
requirements:
|
129
87
|
- - "~>"
|
130
88
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0.
|
89
|
+
version: '0.12'
|
132
90
|
type: :development
|
133
91
|
prerelease: false
|
134
92
|
version_requirements: !ruby/object:Gem::Requirement
|
135
93
|
requirements:
|
136
94
|
- - "~>"
|
137
95
|
- !ruby/object:Gem::Version
|
138
|
-
version: '0.
|
96
|
+
version: '0.12'
|
139
97
|
description: Ruby bindings for lockf(3)
|
140
98
|
email:
|
141
99
|
- 0x1eef@protonmail.com
|
142
100
|
executables: []
|
143
|
-
extensions:
|
144
|
-
- ext/lockf.rb/extconf.rb
|
101
|
+
extensions: []
|
145
102
|
extra_rdoc_files: []
|
146
103
|
files:
|
147
104
|
- ".bundle/config"
|
105
|
+
- ".clang-format"
|
148
106
|
- ".github/workflows/tests.yml"
|
149
107
|
- ".gitignore"
|
150
108
|
- ".projectile"
|
@@ -155,11 +113,13 @@ files:
|
|
155
113
|
- LICENSE
|
156
114
|
- README.md
|
157
115
|
- Rakefile.rb
|
158
|
-
-
|
159
|
-
-
|
160
|
-
-
|
116
|
+
- bin/test-runner
|
117
|
+
- lib/lock/file.rb
|
118
|
+
- lib/lock/file/constants.rb
|
119
|
+
- lib/lock/file/ffi.rb
|
120
|
+
- lib/lock/file/version.rb
|
161
121
|
- lib/lockf.rb
|
162
|
-
- lib/
|
122
|
+
- lib/lockfile.rb
|
163
123
|
- lockf.rb.gemspec
|
164
124
|
- share/lockf.rb/examples/1_lockfile_blocking_variant.rb
|
165
125
|
- share/lockf.rb/examples/2_lockfile_nonblocking_variant.rb
|
@@ -187,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
147
|
- !ruby/object:Gem::Version
|
188
148
|
version: '0'
|
189
149
|
requirements: []
|
190
|
-
rubygems_version: 3.5.
|
150
|
+
rubygems_version: 3.5.11
|
191
151
|
signing_key:
|
192
152
|
specification_version: 4
|
193
153
|
summary: Ruby bindings for lockf(3)
|
data/ext/lockf.rb/extconf.rb
DELETED
data/ext/lockf.rb/lockf.c
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
#include <ruby.h>
|
2
|
-
#include <unistd.h>
|
3
|
-
#include <errno.h>
|
4
|
-
#include "lockf.h"
|
5
|
-
|
6
|
-
static VALUE
|
7
|
-
lockf_lock(VALUE self, VALUE fd, VALUE cmd, VALUE len)
|
8
|
-
{
|
9
|
-
int result;
|
10
|
-
|
11
|
-
Check_Type(fd, T_FIXNUM);
|
12
|
-
Check_Type(cmd, T_FIXNUM);
|
13
|
-
Check_Type(len, T_FIXNUM);
|
14
|
-
errno = 0;
|
15
|
-
result = lockf(NUM2INT(fd), NUM2INT(cmd), NUM2INT(len));
|
16
|
-
if (result == 0) {
|
17
|
-
return INT2NUM(result);
|
18
|
-
} else {
|
19
|
-
rb_syserr_fail(errno, "lockf");
|
20
|
-
}
|
21
|
-
}
|
22
|
-
|
23
|
-
void
|
24
|
-
Init_lockf(void)
|
25
|
-
{
|
26
|
-
VALUE cLockf;
|
27
|
-
|
28
|
-
cLockf = rb_define_class("LockFile", rb_cObject);
|
29
|
-
rb_define_const(cLockf, "F_LOCK", INT2NUM(F_LOCK));
|
30
|
-
rb_define_const(cLockf, "F_TLOCK", INT2NUM(F_TLOCK));
|
31
|
-
rb_define_const(cLockf, "F_ULOCK", INT2NUM(F_ULOCK));
|
32
|
-
rb_define_const(cLockf, "F_TEST", INT2NUM(F_TEST));
|
33
|
-
rb_define_singleton_method(cLockf, "lockf", lockf_lock, 3);
|
34
|
-
}
|
data/ext/lockf.rb/lockf.h
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
void Init_lockf(void);
|
data/lib/lockf/version.rb
DELETED