dst-server 0.1.0__py3-none-any.whl

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.
Files changed (60) hide show
  1. dst_server/__init__.py +89 -0
  2. dst_server/_fd_wrapper.py +24 -0
  3. dst_server/annotations/__init__.py +5 -0
  4. dst_server/annotations/cli.py +80 -0
  5. dst_server/annotations/generator.py +66 -0
  6. dst_server/annotations/values.py +82 -0
  7. dst_server/annotations/visitors.py +252 -0
  8. dst_server/arguments.py +47 -0
  9. dst_server/cluster.py +120 -0
  10. dst_server/console.py +82 -0
  11. dst_server/driver.py +87 -0
  12. dst_server/events/__init__.py +109 -0
  13. dst_server/events/base.py +107 -0
  14. dst_server/events/player.py +281 -0
  15. dst_server/events/record.py +90 -0
  16. dst_server/events/server.py +79 -0
  17. dst_server/events/world.py +120 -0
  18. dst_server/game/__init__.py +7 -0
  19. dst_server/game/client.py +112 -0
  20. dst_server/game/players.py +172 -0
  21. dst_server/game/rpc.py +56 -0
  22. dst_server/game/world.py +99 -0
  23. dst_server/game_events.py +108 -0
  24. dst_server/instrumentation.py +118 -0
  25. dst_server/klei/__init__.py +32 -0
  26. dst_server/klei/client.py +216 -0
  27. dst_server/klei/enums.py +59 -0
  28. dst_server/klei/lobby.py +112 -0
  29. dst_server/klei/schema.py +14 -0
  30. dst_server/klei/version.py +140 -0
  31. dst_server/lua/dst_server/actions.lua +57 -0
  32. dst_server/lua/dst_server/commands.lua +196 -0
  33. dst_server/lua/dst_server/player_events.lua +211 -0
  34. dst_server/lua/dst_server/player_queries.lua +82 -0
  35. dst_server/lua/dst_server/player_values.lua +170 -0
  36. dst_server/lua/dst_server/state.lua +16 -0
  37. dst_server/lua/dst_server/telemetry.lua +53 -0
  38. dst_server/lua/dst_server/values.lua +185 -0
  39. dst_server/lua/dst_server/world_events.lua +136 -0
  40. dst_server/lua/dst_server/world_queries.lua +151 -0
  41. dst_server/lua/dst_server.lua +94 -0
  42. dst_server/models/__init__.py +40 -0
  43. dst_server/models/player.py +129 -0
  44. dst_server/models/server.py +116 -0
  45. dst_server/models/value.py +17 -0
  46. dst_server/mods.py +134 -0
  47. dst_server/observers.py +114 -0
  48. dst_server/otel.py +206 -0
  49. dst_server/process.py +336 -0
  50. dst_server/protocol.py +135 -0
  51. dst_server/py.typed +0 -0
  52. dst_server/runner.py +240 -0
  53. dst_server/schema.py +38 -0
  54. dst_server/server_events.py +98 -0
  55. dst_server/validation.py +50 -0
  56. dst_server-0.1.0.dist-info/METADATA +149 -0
  57. dst_server-0.1.0.dist-info/RECORD +60 -0
  58. dst_server-0.1.0.dist-info/WHEEL +4 -0
  59. dst_server-0.1.0.dist-info/entry_points.txt +3 -0
  60. dst_server-0.1.0.dist-info/licenses/LICENSE +21 -0
