forked from rojo-rbx/rojo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
serve_session.rs
291 lines (245 loc) · 9.55 KB
/
serve_session.rs
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
use std::{
borrow::Cow,
collections::HashSet,
io,
net::IpAddr,
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
time::Instant,
};
use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs;
use thiserror::Error;
use crate::{
change_processor::ChangeProcessor,
message_queue::MessageQueue,
project::{Project, ProjectError},
session_id::SessionId,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
PatchSet, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
/// Contains all of the state for a Rojo serve session. A serve session is used
/// when we need to build a Rojo tree and possibly rebuild it when input files
/// change.
///
/// Nothing here is specific to any Rojo interface. Though the primary way to
/// interact with a serve session is Rojo's HTTP right now, there's no reason
/// why Rojo couldn't expose an IPC or channels-based API for embedding in the
/// future. `ServeSession` would be roughly the right interface to expose for
/// those cases.
pub struct ServeSession {
/// The object responsible for listening to changes from the in-memory
/// filesystem, applying them, updating the Roblox instance tree, and
/// routing messages through the session's message queue to any connected
/// clients.
///
/// SHOULD BE DROPPED FIRST! ServeSession and ChangeProcessor communicate
/// with eachother via channels. If ServeSession hangs up those channels
/// before dropping the ChangeProcessor, its thread will panic with a
/// RecvError, causing the main thread to panic on drop.
///
/// Allowed to be unused because it has side effects when dropped.
#[allow(unused)]
change_processor: ChangeProcessor,
/// When the serve session was started. Used only for user-facing
/// diagnostics.
start_time: Instant,
/// The root project for the serve session.
///
/// This will be defined if a folder with a `default.project.json` file was
/// used for starting the serve session, or if the user specified a full
/// path to a `.project.json` file.
root_project: Project,
/// A randomly generated ID for this serve session. It's used to ensure that
/// a client doesn't begin connecting to a different server part way through
/// an operation that needs to be atomic.
session_id: SessionId,
/// The tree of Roblox instances associated with this session that will be
/// updated in real-time. This is derived from the session's VFS and will
/// eventually be mutable to connected clients.
tree: Arc<Mutex<RojoTree>>,
/// An in-memory filesystem containing all of the files relevant for this
/// live session.
///
/// The main use for accessing it from the session is for debugging issues
/// with Rojo's live-sync protocol.
vfs: Arc<Vfs>,
/// A queue of changes that have been applied to `tree` that affect clients.
///
/// Clients to the serve session will subscribe to this queue either
/// directly or through the HTTP API to be notified of mutations that need
/// to be applied.
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
/// A channel to send mutation requests on. These will be handled by the
/// ChangeProcessor and trigger changes in the tree.
tree_mutation_sender: Sender<PatchSet>,
}
impl ServeSession {
/// Start a new serve session from the given in-memory filesystem and start
/// path.
///
/// The project file is expected to be loaded out-of-band since it's
/// currently loaded from the filesystem directly instead of through the
/// in-memory filesystem layer.
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> {
let start_path = start_path.as_ref();
let start_time = Instant::now();
log::trace!("Starting new ServeSession at path {}", start_path.display());
let project_path = if Project::is_project_file(start_path) {
Cow::Borrowed(start_path)
} else {
Cow::Owned(start_path.join("default.project.json"))
};
log::debug!("Loading project file from {}", project_path.display());
let mut root_project = match vfs.read(&project_path).with_not_found()? {
Some(contents) => Project::load_from_slice(&contents, &project_path)?,
None => {
return Err(ServeSessionError::NoProjectFound {
path: project_path.to_path_buf(),
});
}
};
if root_project.name.is_none() {
if let Some(file_name) = project_path.file_name().and_then(|s| s.to_str()) {
if file_name == "default.project.json" {
let folder_name = project_path
.parent()
.and_then(Path::file_name)
.and_then(|s| s.to_str());
if let Some(folder_name) = folder_name {
root_project.name = Some(folder_name.to_string());
} else {
return Err(ServeSessionError::FolderNameInvalid {
path: project_path.to_path_buf(),
});
}
} else if let Some(trimmed) = file_name.strip_suffix(".project.json") {
root_project.name = Some(trimmed.to_string());
} else {
return Err(ServeSessionError::ProjectNameInvalid {
path: project_path.to_path_buf(),
});
}
} else {
return Err(ServeSessionError::ProjectNameInvalid {
path: project_path.to_path_buf(),
});
}
}
// Rebind it to make it no longer mutable
let root_project = root_project;
let mut tree = RojoTree::new(InstanceSnapshot::new());
let root_id = tree.get_root_id();
let instance_context =
InstanceContext::with_emit_legacy_scripts(root_project.emit_legacy_scripts);
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, start_path)?;
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot, &tree, root_id);
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
let session_id = SessionId::new();
let message_queue = MessageQueue::new();
let tree = Arc::new(Mutex::new(tree));
let message_queue = Arc::new(message_queue);
let vfs = Arc::new(vfs);
let (tree_mutation_sender, tree_mutation_receiver) = crossbeam_channel::unbounded();
log::trace!("Starting ChangeProcessor");
let change_processor = ChangeProcessor::start(
Arc::clone(&tree),
Arc::clone(&vfs),
Arc::clone(&message_queue),
tree_mutation_receiver,
);
Ok(Self {
change_processor,
start_time,
session_id,
root_project,
tree,
message_queue,
tree_mutation_sender,
vfs,
})
}
pub fn tree_handle(&self) -> Arc<Mutex<RojoTree>> {
Arc::clone(&self.tree)
}
pub fn tree(&self) -> MutexGuard<'_, RojoTree> {
self.tree.lock().unwrap()
}
pub fn tree_mutation_sender(&self) -> Sender<PatchSet> {
self.tree_mutation_sender.clone()
}
#[allow(unused)]
pub fn vfs(&self) -> &Vfs {
&self.vfs
}
pub fn message_queue(&self) -> &MessageQueue<AppliedPatchSet> {
&self.message_queue
}
pub fn session_id(&self) -> SessionId {
self.session_id
}
pub fn project_name(&self) -> &str {
self.root_project
.name
.as_ref()
.expect("all top-level projects must have their name set")
}
pub fn project_port(&self) -> Option<u16> {
self.root_project.serve_port
}
pub fn place_id(&self) -> Option<u64> {
self.root_project.place_id
}
pub fn game_id(&self) -> Option<u64> {
self.root_project.game_id
}
pub fn start_time(&self) -> Instant {
self.start_time
}
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
self.root_project.serve_place_ids.as_ref()
}
pub fn serve_address(&self) -> Option<IpAddr> {
self.root_project.serve_address
}
pub fn root_dir(&self) -> &Path {
self.root_project.folder_location()
}
}
#[derive(Debug, Error)]
pub enum ServeSessionError {
#[error(
"Rojo requires a project file, but no project file was found in path {}\n\
See https://rojo.space/docs/ for guides and documentation.",
.path.display()
)]
NoProjectFound { path: PathBuf },
#[error("The folder for the provided project cannot be used as a project name: {}\n\
Consider setting the `name` field on this project.", .path.display())]
FolderNameInvalid { path: PathBuf },
#[error("The file name of the provided project cannot be used as a project name: {}.\n\
Consider setting the `name` field on this project.", .path.display())]
ProjectNameInvalid { path: PathBuf },
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
#[error(transparent)]
Project {
#[from]
source: ProjectError,
},
#[error(transparent)]
Other {
#[from]
source: anyhow::Error,
},
}