@minecraft-docker/mcctl 1.6.13 → 1.6.15

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.
@@ -0,0 +1,248 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # common.sh - Shared functions for mcctl scripts
4
+ # =============================================================================
5
+ # Source this file in other scripts:
6
+ # source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
7
+ #
8
+ # When running via npm package (mcctl CLI), these environment variables are set:
9
+ # MCCTL_ROOT - User data directory (~/.minecraft-servers)
10
+ # MCCTL_SCRIPTS - Package scripts directory
11
+ # MCCTL_TEMPLATES - Package templates directory
12
+ # =============================================================================
13
+
14
+ # Get script/platform directories
15
+ # Support both direct execution and npm package execution
16
+ if [[ -n "${MCCTL_ROOT:-}" ]]; then
17
+ # Running via npm package (mcctl CLI)
18
+ PLATFORM_DIR="$MCCTL_ROOT"
19
+ SCRIPT_DIR="${MCCTL_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
20
+ TEMPLATES_DIR="${MCCTL_TEMPLATES:-$PLATFORM_DIR/servers/_template}"
21
+ else
22
+ # Running directly (development mode)
23
+ SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
24
+ PLATFORM_DIR="${PLATFORM_DIR:-$(dirname "$SCRIPT_DIR")}"
25
+ TEMPLATES_DIR="$PLATFORM_DIR/servers/_template"
26
+ fi
27
+
28
+ # Export for sub-scripts
29
+ export PLATFORM_DIR SCRIPT_DIR TEMPLATES_DIR
30
+
31
+ # Colors (disabled if not terminal or JSON output)
32
+ setup_colors() {
33
+ if [[ -t 1 ]] && [[ "${JSON_OUTPUT:-}" != "true" ]]; then
34
+ RED='\033[0;31m'
35
+ GREEN='\033[0;32m'
36
+ YELLOW='\033[1;33m'
37
+ BLUE='\033[0;34m'
38
+ CYAN='\033[0;36m'
39
+ BOLD='\033[1m'
40
+ NC='\033[0m'
41
+ else
42
+ RED=''
43
+ GREEN=''
44
+ YELLOW=''
45
+ BLUE=''
46
+ CYAN=''
47
+ BOLD=''
48
+ NC=''
49
+ fi
50
+ }
51
+ setup_colors
52
+
53
+ # =============================================================================
54
+ # Output Functions
55
+ # =============================================================================
56
+
57
+ error() {
58
+ echo -e "${RED}[ERROR]${NC} $1" >&2
59
+ }
60
+
61
+ warn() {
62
+ echo -e "${YELLOW}[WARN]${NC} $1" >&2
63
+ }
64
+
65
+ info() {
66
+ echo -e "${GREEN}[INFO]${NC} $1"
67
+ }
68
+
69
+ debug() {
70
+ if [[ "${DEBUG:-}" == "true" ]]; then
71
+ echo -e "${BLUE}[DEBUG]${NC} $1" >&2
72
+ fi
73
+ }
74
+
75
+ # =============================================================================
76
+ # Sudo Functions
77
+ # =============================================================================
78
+ # Supports MCCTL_SUDO_PASSWORD environment variable for automation
79
+ # When set, uses sudo -S to pipe password via stdin
80
+
81
+ # Run command with sudo, supporting MCCTL_SUDO_PASSWORD for automation
82
+ # Usage: run_with_sudo <command> [args...]
83
+ # Example: run_with_sudo tee -a /etc/avahi/hosts
84
+ # Example: run_with_sudo systemctl restart avahi-daemon
85
+ run_with_sudo() {
86
+ if [[ -z "$*" ]]; then
87
+ error "run_with_sudo: No command specified"
88
+ return 1
89
+ fi
90
+
91
+ if [[ -n "${MCCTL_SUDO_PASSWORD:-}" ]]; then
92
+ # Automation mode: use sudo -S to read password from stdin
93
+ debug "Using MCCTL_SUDO_PASSWORD for sudo"
94
+ echo "$MCCTL_SUDO_PASSWORD" | sudo -S "$@" 2>/dev/null
95
+ else
96
+ # Interactive mode: let sudo prompt for password
97
+ sudo "$@"
98
+ fi
99
+ }
100
+
101
+ # Run command with sudo and capture output (for commands like tee)
102
+ # Usage: echo "content" | run_with_sudo_stdin <command> [args...]
103
+ # Example: echo "$ip $hostname" | run_with_sudo_stdin tee -a /etc/avahi/hosts
104
+ run_with_sudo_stdin() {
105
+ if [[ -z "$*" ]]; then
106
+ error "run_with_sudo_stdin: No command specified"
107
+ return 1
108
+ fi
109
+
110
+ if [[ -n "${MCCTL_SUDO_PASSWORD:-}" ]]; then
111
+ # Automation mode: need to handle both password and stdin content
112
+ # Use expect-like approach with heredoc
113
+ local stdin_content
114
+ stdin_content=$(cat)
115
+ debug "Using MCCTL_SUDO_PASSWORD for sudo with stdin"
116
+ {
117
+ echo "$MCCTL_SUDO_PASSWORD"
118
+ echo "$stdin_content"
119
+ } | sudo -S "$@" 2>/dev/null
120
+ else
121
+ # Interactive mode: let sudo prompt for password, pass stdin through
122
+ sudo "$@"
123
+ fi
124
+ }
125
+
126
+ # Check if sudo password is configured for automation
127
+ has_sudo_password() {
128
+ [[ -n "${MCCTL_SUDO_PASSWORD:-}" ]]
129
+ }
130
+
131
+ # Validate sudo password (test if it works)
132
+ validate_sudo_password() {
133
+ if [[ -z "${MCCTL_SUDO_PASSWORD:-}" ]]; then
134
+ return 1
135
+ fi
136
+ echo "$MCCTL_SUDO_PASSWORD" | sudo -S -v 2>/dev/null
137
+ }
138
+
139
+ # =============================================================================
140
+ # Docker Functions
141
+ # =============================================================================
142
+
143
+ # Check if Docker is available
144
+ check_docker() {
145
+ if ! command -v docker &> /dev/null; then
146
+ error "Docker is not installed or not in PATH"
147
+ return 1
148
+ fi
149
+ if ! docker info &> /dev/null; then
150
+ error "Docker daemon is not running or permission denied"
151
+ return 1
152
+ fi
153
+ return 0
154
+ }
155
+
156
+ # Get container status (running, exited, paused, etc.)
157
+ get_container_status() {
158
+ local container="$1"
159
+ local status
160
+ status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null) || status="not_found"
161
+ echo -n "$status"
162
+ }
163
+
164
+ # Check if container exists
165
+ container_exists() {
166
+ local container="$1"
167
+ docker inspect "$container" &>/dev/null
168
+ }
169
+
170
+ # Get container health status
171
+ get_container_health() {
172
+ local container="$1"
173
+ local health
174
+ health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container" 2>/dev/null) || health="unknown"
175
+ echo -n "$health"
176
+ }
177
+
178
+ # Get minecraft server containers (those starting with mc-)
179
+ get_mc_containers() {
180
+ docker ps -a --filter "name=mc-" --format '{{.Names}}' | grep -v "mc-router" | sort
181
+ }
182
+
183
+ # Get running minecraft server containers
184
+ get_running_mc_containers() {
185
+ docker ps --filter "name=mc-" --filter "status=running" --format '{{.Names}}' | grep -v "mc-router" | sort
186
+ }
187
+
188
+ # Get container's assigned world (from environment or volume mount)
189
+ get_container_world() {
190
+ local container="$1"
191
+ # Check WORLD environment variable first
192
+ local world=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$container" 2>/dev/null | grep "^WORLD=" | cut -d= -f2)
193
+ if [[ -n "$world" ]]; then
194
+ echo "$world"
195
+ return
196
+ fi
197
+ # Otherwise try to get from volume mount
198
+ echo "-"
199
+ }
200
+
201
+ # Get container's hostname label (mc-router.host)
202
+ get_container_hostname() {
203
+ local container="$1"
204
+ local hostname
205
+ hostname=$(docker inspect --format '{{index .Config.Labels "mc-router.host"}}' "$container" 2>/dev/null) || hostname="-"
206
+ [[ -z "$hostname" ]] && hostname="-"
207
+ echo -n "$hostname"
208
+ }
209
+
210
+ # =============================================================================
211
+ # JSON Output Functions
212
+ # =============================================================================
213
+
214
+ # Escape string for JSON
215
+ json_escape() {
216
+ local str="$1"
217
+ str="${str//\\/\\\\}"
218
+ str="${str//\"/\\\"}"
219
+ str="${str//$'\n'/\\n}"
220
+ str="${str//$'\r'/\\r}"
221
+ str="${str//$'\t'/\\t}"
222
+ echo "$str"
223
+ }
224
+
225
+ # =============================================================================
226
+ # Path Functions
227
+ # =============================================================================
228
+
229
+ get_servers_dir() {
230
+ echo "$PLATFORM_DIR/servers"
231
+ }
232
+
233
+ get_worlds_dir() {
234
+ echo "$PLATFORM_DIR/worlds"
235
+ }
236
+
237
+ get_locks_dir() {
238
+ echo "$PLATFORM_DIR/worlds/.locks"
239
+ }
240
+
241
+ # List all server directories
242
+ get_server_names() {
243
+ local servers_dir
244
+ servers_dir=$(get_servers_dir)
245
+ if [[ -d "$servers_dir" ]]; then
246
+ find "$servers_dir" -maxdepth 1 -mindepth 1 -type d ! -name '_template' -exec basename {} \; 2>/dev/null | sort
247
+ fi
248
+ }
@@ -0,0 +1,448 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # lock.sh - World locking system for Minecraft servers
4
+ # =============================================================================
5
+ # Provides flock-based exclusive locking to prevent simultaneous world access.
6
+ #
7
+ # Usage:
8
+ # ./scripts/lock.sh lock <world> <server> # Acquire lock
9
+ # ./scripts/lock.sh unlock <world> <server> # Release lock
10
+ # ./scripts/lock.sh check <world> # Check lock status
11
+ # ./scripts/lock.sh list # List all locks
12
+ # ./scripts/lock.sh list --json # JSON output
13
+ #
14
+ # Lock file format:
15
+ # worlds/.locks/<world-name>.lock
16
+ # Content: <server-name>:<timestamp>:<pid>
17
+ #
18
+ # Exit codes:
19
+ # 0 - Success
20
+ # 1 - Error (lock failed, invalid args, etc.)
21
+ # 2 - Warning (lock already held, stale lock detected)
22
+ # =============================================================================
23
+
24
+ set -e
25
+
26
+ # Get script/platform directories
27
+ # Support both direct execution and npm package execution (mcctl CLI)
28
+ if [[ -n "${MCCTL_ROOT:-}" ]]; then
29
+ # Running via npm package
30
+ PLATFORM_DIR="$MCCTL_ROOT"
31
+ SCRIPT_DIR="${MCCTL_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
32
+ else
33
+ # Running directly (development mode)
34
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ PLATFORM_DIR="$(dirname "$SCRIPT_DIR")"
36
+ fi
37
+
38
+ WORLDS_DIR="$PLATFORM_DIR/worlds"
39
+ LOCKS_DIR="$WORLDS_DIR/.locks"
40
+
41
+ # Configuration
42
+ STALE_THRESHOLD=86400 # 24 hours in seconds
43
+
44
+ # Colors (disabled if not terminal)
45
+ if [[ -t 1 ]]; then
46
+ RED='\033[0;31m'
47
+ GREEN='\033[0;32m'
48
+ YELLOW='\033[1;33m'
49
+ BLUE='\033[0;34m'
50
+ NC='\033[0m'
51
+ else
52
+ RED=''
53
+ GREEN=''
54
+ YELLOW=''
55
+ BLUE=''
56
+ NC=''
57
+ fi
58
+
59
+ # =============================================================================
60
+ # Helper Functions
61
+ # =============================================================================
62
+
63
+ usage() {
64
+ cat <<EOF
65
+ Usage: $(basename "$0") <command> [options]
66
+
67
+ Commands:
68
+ lock <world> <server> Acquire exclusive lock on world for server
69
+ unlock <world> <server> Release lock (only owner can release)
70
+ check <world> Check if world is locked
71
+ list [--json] List all worlds and their lock status
72
+
73
+ Options:
74
+ --json Output in JSON format (for list command)
75
+ --force Force unlock (admin override, use with caution)
76
+
77
+ Exit codes:
78
+ 0 - Success
79
+ 1 - Error
80
+ 2 - Warning (lock held by another, stale lock)
81
+
82
+ Examples:
83
+ $(basename "$0") lock survival mc-ironwood
84
+ $(basename "$0") unlock survival mc-ironwood
85
+ $(basename "$0") check survival
86
+ $(basename "$0") list --json
87
+ EOF
88
+ }
89
+
90
+ error() {
91
+ echo -e "${RED}[ERROR]${NC} $1" >&2
92
+ }
93
+
94
+ warn() {
95
+ echo -e "${YELLOW}[WARN]${NC} $1" >&2
96
+ }
97
+
98
+ info() {
99
+ echo -e "${GREEN}[INFO]${NC} $1"
100
+ }
101
+
102
+ debug() {
103
+ if [[ "${DEBUG:-}" == "true" ]]; then
104
+ echo -e "${BLUE}[DEBUG]${NC} $1" >&2
105
+ fi
106
+ }
107
+
108
+ # Ensure locks directory exists
109
+ ensure_locks_dir() {
110
+ if [[ ! -d "$LOCKS_DIR" ]]; then
111
+ mkdir -p "$LOCKS_DIR"
112
+ debug "Created locks directory: $LOCKS_DIR"
113
+ fi
114
+ }
115
+
116
+ # Get lock file path for a world
117
+ get_lock_file() {
118
+ local world="$1"
119
+ echo "$LOCKS_DIR/${world}.lock"
120
+ }
121
+
122
+ # Parse lock file content
123
+ # Returns: server:timestamp:pid
124
+ parse_lock_file() {
125
+ local lock_file="$1"
126
+ if [[ -f "$lock_file" ]]; then
127
+ cat "$lock_file"
128
+ fi
129
+ }
130
+
131
+ # Check if lock is stale (older than threshold)
132
+ is_stale_lock() {
133
+ local lock_file="$1"
134
+ local lock_content
135
+ local lock_timestamp
136
+ local current_time
137
+ local age
138
+
139
+ if [[ ! -f "$lock_file" ]]; then
140
+ return 1 # No lock file, not stale
141
+ fi
142
+
143
+ lock_content=$(parse_lock_file "$lock_file")
144
+ lock_timestamp=$(echo "$lock_content" | cut -d: -f2)
145
+
146
+ if [[ -z "$lock_timestamp" ]]; then
147
+ return 0 # Invalid format, consider stale
148
+ fi
149
+
150
+ current_time=$(date +%s)
151
+ age=$((current_time - lock_timestamp))
152
+
153
+ if [[ $age -gt $STALE_THRESHOLD ]]; then
154
+ return 0 # Stale
155
+ fi
156
+
157
+ return 1 # Not stale
158
+ }
159
+
160
+ # Get all world directories
161
+ get_worlds() {
162
+ if [[ -d "$WORLDS_DIR" ]]; then
163
+ find "$WORLDS_DIR" -maxdepth 1 -mindepth 1 -type d ! -name '.locks' -exec basename {} \; 2>/dev/null | sort
164
+ fi
165
+ }
166
+
167
+ # =============================================================================
168
+ # Lock Functions
169
+ # =============================================================================
170
+
171
+ # Acquire lock on a world
172
+ # Usage: lock_world <world> <server>
173
+ lock_world() {
174
+ local world="$1"
175
+ local server="$2"
176
+ local lock_file
177
+ local lock_content
178
+ local current_holder
179
+ local timestamp
180
+ local pid
181
+
182
+ if [[ -z "$world" || -z "$server" ]]; then
183
+ error "Usage: lock <world> <server>"
184
+ return 1
185
+ fi
186
+
187
+ ensure_locks_dir
188
+ lock_file=$(get_lock_file "$world")
189
+ timestamp=$(date +%s)
190
+ pid=$$
191
+
192
+ # Use flock for atomic locking
193
+ (
194
+ flock -n 200 || {
195
+ # Lock is held, check who owns it
196
+ if [[ -f "$lock_file" ]]; then
197
+ lock_content=$(parse_lock_file "$lock_file")
198
+ current_holder=$(echo "$lock_content" | cut -d: -f1)
199
+
200
+ if [[ "$current_holder" == "$server" ]]; then
201
+ warn "World '$world' is already locked by this server ($server)"
202
+ exit 2
203
+ else
204
+ error "World '$world' is locked by '$current_holder'"
205
+ exit 1
206
+ fi
207
+ fi
208
+ error "Failed to acquire lock on '$world'"
209
+ exit 1
210
+ }
211
+
212
+ # Check for existing lock file
213
+ if [[ -f "$lock_file" ]]; then
214
+ lock_content=$(parse_lock_file "$lock_file")
215
+ current_holder=$(echo "$lock_content" | cut -d: -f1)
216
+
217
+ if [[ "$current_holder" == "$server" ]]; then
218
+ # Same server, update timestamp
219
+ echo "${server}:${timestamp}:${pid}" > "$lock_file"
220
+ info "Lock renewed: $world -> $server"
221
+ exit 0
222
+ fi
223
+
224
+ # Check for stale lock
225
+ if is_stale_lock "$lock_file"; then
226
+ warn "Removing stale lock on '$world' (was held by '$current_holder')"
227
+ rm -f "$lock_file"
228
+ else
229
+ error "World '$world' is locked by '$current_holder'"
230
+ exit 1
231
+ fi
232
+ fi
233
+
234
+ # Create lock file
235
+ echo "${server}:${timestamp}:${pid}" > "$lock_file"
236
+ info "Lock acquired: $world -> $server"
237
+ exit 0
238
+ ) 200>"$lock_file.flock"
239
+
240
+ local result=$?
241
+ rm -f "$lock_file.flock"
242
+ return $result
243
+ }
244
+
245
+ # Release lock on a world
246
+ # Usage: unlock_world <world> <server> [--force]
247
+ unlock_world() {
248
+ local world="$1"
249
+ local server="$2"
250
+ local force="${3:-}"
251
+ local lock_file
252
+ local lock_content
253
+ local current_holder
254
+
255
+ if [[ -z "$world" || -z "$server" ]]; then
256
+ error "Usage: unlock <world> <server>"
257
+ return 1
258
+ fi
259
+
260
+ lock_file=$(get_lock_file "$world")
261
+
262
+ if [[ ! -f "$lock_file" ]]; then
263
+ warn "World '$world' is not locked"
264
+ return 2
265
+ fi
266
+
267
+ lock_content=$(parse_lock_file "$lock_file")
268
+ current_holder=$(echo "$lock_content" | cut -d: -f1)
269
+
270
+ # Check ownership
271
+ if [[ "$current_holder" != "$server" ]]; then
272
+ if [[ "$force" == "--force" ]]; then
273
+ warn "Force unlocking '$world' (was held by '$current_holder')"
274
+ else
275
+ error "World '$world' is locked by '$current_holder', not '$server'"
276
+ error "Use --force to override (admin only)"
277
+ return 1
278
+ fi
279
+ fi
280
+
281
+ # Remove lock file
282
+ rm -f "$lock_file"
283
+ info "Lock released: $world (was held by $current_holder)"
284
+ return 0
285
+ }
286
+
287
+ # Check lock status of a world
288
+ # Usage: check_lock <world>
289
+ check_lock() {
290
+ local world="$1"
291
+ local lock_file
292
+ local lock_content
293
+ local holder
294
+ local timestamp
295
+ local lock_time
296
+ local age
297
+
298
+ if [[ -z "$world" ]]; then
299
+ error "Usage: check <world>"
300
+ return 1
301
+ fi
302
+
303
+ lock_file=$(get_lock_file "$world")
304
+
305
+ if [[ ! -f "$lock_file" ]]; then
306
+ echo "unlocked"
307
+ return 0
308
+ fi
309
+
310
+ lock_content=$(parse_lock_file "$lock_file")
311
+ holder=$(echo "$lock_content" | cut -d: -f1)
312
+ timestamp=$(echo "$lock_content" | cut -d: -f2)
313
+
314
+ if [[ -n "$timestamp" ]]; then
315
+ lock_time=$(date -d "@$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")
316
+ age=$(( $(date +%s) - timestamp ))
317
+
318
+ if is_stale_lock "$lock_file"; then
319
+ echo "stale:$holder:$lock_time (${age}s ago)"
320
+ return 2
321
+ else
322
+ echo "locked:$holder:$lock_time (${age}s ago)"
323
+ return 0
324
+ fi
325
+ else
326
+ echo "locked:$holder:unknown"
327
+ return 0
328
+ fi
329
+ }
330
+
331
+ # List all worlds and their lock status
332
+ # Usage: list_locks [--json]
333
+ list_locks() {
334
+ local json_output="${1:-}"
335
+ local worlds
336
+ local world
337
+ local lock_file
338
+ local lock_content
339
+ local holder
340
+ local timestamp
341
+ local lock_time
342
+ local status
343
+ local first=true
344
+
345
+ ensure_locks_dir
346
+
347
+ if [[ "$json_output" == "--json" ]]; then
348
+ echo "{"
349
+ echo ' "worlds": ['
350
+ else
351
+ printf "%-20s %-12s %-20s %s\n" "WORLD" "STATUS" "HOLDER" "LOCKED_AT"
352
+ printf "%-20s %-12s %-20s %s\n" "-----" "------" "------" "---------"
353
+ fi
354
+
355
+ worlds=$(get_worlds)
356
+
357
+ for world in $worlds; do
358
+ lock_file=$(get_lock_file "$world")
359
+ holder=""
360
+ timestamp=""
361
+ lock_time=""
362
+
363
+ if [[ -f "$lock_file" ]]; then
364
+ lock_content=$(parse_lock_file "$lock_file")
365
+ holder=$(echo "$lock_content" | cut -d: -f1)
366
+ timestamp=$(echo "$lock_content" | cut -d: -f2)
367
+
368
+ if [[ -n "$timestamp" ]]; then
369
+ lock_time=$(date -d "@$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")
370
+ fi
371
+
372
+ if is_stale_lock "$lock_file"; then
373
+ status="stale"
374
+ else
375
+ status="locked"
376
+ fi
377
+ else
378
+ status="available"
379
+ holder="-"
380
+ lock_time="-"
381
+ fi
382
+
383
+ if [[ "$json_output" == "--json" ]]; then
384
+ if [[ "$first" != "true" ]]; then
385
+ echo ","
386
+ fi
387
+ first=false
388
+ printf ' {"name": "%s", "status": "%s"' "$world" "$status"
389
+ if [[ "$status" != "available" ]]; then
390
+ printf ', "holder": "%s", "locked_at": "%s", "timestamp": %s' "$holder" "$lock_time" "${timestamp:-null}"
391
+ fi
392
+ printf "}"
393
+ else
394
+ printf "%-20s %-12s %-20s %s\n" "$world" "$status" "$holder" "$lock_time"
395
+ fi
396
+ done
397
+
398
+ if [[ "$json_output" == "--json" ]]; then
399
+ echo ""
400
+ echo " ],"
401
+ echo " \"locks_dir\": \"$LOCKS_DIR\","
402
+ echo " \"stale_threshold_seconds\": $STALE_THRESHOLD"
403
+ echo "}"
404
+ fi
405
+ }
406
+
407
+ # =============================================================================
408
+ # Main
409
+ # =============================================================================
410
+
411
+ main() {
412
+ local command="${1:-}"
413
+ shift || true
414
+
415
+ case "$command" in
416
+ lock)
417
+ lock_world "$@"
418
+ ;;
419
+ unlock)
420
+ unlock_world "$@"
421
+ ;;
422
+ check)
423
+ check_lock "$@"
424
+ ;;
425
+ list)
426
+ list_locks "$@"
427
+ ;;
428
+ -h|--help|help)
429
+ usage
430
+ exit 0
431
+ ;;
432
+ "")
433
+ error "No command specified"
434
+ usage
435
+ exit 1
436
+ ;;
437
+ *)
438
+ error "Unknown command: $command"
439
+ usage
440
+ exit 1
441
+ ;;
442
+ esac
443
+ }
444
+
445
+ # Run main if script is executed directly
446
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
447
+ main "$@"
448
+ fi