dst_server/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from .arguments import ServerArgs
4
+ from .cluster import Shard, ShardConfig, discover_shards, ensure_fifo, prepare_cluster
5
+ from .events import (
6
+ DriverHealth,
7
+ EntityRef,
8
+ GameEvent,
9
+ ItemRef,
10
+ ObservedGameEvent,
11
+ ServerEvent,
12
+ ServerReadyEvent,
13
+ ServerSavedEvent,
14
+ ServerSessionEvent,
15
+ ServerShutdownEvent,
16
+ ServerStoppingEvent,
17
+ TelemetrySettings,
18
+ UnknownServerEvent,
19
+ )
20
+ from .game import GameClient
21
+ from .models import (
22
+ Age,
23
+ Container,
24
+ Entity,
25
+ EquipmentSlot,
26
+ Health,
27
+ Inventory,
28
+ InventorySlot,
29
+ Item,
30
+ Mod,
31
+ Player,
32
+ PlayerState,
33
+ Position,
34
+ Room,
35
+ Runtime,
36
+ ShardStatus,
37
+ Stat,
38
+ Temperature,
39
+ Vitals,
40
+ World,
41
+ )
42
+ from .mods import prepare_mods, update_server_mods, workshop_mod_ids
43
+ from .process import Server
44
+
45
+ __all__ = [
46
+ "Age",
47
+ "Container",
48
+ "DriverHealth",
49
+ "Entity",
50
+ "EntityRef",
51
+ "EquipmentSlot",
52
+ "GameClient",
53
+ "GameEvent",
54
+ "Health",
55
+ "Inventory",
56
+ "InventorySlot",
57
+ "Item",
58
+ "ItemRef",
59
+ "Mod",
60
+ "ObservedGameEvent",
61
+ "Player",
62
+ "PlayerState",
63
+ "Position",
64
+ "Room",
65
+ "Runtime",
66
+ "Server",
67
+ "ServerArgs",
68
+ "ServerEvent",
69
+ "ServerReadyEvent",
70
+ "ServerSavedEvent",
71
+ "ServerSessionEvent",
72
+ "ServerShutdownEvent",
73
+ "ServerStoppingEvent",
74
+ "Shard",
75
+ "ShardConfig",
76
+ "ShardStatus",
77
+ "Stat",
78
+ "TelemetrySettings",
79
+ "Temperature",
80
+ "UnknownServerEvent",
81
+ "Vitals",
82
+ "World",
83
+ "discover_shards",
84
+ "ensure_fifo",
85
+ "prepare_cluster",
86
+ "prepare_mods",
87
+ "update_server_mods",
88
+ "workshop_mod_ids",
89
+ ]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+
6
+ _PROTOCOL_FDS = (3, 4, 5)
7
+
8
+
9
+ def main() -> None:
10
+ sources = tuple(int(value) for value in sys.argv[1:4])
11
+ command = sys.argv[4:]
12
+ if len(sources) != len(_PROTOCOL_FDS) or not command:
13
+ msg = "usage: _fd_wrapper.py FD3 FD4 FD5 COMMAND [ARG ...]"
14
+ raise SystemExit(msg)
15
+ for source, target in zip(sources, _PROTOCOL_FDS, strict=True):
16
+ os.dup2(source, target, inheritable=True)
17
+ for source in sources:
18
+ if source not in _PROTOCOL_FDS:
19
+ os.close(source)
20
+ os.execv(command[0], command) # ruff:ignore[start-process-with-no-shell]
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .generator import generate_components, generate_modutil
4
+
5
+ __all__ = ["generate_components", "generate_modutil"]
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+
8
+ from .generator import generate_components, generate_modutil
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def create_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ description="Generate LSP-compatible Lua annotations."
16
+ )
17
+ parser.add_argument("input", type=Path, help="Components directory or modutil file")
18
+ parser.add_argument("-o", "--output", type=Path, help="Output Lua file")
19
+ parser.add_argument("-m", "--max-workers", type=int, help="Worker process count")
20
+ parser.add_argument(
21
+ "--mode",
22
+ choices=("auto", "components", "modutil"),
23
+ default="auto",
24
+ )
25
+ return parser
26
+
27
+
28
+ def resolve_mode(
29
+ parser: argparse.ArgumentParser,
30
+ input_path: Path,
31
+ requested: str,
32
+ ) -> str:
33
+ if requested != "auto":
34
+ return requested
35
+ if input_path.is_dir():
36
+ return "components"
37
+ if input_path.is_file() and "modutil" in input_path.name.lower():
38
+ return "modutil"
39
+ return parser.error("Cannot infer mode; pass --mode explicitly.")
40
+
41
+
42
+ def write_components(input_path: Path, output: Path | None, workers: int | None) -> int:
43
+ content = generate_components(input_path, workers)
44
+ if not content:
45
+ logger.error("No Lua component definitions generated from %s", input_path)
46
+ return 1
47
+
48
+ output_path = output or Path(f"{input_path.name}_def.lua")
49
+ output_path.write_text(content, encoding="utf-8")
50
+ logger.info("Output written to %s", output_path)
51
+ return 0
52
+
53
+
54
+ def write_modutil(input_path: Path, output: Path | None) -> int:
55
+ if not input_path.is_file():
56
+ raise FileNotFoundError(input_path)
57
+
58
+ content = generate_modutil(input_path)
59
+ if not content:
60
+ logger.error("No definitions found in %s", input_path)
61
+ return 1
62
+
63
+ output_path = output or Path(f"{input_path.stem}_def.lua")
64
+ output_path.write_text(content, encoding="utf-8")
65
+ logger.info("Output written to %s", output_path)
66
+ return 0
67
+
68
+
69
+ def main(argv: Sequence[str] | None = None) -> int:
70
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
71
+ parser = create_parser()
72
+ args = parser.parse_args(argv)
73
+ mode = resolve_mode(parser, args.input, args.mode)
74
+
75
+ try:
76
+ if mode == "components":
77
+ return write_components(args.input, args.output, args.max_workers)
78
+ return write_modutil(args.input, args.output)
79
+ except Exception as error:
80
+ parser.error(str(error))
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from concurrent.futures import ProcessPoolExecutor
5
+ from itertools import repeat
6
+ from pathlib import Path
7
+
8
+ from .visitors import DEFAULT_VAR, parse_component, parse_modutil
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def extract_class_name(content: str) -> str | None:
14
+ for source_line in reversed(content.splitlines()):
15
+ line = source_line.strip()
16
+ if line.startswith("return "):
17
+ return line.removeprefix("return ").split(maxsplit=1)[0].rstrip(",")
18
+ return None
19
+
20
+
21
+ def component_definition(path: Path, folder_name: str) -> tuple[str, str] | None:
22
+ try:
23
+ content = path.read_text(encoding="utf-8")
24
+ class_name = extract_class_name(content) or path.stem
25
+ fields, definitions = parse_component(
26
+ content,
27
+ path.stem,
28
+ class_name,
29
+ folder_name,
30
+ )
31
+ except Exception as error:
32
+ logger.warning("Could not parse %s: %s", path, error)
33
+ return None
34
+
35
+ lines = [f"---@class {class_name}", *fields]
36
+ lines.extend((
37
+ f"local {DEFAULT_VAR}={{}}",
38
+ f"{folder_name}.{path.stem}={DEFAULT_VAR}",
39
+ "",
40
+ *definitions,
41
+ "",
42
+ ))
43
+ return path.stem, "\n".join(lines)
44
+
45
+
46
+ def generate_components(input_dir: Path, max_workers: int | None = None) -> str:
47
+ if not input_dir.is_dir():
48
+ raise NotADirectoryError(input_dir)
49
+
50
+ files = sorted(input_dir.rglob("*.lua"))
51
+ if max_workers == 1:
52
+ results = [component_definition(path, input_dir.name) for path in files]
53
+ else:
54
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
55
+ results = list(
56
+ executor.map(component_definition, files, repeat(input_dir.name))
57
+ )
58
+
59
+ definitions = sorted(result for result in results if result is not None)
60
+ return "\n".join(content for _, content in definitions)
61
+
62
+
63
+ def generate_modutil(input_file: Path) -> str:
64
+ content = input_file.read_text(encoding="utf-8")
65
+ definitions = parse_modutil(content, input_file.stem)
66
+ return "\n".join(definitions) + ("\n" if definitions else "")
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from luaparser import ast
7
+
8
+
9
+ class LuaType(StrEnum):
10
+ STRING = "string"
11
+ NUMBER = "number"
12
+ BOOLEAN = "boolean"
13
+ TABLE = "table"
14
+ NIL = "nil"
15
+ FUNCTION = "function"
16
+ USERDATA = "userdata"
17
+ ANY = "any"
18
+
19
+
20
+ DEFAULT_VALUES = {
21
+ LuaType.TABLE: "{}",
22
+ LuaType.NUMBER: "0",
23
+ LuaType.FUNCTION: "function()end",
24
+ LuaType.USERDATA: "newproxy()",
25
+ LuaType.STRING: '""',
26
+ LuaType.NIL: "nil",
27
+ LuaType.ANY: "variable",
28
+ }
29
+
30
+
31
+ def infer_type(node: Any) -> LuaType:
32
+ match node:
33
+ case ast.String() | ast.Concat():
34
+ return LuaType.STRING
35
+ case (
36
+ ast.Number()
37
+ | ast.AddOp()
38
+ | ast.SubOp()
39
+ | ast.MultOp()
40
+ | ast.FloatDivOp()
41
+ | ast.FloorDivOp()
42
+ ):
43
+ return LuaType.NUMBER
44
+ case ast.TrueExpr() | ast.FalseExpr():
45
+ return LuaType.BOOLEAN
46
+ case ast.Table():
47
+ return LuaType.TABLE
48
+ case ast.Nil():
49
+ return LuaType.NIL
50
+ case ast.Function():
51
+ return LuaType.FUNCTION
52
+ case ast.Call(func=ast.Name(id="newproxy")):
53
+ return LuaType.USERDATA
54
+ case _:
55
+ return LuaType.ANY
56
+
57
+
58
+ def construct_value(
59
+ lua_type: LuaType | list[LuaType] | None,
60
+ source: str | None = None,
61
+ ) -> str:
62
+ if lua_type is None:
63
+ return "variable"
64
+
65
+ if isinstance(lua_type, list):
66
+ values = source.split(",") if source else []
67
+ return ",".join(
68
+ construct_value(
69
+ item_type,
70
+ values[index].strip() if index < len(values) else None,
71
+ )
72
+ for index, item_type in enumerate(lua_type)
73
+ )
74
+
75
+ if lua_type == LuaType.BOOLEAN:
76
+ value = source.strip() if source else None
77
+ return value if value in {"true", "false"} else "false"
78
+
79
+ if source and source.strip() == "inst":
80
+ return "inst"
81
+
82
+ return DEFAULT_VALUES.get(lua_type, "variable")
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from luaparser import ast
6
+
7
+ from .values import LuaType, construct_value, infer_type
8
+
9
+ type ReturnInfo = tuple[list[LuaType], str]
10
+
11
+ BANNED_NAMES = ("inst", "GetDebugString")
12
+ DEFAULT_VAR = "_l"
13
+
14
+
15
+ def argument_names(arguments: Any) -> list[str]:
16
+ names = []
17
+ for argument in arguments:
18
+ name = getattr(argument, "id", None)
19
+ if isinstance(name, str):
20
+ names.append(name)
21
+ elif type(argument).__name__ == "Varargs":
22
+ names.append("...")
23
+ else:
24
+ names.append("arg")
25
+ return names
26
+
27
+
28
+ def indexed_name(node: Any, owner: str) -> str | None:
29
+ if not isinstance(node, ast.Index):
30
+ return None
31
+ if not isinstance(node.value, ast.Name) or node.value.id != owner:
32
+ return None
33
+ return node.idx.id if isinstance(node.idx, ast.Name) else None
34
+
35
+
36
+ def method_name(node: Any, owner: str) -> str | None:
37
+ if not isinstance(node.source, ast.Name) or node.source.id != owner:
38
+ return None
39
+ return node.name.id if isinstance(node.name, ast.Name) else None
40
+
41
+
42
+ def anonymous_function(node: Any) -> Any | None:
43
+ values = getattr(node, "values", ())
44
+ if not values or not isinstance(values[0], ast.AnonymousFunction):
45
+ return None
46
+ return values[0]
47
+
48
+
49
+ def pretty_value(node: Any) -> str:
50
+ try:
51
+ return ast.to_pretty_str(node)
52
+ except Exception:
53
+ return "variable"
54
+
55
+
56
+ class ReturnVisitor(ast.ASTVisitor):
57
+ def __init__(self) -> None:
58
+ self.returns: list[ReturnInfo] = []
59
+
60
+ def visit_Return(self, node: Any) -> bool:
61
+ values = getattr(node, "values", ())
62
+ self.returns.append((
63
+ [infer_type(value) for value in values],
64
+ ", ".join(pretty_value(value) for value in values),
65
+ ))
66
+ return False
67
+
68
+
69
+ def return_info(body: Any) -> list[ReturnInfo]:
70
+ visitor = ReturnVisitor()
71
+ visitor.visit(body)
72
+ return visitor.returns
73
+
74
+
75
+ class BaseVisitor(ast.ASTVisitor):
76
+ def __init__(self, filename: str, local_var: str) -> None:
77
+ self.filename = filename
78
+ self.local_var = local_var
79
+ self.processed = set[str]()
80
+
81
+ def build_annotations(
82
+ self,
83
+ parameters: list[str],
84
+ returns: list[ReturnInfo],
85
+ node: Any,
86
+ source_prefix: str = "",
87
+ ) -> list[str]:
88
+ token = getattr(node, "first_token", None)
89
+ line = getattr(token, "line", "unknown")
90
+ annotations = [f"---@source {source_prefix}{self.filename}.lua:{line}"]
91
+
92
+ for parameter in parameters:
93
+ if parameter == "...":
94
+ annotations.append("---@param ... any")
95
+ elif parameter != "self":
96
+ annotations.append(f"---@param {parameter} any")
97
+
98
+ if returns and returns[-1][0]:
99
+ annotations.append(f"---@return {', '.join(returns[-1][0])}")
100
+
101
+ return annotations
102
+
103
+ def build_function(
104
+ self,
105
+ name: str,
106
+ parameters: list[str],
107
+ returns: list[ReturnInfo],
108
+ node: Any,
109
+ source_prefix: str = "",
110
+ ) -> str:
111
+ lines = self.build_annotations(parameters, returns, node, source_prefix)
112
+ function = f"function {name}({', '.join(parameters)})"
113
+
114
+ if returns:
115
+ return_types, return_source = returns[-1]
116
+ value = construct_value(return_types, return_source)
117
+ lines.append(f"{function} return {value} end")
118
+ else:
119
+ lines.append(f"{function} end")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ class ComponentVisitor(BaseVisitor):
125
+ def __init__(
126
+ self,
127
+ filename: str,
128
+ class_name: str,
129
+ folder_name: str,
130
+ local_var: str = DEFAULT_VAR,
131
+ ) -> None:
132
+ super().__init__(filename, local_var)
133
+ self.class_name = class_name
134
+ self.folder_name = folder_name
135
+ self.methods: list[str] = []
136
+ self.fields: dict[str, tuple[LuaType | None, str | None]] = {}
137
+
138
+ def visit_Method(self, node: Any) -> bool:
139
+ name = method_name(node, self.class_name)
140
+ if name is None or name in BANNED_NAMES or name in self.processed:
141
+ return False
142
+
143
+ self.processed.add(name)
144
+ self.methods.append(
145
+ self.build_function(
146
+ f"{self.local_var}:{name}",
147
+ argument_names(node.args),
148
+ return_info(node.body),
149
+ node,
150
+ source_prefix=f"{self.folder_name}/",
151
+ )
152
+ )
153
+ return False
154
+
155
+ def visit_Assign(self, node: Any) -> None:
156
+ function = anonymous_function(node)
157
+ for target in node.targets:
158
+ if name := indexed_name(target, "self"):
159
+ self.record_field(name, node)
160
+ continue
161
+
162
+ if function is not None and (name := indexed_name(target, self.class_name)):
163
+ self.record_function(name, function)
164
+
165
+ def record_field(self, name: str, node: Any) -> None:
166
+ if name in BANNED_NAMES or name in self.processed:
167
+ return
168
+
169
+ value_type = None
170
+ value_source = None
171
+ if node.values:
172
+ value = node.values[0]
173
+ value_type = infer_type(value)
174
+ value_source = (
175
+ "function() end"
176
+ if isinstance(value, (ast.Function, ast.Method, ast.AnonymousFunction))
177
+ else pretty_value(value)
178
+ )
179
+
180
+ current = self.fields.get(name)
181
+ if current is None or current[0] in {None, LuaType.ANY}:
182
+ self.fields[name] = value_type, value_source
183
+
184
+ def record_function(self, name: str, node: Any) -> None:
185
+ if name in BANNED_NAMES or name in self.processed:
186
+ return
187
+
188
+ self.processed.add(name)
189
+ self.methods.append(
190
+ self.build_function(
191
+ name,
192
+ argument_names(node.args),
193
+ return_info(node.body),
194
+ node,
195
+ source_prefix=f"{self.folder_name}/",
196
+ )
197
+ )
198
+
199
+ def definitions(self) -> tuple[list[str], list[str]]:
200
+ fields = sorted(self.fields.items())
201
+ annotations = [
202
+ f"---@field {name} {value_type or 'any'}"
203
+ for name, (value_type, _) in fields
204
+ ]
205
+ values = [
206
+ f"{self.local_var}.{name}={construct_value(value_type, source)}"
207
+ for name, (value_type, source) in fields
208
+ ]
209
+ return annotations, [*self.methods, *values]
210
+
211
+
212
+ class ModutilVisitor(BaseVisitor):
213
+ def __init__(self, filename: str) -> None:
214
+ super().__init__(filename, "_m")
215
+ self.functions: list[str] = []
216
+
217
+ def visit_Assign(self, node: Any) -> None:
218
+ function = anonymous_function(node)
219
+ if function is None:
220
+ return
221
+
222
+ for target in node.targets:
223
+ name = indexed_name(target, "env")
224
+ if name is None or name in self.processed:
225
+ continue
226
+
227
+ self.processed.add(name)
228
+ self.functions.append(
229
+ self.build_function(
230
+ name,
231
+ argument_names(function.args),
232
+ return_info(function.body),
233
+ function,
234
+ )
235
+ )
236
+
237
+
238
+ def parse_component(
239
+ content: str,
240
+ filename: str,
241
+ class_name: str,
242
+ folder_name: str,
243
+ ) -> tuple[list[str], list[str]]:
244
+ visitor = ComponentVisitor(filename, class_name, folder_name)
245
+ visitor.visit(ast.parse(content))
246
+ return visitor.definitions()
247
+
248
+
249
+ def parse_modutil(content: str, filename: str) -> list[str]:
250
+ visitor = ModutilVisitor(filename)
251
+ visitor.visit(ast.parse(content))
252
+ return visitor.functions
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ from .events import TelemetrySettings
7
+
8
+ LUA_DIRECTORY = Path(__file__).with_name("lua")
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ServerArgs:
13
+ shard: str
14
+ executable: Path = Path(
15
+ "/install/bin64/dontstarve_dedicated_server_nullrenderer_x64"
16
+ )
17
+ persistent_storage_root: Path = Path("/")
18
+ conf_dir: str = "/"
19
+ cluster: str = "cluster"
20
+ ugc_directory: Path | None = Path("/cluster/mods/ugc")
21
+ extra: tuple[str, ...] = ("-skip_update_server_mods",)
22
+ lua_directory: Path = LUA_DIRECTORY
23
+ telemetry: TelemetrySettings = field(default_factory=TelemetrySettings)
24
+ monitor_parent_process: bool = True
25
+
26
+ def command(self, *, monitor_parent_process: int | None = None) -> tuple[str, ...]:
27
+ command = [
28
+ str(self.executable),
29
+ "-persistent_storage_root",
30
+ str(self.persistent_storage_root),
31
+ "-conf_dir",
32
+ self.conf_dir,
33
+ "-cluster",
34
+ self.cluster,
35
+ "-shard",
36
+ self.shard,
37
+ ]
38
+ if self.ugc_directory is not None:
39
+ command.extend(("-ugc_directory", str(self.ugc_directory)))
40
+ if monitor_parent_process is not None:
41
+ command.extend(("-monitor_parent_process", str(monitor_parent_process)))
42
+ command.extend(self.extra)
43
+ command.append("-cloudserver")
44
+ return tuple(command)
45
+
46
+
47
+ __all__ = ["ServerArgs"]