1 #![allow(clippy::let_and_return)]
5 use slotmap::dense as sm;
7 // ---------- newtypes and type aliases ----------
9 visible_slotmap_key!{ ClientId(b'C') }
11 const MAX_CLIENT_INACTIVITY: Duration = Duration::from_secs(200);
13 const GAME_SAVE_LAG: Duration = Duration::from_millis(500);
15 const MAX_LOG_AGE: Duration = Duration::from_secs(10 * 86400);
17 #[derive(Hash,Ord,PartialOrd,Eq,PartialEq,Serialize)]
19 pub struct RawTokenVal(str);
21 // ---------- public data structure ----------
23 #[derive(Clone,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
24 #[derive(Serialize,Deserialize)]
25 pub struct InstanceName {
26 pub account: AccountName,
30 #[derive(Debug,Clone)]
31 pub struct InstanceRef(Arc<InstanceOuter>);
33 #[derive(Debug,Clone)]
34 pub struct InstanceWeakRef(std::sync::Weak<InstanceOuter>);
37 pub struct InstanceOuter {
38 c: Mutex<InstanceContainer>,
39 b: Mutex<InstanceBundles>,
42 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
43 #[derive(Deref,DerefMut)]
45 pub struct LinksTable(pub EnumMap<LinkKind, Option<String>>);
48 pub name: Arc<InstanceName>,
51 pub pcaliases: PieceAliases,
52 pub ioccults: IOccults,
53 pub clients: DenseSlotMap<ClientId, Client>,
54 pub iplayers: SecondarySlotMap<PlayerId, PlayerRecord>,
55 pub tokens_players: TokenRegistry<PlayerId>,
56 pub tokens_clients: TokenRegistry<ClientId>,
57 pub acl: LoadedAcl<TablePermission>,
58 pub links: Arc<LinksTable>,
59 pub bundle_list: MgmtBundleList, // copy for easy access
60 pub bundle_specs: bundles::SpecsInBundles,
61 pub bundle_hashes: bundles::HashCache,
62 pub asset_url_key: AssetUrlKey,
63 pub local_libs: shapelib::Registry,
64 pub ifastsplits: IFastSplits,
67 pub struct PlayerRecord {
70 pub account: Arc<AccountName>,
73 #[derive(Debug,Clone,Serialize,Deserialize)]
74 pub struct IPlayer { // usual variable: ipl
75 pub acctid: AccountId,
76 pub tokens_revealed: HashMap<TokenRevelationKey, TokenRevelationValue>,
80 /// Strange ownership and serialisation rules, like `OccultIlkOwningId`
81 #[derive(Debug,Serialize,Deserialize)]
84 #[deref] pub p: IPieceTraitObj,
85 pub occilk: Option<IOccultIlk>,
86 #[serde(default)] pub special: PieceSpecialProperties,
89 #[derive(Debug,Serialize,Deserialize)]
91 pub struct IPieces(ActualIPieces);
92 pub type ActualIPieces = SecondarySlotMap<PieceId, IPiece>;
94 /// Proof token that it is OK to modify the array
96 /// This exists to prevent bugs where we forget to save aux
97 #[derive(Copy,Clone,Debug)]
98 pub struct ModifyingPieces(());
100 #[derive(Debug,Serialize,Deserialize,Default)]
101 pub struct IOccults {
102 pub ilks: OccultIlks,
105 #[derive(Debug,Serialize,Deserialize,Default)]
106 #[derive(Deref)] // No DerefMut to make sure we send updates, save, etc.
107 #[serde(transparent)]
108 pub struct GPieces(pub(in crate::global) ActualGPieces);
109 type ActualGPieces = DenseSlotMap<PieceId, GPiece>;
113 pub player: PlayerId,
114 pub lastseen: Instant,
117 pub type BundlesGuard<'b> = MutexGuard<'b, InstanceBundles>;
119 // KINDS OF PERSISTENT STATE
121 // TokenTable TokenTable GameState Instance GameState
122 // <ClientId> <PlayerId> .players .pieces .pieces
124 // Saved No a-* g-* a-* g-*
125 // Spec TOML Absent table, ish table game game
128 // UPDATE RELIABILITY/PERSISTENCE RULES
130 // From the caller's point of view
132 // We offer atomic creation/modification/destruction of:
134 // * Games (roughtly, a map from InstanceName to GameState;
135 // includes any player)
137 // * Player access tokens, for an existing (game, player)
139 // * Clients (for an existing (game, player)
141 // We also offer atomic destruction of:
147 // All of the above, except clients, are persistent, in the sense
148 // that a server restart will preserve them. See above.
150 // The general code gets mutable access to the GameState. We offer
151 // post-hoc saving of a modified game. This should not be used for
152 // player changes. For other changes, if the save fails, a server
153 // restart may be a rewind. This relaxation of the rules is
154 // necessary avoid complicated three-phase commit on GameState update
155 // for routine game events.
159 // Games are created in this order:
161 // save/a-<nameforfs> MessagePack of InstanceSaveAuxiliary
162 // if, on reload, this does not exist, the game is considered
163 // not to exist, so at this stage the game conclusively does
166 // save/g-<nameforfs> MessagePack of GameState
169 // can always be done right after g-<nameforfs>
170 // since games update is infallible (barring unexpected panic)
172 // For the access elements such as player tokens and client
174 // 1. check constraints against existing state
176 // 3. modify in memory (infallibly)
178 // A consequence is that game creation or deletion means running
179 // much of this code (including game save/load) with a write lock
182 // The Instance object is inside Arc<Mutex>. Because of Arc the Mutex
183 // may persist beyond the lifetime of the actual game. So within the
184 // Mutex we have a field `live' that tells us if the thing is dead.
185 // This allows a lockholder to infallibly declare the thing dead,
186 // atomically from the pov of anyone else who has got a reference
187 // to it. We prevent the caller from ever seeing an Instance whosae
188 // `live` is `false`.
190 pub struct InstanceGuard<'g> {
191 pub c: MutexGuard<'g, InstanceContainer>,
192 pub gref: InstanceRef,
195 #[derive(Debug,Default)]
196 pub struct TokenRegistry<Id: AccessId> {
197 tr: HashSet<RawToken>,
201 #[derive(Clone,Debug)]
202 pub struct InstanceAccessDetails<Id> {
203 pub gref: InstanceRef,
205 pub acctid: AccountId,
208 // ========== internal data structures ==========
211 pub static ref GLOBAL: Global = default();
216 // lock hierarchy: all of these are in order of acquisition
217 // (in order of lock acquisition (L first), so also in order of criticality
218 // (perf impact); outermost first, innermost last)
220 // <- accounts::accounts ->
221 games_table: RwLock<GamesTable>,
224 // <- InstanceContainer ->
225 // inner locks which the game needs:
226 dirty: Mutex<VecDeque<InstanceRef>>,
228 // fast global lookups
229 players: RwLock<TokenTable<PlayerId>>,
230 clients: RwLock<TokenTable<ClientId>>,
233 pub type GamesGuard = RwLockWriteGuard<'static, GamesTable>;
234 pub type GamesTable = HashMap<Arc<InstanceName>, InstanceRef>;
237 pub struct InstanceContainer {
244 #[derive(Debug,Default,Serialize,Deserialize)]
245 struct InstanceSaveAuxiliary<RawTokenStr, PiecesLoadedRef, OccultIlksRef,
246 PieceAliasesRef, IFastSplitsRef> {
247 ipieces: PiecesLoadedRef,
248 ioccults: OccultIlksRef,
249 pcaliases: PieceAliasesRef,
250 tokens_players: Vec<(RawTokenStr, PlayerId)>,
251 aplayers: SecondarySlotMap<PlayerId, IPlayer>,
252 acl: Acl<TablePermission>,
253 pub links: Arc<LinksTable>,
254 asset_url_key: AssetUrlKey,
255 #[serde(default)] pub bundle_hashes: bundles::HashCache,
256 #[serde(default)] ifastsplits: IFastSplitsRef,
259 pub struct PrivateCaller(());
260 // outsiders cannot construct this
261 // workaround for inability to have private trait methods
262 const PRIVATE_Y: PrivateCaller = PrivateCaller(());
264 // ========== implementations ==========
266 impl<'s> From<&'s str> for &'s RawTokenVal {
267 // str is [u8] with a funny hat on, so &str is pointer + byte count.
268 // nomicon says &SomeStruct([T]) is pointer plus number of elements.
269 // So &str and &SomeStruct(str) have the same layout
270 fn from(s: &str) -> &RawTokenVal { unsafe { mem::transmute(s) } }
273 impl Borrow<RawTokenVal> for RawToken {
274 fn borrow(&self) -> &RawTokenVal { (&*self.0).into() }
277 impl Debug for RawTokenVal {
278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279 crate::spec::imp::raw_token_debug_as_str(&self.0, f)
283 impl Debug for Instance {
284 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285 write!(f, "Instance {{ name: {:?}, .. }}", &self.name)
289 impl ModifyingPieces {
290 pub fn allow_without_necessarily_saving() -> ModifyingPieces {
295 // ---------- Main API for instance lifecycle ----------
298 #[throws(GameBeingDestroyed)]
299 pub fn lock(&self) -> InstanceGuard<'_> {
300 let c = self.0.c.lock();
301 if !c.live { throw!(GameBeingDestroyed) }
302 InstanceGuard { c, gref: self.clone() }
305 pub fn lock_even_destroying(&self) -> MutexGuard<InstanceContainer> {
309 pub fn downgrade_to_weak(&self) -> InstanceWeakRef {
310 InstanceWeakRef(Arc::downgrade(&self.0))
313 pub fn lock_bundles(&self) -> MutexGuard<'_, InstanceBundles> {
318 impl InstanceWeakRef {
319 pub fn upgrade(&self) -> Option<InstanceRef> {
320 Some(InstanceRef(self.0.upgrade()?))
325 impl<A> Unauthorised<InstanceRef, A> {
326 #[throws(GameBeingDestroyed)]
327 fn lock<'r>(&'r self) -> Unauthorised<InstanceGuard<'r>, A> {
328 let must_not_escape = self.by_ref(Authorisation::promise_any());
329 Unauthorised::of(must_not_escape.lock()?)
332 fn lock_even_destroying<'r>(&'r self) -> Unauthorised<InstanceGuard<'r>, A> {
333 let must_not_escape = self.by_ref(Authorisation::promise_any());
334 Unauthorised::of(InstanceGuard {
335 c: must_not_escape.lock_even_destroying(),
336 gref: must_not_escape.clone(),
340 fn lock_bundles<'r>(&'r self) -> Unauthorised<BundlesGuard<'_>, A> {
341 let must_not_escape = self.by_ref(Authorisation::promise_any());
342 Unauthorised::of(must_not_escape.lock_bundles())
347 /// Returns `None` if a game with this name already exists
348 #[allow(clippy::new_ret_no_self)]
350 pub fn new(name: InstanceName, gs: GameState,
351 games: &mut GamesGuard,
352 acl: LoadedAcl<TablePermission>, _: Authorisation<InstanceName>)
354 let name = Arc::new(name);
359 ipieces: IPieces(default()),
360 pcaliases: default(),
364 tokens_players: default(),
365 tokens_clients: default(),
367 bundle_list: default(),
368 bundle_specs: default(),
369 bundle_hashes: default(),
370 asset_url_key: AssetUrlKey::new_random()?,
371 local_libs: default(),
372 ifastsplits: default(),
375 let c = InstanceContainer {
382 let c = Mutex::new(c);
383 let b = Mutex::new(InstanceBundles::new());
384 let gref = InstanceRef(Arc::new(InstanceOuter { c, b }));
385 let mut ig = gref.lock()?;
387 let entry = games.entry(name);
389 use hash_map::Entry::*;
390 let entry = match entry {
392 Occupied(_) => throw!(ME::AlreadyExists),
399 entry.insert(gref.clone());
400 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
406 pub fn lookup_by_name_locked_unauth
407 (games_table: &GamesTable, name: &InstanceName)
408 -> Unauthorised<InstanceRef, InstanceName>
413 .ok_or(ME::GameNotFound)?
419 pub fn lookup_by_name_locked(games: &GamesTable,
421 auth: Authorisation<InstanceName>)
423 Self::lookup_by_name_locked_unauth(games, name)?.by(auth)
427 pub fn lookup_by_name_unauth(name: &InstanceName)
428 -> Unauthorised<InstanceRef, InstanceName>
430 let games = GLOBAL.games_table.read();
431 Self::lookup_by_name_locked_unauth(&games, name)?
435 pub fn lookup_by_name(name: &InstanceName, auth: Authorisation<InstanceName>)
437 Self::lookup_by_name_unauth(name)?.by(auth)
440 #[throws(InternalError)]
441 pub fn destroy_game(games: &mut GamesGuard,
442 mut g: MutexGuard<InstanceContainer>,
443 _: Authorisation<InstanceName>) {
444 let a_savefile = savefilename(&g.g.name, "a-", "");
445 let b_dir = bundles::b_dir(&g.g.name);
447 let g_file = savefilename(&g.g.name, "g-", "");
448 fs::remove_file(&g_file).context("remove").context(g_file)?;
452 games.remove(&g.g.name);
453 InstanceGuard::forget_all_tokens(&mut g.g.tokens_clients);
454 InstanceGuard::forget_all_tokens(&mut g.g.tokens_players);
456 InstanceBundles::truncate_all_besteffort(&g.g.name);
458 fn best_effort<F>(rm: F, path: &str, desc: &str)
459 where F: FnOnce(&str) -> Result<(), io::Error>
463 |e| warn!("failed to delete stale {} {:?}: {:?}",
465 // apart from that, ignore the error
466 // load_games will clean it up later.
469 best_effort(|f| fs::remove_file(f), &a_savefile, "auth file");
470 best_effort(|f| fs::remove_dir_all(f), &b_dir, "bundles dir");
472 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
475 pub fn list_names(account: Option<&AccountName>,
476 _: Authorisation<AccountName>)
477 -> Vec<Arc<InstanceName>> {
478 let games = GLOBAL.games_table.read();
479 let out: Vec<Arc<InstanceName>> =
481 .filter(|k| account == None || account == Some(&k.account))
487 #[throws(InternalError)]
488 pub fn player_info_pane(&self) -> Html {
489 #[derive(Serialize,Debug)]
490 struct RenderContext<'r> {
491 players: Vec<RenderPlayer<'r>>,
493 #[derive(Serialize,Debug)]
494 struct RenderPlayer<'r> {
497 account: &'r AccountName,
499 let players = self.gs.players.iter().filter_map(|(player, gpl)| {
500 let ipl = self.iplayers.get(player)?;
501 let (idx, _) = player.data().get_idx_version();
505 account: &ipl.account,
507 }).collect::<Vec<_>>();
508 let render = RenderContext { players };
509 let html = Html::from_html_string(
510 nwtemplates::render("player-info-pane.tera", &render)
511 .context("render player info template")?
516 pub fn dummy() -> Instance {
518 name: Arc::new("server::".parse().unwrap()),
519 gs: GameState::dummy(),
520 ipieces: IPieces(default()),
521 pcaliases: default(),
524 tokens_players: default(),
525 tokens_clients: default(),
528 bundle_list: default(),
529 bundle_specs: default(),
530 bundle_hashes: default(),
531 asset_url_key: AssetUrlKey::Dummy,
532 local_libs: default(),
534 ifastsplits: default(),
540 pub fn games_lock() -> RwLockWriteGuard<'static, GamesTable> {
541 GLOBAL.games_table.write()
544 // ---------- Simple trait implementations ----------
546 deref_to_field_mut!{InstanceGuard<'_>, Instance, c.g}
548 impl FromStr for AccountScope {
549 type Err = InvalidScopedName;
550 #[throws(InvalidScopedName)]
551 fn from_str(s: &str) -> Self {
552 let scope = AccountScope::parse_name(s, &mut [])?;
557 impl FromStr for InstanceName {
558 type Err = InvalidScopedName;
559 #[throws(InvalidScopedName)]
560 fn from_str(s: &str) -> Self {
561 let mut names: [_;2] = default();
562 let scope = AccountScope::parse_name(s, &mut names)?;
563 let [subaccount, game] = names;
565 account: AccountName { scope, subaccount },
571 impl Display for InstanceName {
572 #[throws(fmt::Error)]
573 fn fmt(&self, f: &mut fmt::Formatter) {
574 self.account.scope.display_name(
575 &[ self.account.subaccount.as_str(), self.game.as_str() ],
580 hformat_as_display!{InstanceName}
582 impl DebugIdentify for InstanceContainer {
583 #[throws(fmt::Error)]
584 fn debug_identify(&self, f: &mut fmt::Formatter) {
585 write!(f, "InstanceContainer({})", &self.g.name)?;
587 #[throws(fmt::Error)]
588 fn debug_identify_type(f: &mut fmt::Formatter) {
589 write!(f, "InstanceContainer")?;
593 impl DebugIdentify for InstanceRef {
594 #[throws(fmt::Error)]
595 fn debug_identify_type(f: &mut fmt::Formatter) {
596 write!(f, "InstanceRef")?;
600 fn link_a_href(k: &HtmlStr, v: &str) -> Html {
601 hformat!("<a href={}>{}</a>", v, k)
604 impl (LinkKind, &str) {
605 fn to_html(self) -> Html {
607 link_a_href(&k.to_html(), v)
611 impl From<&LinksTable> for Html {
612 fn from(links: &LinksTable) -> Html {
613 let mut s = links.iter()
614 .filter_map(|(k,v)| {
616 Some((k, v.as_str()).to_html())
619 link_a_href(Html::lit("Shapelib").into(), "/_/shapelib.html")
621 .collect::<Vec<Html>>()
623 .hjoin(&Html::lit(" "));
624 if s.len() != 0 { s = hformat!("links: {}", s) }
629 // ---------- Player and token functionality ----------
631 impl<Id> InstanceAccessDetails<Id>
632 where Id: AccessId, Fatal: From<Id::Error>
635 pub fn from_token(token: &RawTokenVal) -> InstanceAccessDetails<Id> {
636 let g = Id::global_tokens(PRIVATE_Y).read();
637 let i = g.get(token).ok_or(Id::ERROR)?;
642 impl<'ig> InstanceGuard<'ig> {
643 /// Core of piece deletion
645 /// Caller is completely responsible for the necessary log entries.
647 /// Idempotent (so does not detect if the piece didn't exist,
648 /// other than by passing `None`s to the callback.
650 pub fn delete_piece<H,T>(&mut self, modperm: ModifyingPieces,
651 to_permute: &mut ToRecalculate,
652 piece: PieceId, hook: H)
653 -> (T, PieceUpdateOp<(),()>, UnpreparedUpdates)
654 where H: FnOnce(&IOccults, &GOccults,
655 Option<&IPiece>, Option<&GPiece>) -> T
657 let ig = &mut **self;
659 let gpc = gs.pieces.as_mut(modperm).get_mut(piece);
660 let mut xupdates = vec![];
661 if let Some(gpc) = gpc {
662 gpc.occult.passive_delete_hook(&mut gs.occults, piece);
663 if gpc.occult.is_active() {
667 &mut gs.gen.unique_gen(),
678 let ioccults = &ig.ioccults;
679 let gpc = gs.pieces.as_mut(modperm).remove(piece);
680 let ipc = ig.ipieces.as_mut(modperm).remove(piece);
682 let hook_r = hook(ioccults, &gs.occults,
683 ipc.as_ref(), gpc.as_ref());
685 if let Some(ipc) = ipc {
686 if let Some(gpc) = gpc {
687 ipc.p.into_inner().delete_hook(&gpc, gs);
689 if let Some(occilk) = ipc.occilk {
690 ig.ioccults.ilks.dispose_iilk(occilk);
694 (hook_r, PieceUpdateOp::Delete(), xupdates.into_unprepared(None))
697 /// caller is responsible for logging; threading it through
698 /// proves the caller has a log entry.
700 pub fn player_new(&mut self, gnew: GPlayer, inew: IPlayer,
701 account: Arc<AccountName>, logentry: LogEntry)
702 -> (PlayerId, PreparedUpdateEntry, LogEntry) {
703 // saving is fallible, but we can't attempt to save unless
704 // we have a thing to serialise with the player in it
705 self.check_new_nick(&gnew.nick)?;
706 if self.c.g.iplayers.values().any(|r| r.ipl.acctid == inew.acctid) {
707 Err(ME::AlreadyExists)?;
709 let player = self.c.g.gs.players.insert(gnew);
710 let u = PlayerUpdates::start(&self.c.g.gs).for_player();
711 let record = PlayerRecord { u, account, ipl: inew, };
713 self.c.g.iplayers.insert(player, record);
716 let update = self.prepare_set_player_update(player)?;
717 self.save_game_now()?;
718 self.save_aux_now()?;
719 Ok::<_,InternalError>(update)
721 self.c.g.iplayers.remove(player);
722 self.c.g.gs.players.remove(player);
723 // Perhaps we leave the g-* file with this player recorded,
724 // but this will be ignored when we load.
729 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
730 (player, update, logentry)
734 pub fn check_new_nick(&mut self, new_nick: &str) {
735 if self.c.g.gs.players.values().any(|old| old.nick == new_nick) {
736 Err(ME::NickCollision)?;
741 pub fn prepare_set_player_update(&self, player: PlayerId)
742 -> PreparedUpdateEntry {
743 let new_info_pane = Arc::new(self.player_info_pane()?);
744 PreparedUpdateEntry::SetPlayer {
745 player, new_info_pane,
746 data: DataLoadPlayer::from_player(self, player),
750 pub fn remove_clients(&mut self,
751 players: &HashSet<PlayerId>,
752 signal: ErrorSignaledViaUpdate<PUE_P, String>) {
753 let mut clients_to_remove = HashSet::new();
754 self.clients.retain(|k,v| {
755 let remove = players.contains(&v.player);
756 if remove { clients_to_remove.insert(k); }
760 let gen = self.c.g.gs.gen;
761 for &player in players {
762 if let Some(iplayer) = self.iplayers.get_mut(player) {
763 iplayer.u.push(PreparedUpdate {
765 when: Instant::now(),
766 us: vec![ PreparedUpdateEntry::Error(
772 self.tokens_deregister_for_id(|id| clients_to_remove.contains(&id));
775 // #[throws(InternalError)]
776 // https://github.com/withoutboats/fehler/issues/62
777 pub fn players_remove(&mut self, old_players_set: &HashSet<PlayerId>)
780 (Option<GPlayer>, Option<IPlayer>, PreparedUpdateEntry)
783 // We have to filter this player out of everything
786 // We make a copy so if the save fails, we can put everything back
788 let mut players = self.c.g.gs.players.clone();
789 let old_players: Vec<_> = old_players_set.iter().cloned().collect();
790 let old_gpls: Vec<_> = old_players.iter().cloned().map(|oldplayer| {
791 players.remove(oldplayer)
795 let mut gs = GameState {
796 // These parts are straightforward and correct
797 table_colour: self.c.g.gs.table_colour.clone(),
798 table_size: self.c.g.gs.table_size,
799 gen: self.c.g.gs.gen,
800 max_z: self.gs.max_z.clone(),
802 // These have special handling
808 let held_by_old = |p: &GPiece| if_chain! {
809 if let Some(held) = p.held;
810 if old_players_set.contains(&held);
815 let mut updated_pieces = vec![];
817 // drop order is reverse of creation order, so create undo
818 // after all the things it will reference
819 let mut undo: Vec<Box<dyn FnOnce(&mut InstanceGuard)>> = vec![];
822 for (piece,p) in &mut self.c.g.gs.pieces {
825 updated_pieces.push(piece);
828 undo.push(Box::new(|ig| for &piece in &updated_pieces {
830 held_by_old(ig.c.g.gs.pieces.get_mut(piece)?);
835 // Installs gs as the new game state, stealing the log and pieces
836 let mut swap_things = |ig: &mut InstanceGuard| {
837 mem::swap(&mut ig.c.g.gs.log, &mut gs.log );
838 mem::swap(&mut ig.c.g.gs.pieces, &mut gs.pieces);
839 mem::swap(&mut ig.c.g.gs.occults,&mut gs.occults);
840 mem::swap(&mut ig.c.g.gs, &mut gs, );
843 undo.push(Box::new(swap_things));
845 let new_info_pane = Arc::new(self.player_info_pane()?);
847 self.save_game_now().map_err(|e|{
849 #[allow(clippy::iter_with_drain)] // don't eat undo, so we can drop it
850 for u in undo.drain(..).rev() {
856 // point of no return
860 for &piece in &updated_pieces {
862 self.c.g.gs.pieces.get_mut(piece)?.gen = self.c.g.gs.gen;
866 let estimate = updated_pieces.len() + 1;
867 let mut buf = PrepareUpdatesBuffer::new(self, Some(estimate));
868 for &piece in &updated_pieces {
869 buf.piece_update(piece, &None, PieceUpdateOp::Modify(()).into());
873 self.remove_clients(old_players_set, ESVU::PlayerRemoved);
874 self.tokens_deregister_for_id(
875 |id:PlayerId| old_players_set.contains(&id)
877 let old_ipls: Vec<_> = old_players.iter().cloned().map(
878 |oldplayer| self.iplayers.remove(oldplayer)
881 self.save_aux_now().unwrap_or_else(
883 "trouble garbage collecting accesses for deleted player: {:?}",
887 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
889 let updates = old_players.iter().cloned().map(
890 |player| PreparedUpdateEntry::RemovePlayer {
892 new_info_pane: new_info_pane.clone(),
905 #[throws(InternalError)]
906 pub fn invalidate_tokens(&mut self, player: PlayerId) {
907 let old_tokens = TokenRegistry {
908 tr: self.tokens_players.tr.clone(),
909 id: self.tokens_players.id,
911 self.tokens_deregister_for_id(|id:PlayerId| id==player);
912 self.save_aux_now().map_err(|e|{
913 // oof, the tokens are already out of the global map, but
914 // not saved, so they might come back. We need to leave
915 // them here so they can be deleted later.
916 self.tokens_players = old_tokens;
919 // ppoint of no return
921 self.remove_clients(&[player].iter().cloned().collect(),
923 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
927 fn player_access_reset_redeliver(&mut self,
928 accounts: &mut AccountsGuard,
930 _auth: Authorisation<AccountName>,
932 -> AccessTokenReport {
933 let acctid = self.iplayers.byid(player)?.ipl.acctid;
936 let (acct, _) = accounts.lookup(acctid)?;
937 let access = acct.access.clone();
938 let desc = access.describe_html();
939 let now = Timestamp::now();
940 let revk = TokenRevelationKey {
941 account: (*acct.account).clone(),
944 self.iplayers.byid_mut(player)?.ipl.tokens_revealed.entry(revk)
945 .or_insert(TokenRevelationValue {
953 let current_tokens: ArrayVec<&RawToken,2> = {
954 let players = GLOBAL.players.read();
955 self.tokens_players.tr.iter().
957 let iad = players.get(token)?;
958 if iad.ident != player { return None }
959 if ! Arc::ptr_eq(&iad.gref.0, &self.gref.0) { return None }
966 let reset = reset || current_tokens.is_empty();
968 let token: RawToken = if reset {
969 drop(current_tokens);
971 self.invalidate_tokens(player)?;
972 self.save_aux_now()?;
978 RawToken::new_random()
981 let iad = InstanceAccessDetails {
982 gref: self.gref.clone(),
986 self.token_register(token.clone(), iad);
987 self.save_aux_now()?;
993 let token = match current_tokens.as_slice() {
994 [] => panic!(), // this possibility was excluded earlier
997 warn!("duplicate token for {}", player);
998 throw!(ME::ServerFailure("duplicate token".to_string()));
1005 let ipl = &self.c.g.iplayers.byid(player)?.ipl;
1006 let gpl = self.c.g.gs.players.byid(player)?;
1008 let url = format!("{}/?{}",
1009 &config().public_url.trim_end_matches('/'),
1011 let info = AccessTokenInfo { url };
1012 let report = access.deliver(accounts, &self.c.g, gpl, ipl, info)?;
1016 #[throws(MgmtError)]
1017 pub fn player_access_reset(&mut self,
1018 accounts: &mut AccountsGuard,
1020 auth: Authorisation<AccountName>)
1021 -> AccessTokenReport {
1022 self.player_access_reset_redeliver(accounts, player, auth, true)?
1025 #[throws(MgmtError)]
1026 pub fn player_access_redeliver(&mut self,
1027 accounts: &mut AccountsGuard,
1029 auth: Authorisation<AccountName>)
1030 -> AccessTokenReport {
1031 self.player_access_reset_redeliver(accounts, player, auth, false)?
1034 pub fn modify_pieces(&mut self) -> ModifyingPieces {
1035 self.save_game_and_aux_later();
1036 // want this to be borrowed from self, so that we tie it properly
1037 // to the same game. But in practice we don't expect to write
1038 // bugs where we get different games mixed up. Borrowing self
1039 // from the caller's pov is troublesome beczuse ultimately the
1040 // caller will need to manipulate various fields of Instance (so
1041 // we mustn't have a borrow of it).
1045 pub fn modify_pieces_not_necessarily_saving_aux(&mut self)
1046 -> ModifyingPieces {
1047 self.save_game_later();
1051 fn token_register<Id:AccessId>(
1054 iad: InstanceAccessDetails<Id>
1056 Id::tokens_registry(&mut self.c.g, PRIVATE_Y).tr.insert(token.clone());
1057 Id::global_tokens(PRIVATE_Y).write().insert(token, iad);
1060 fn forget_all_tokens<Id:AccessId>(tokens: &mut TokenRegistry<Id>) {
1061 let global: &RwLock<TokenTable<Id>> = AccessId::global_tokens(PRIVATE_Y);
1062 let mut global = global.write();
1063 for t in tokens.tr.drain() { global.remove(&t); }
1066 fn tokens_deregister_for_id<Id:AccessId, F: Fn(Id) -> bool
1067 > (&mut self, oldid: F) {
1068 let mut tokens = AccessId::global_tokens(PRIVATE_Y).write();
1069 tokens.retain(|k,v| if_chain! {
1071 if Id::tokens_registry(self, PRIVATE_Y).tr.remove(k);
1079 // ---------- save/load ----------
1081 #[derive(Copy,Clone,Debug)]
1082 enum SaveFileOrDir { File, Dir }
1084 impl SaveFileOrDir {
1085 #[throws(io::Error)]
1086 fn remove<P:AsRef<std::path::Path>>(self, path: P) {
1088 SaveFileOrDir::File => fs::remove_file(path)?,
1089 SaveFileOrDir::Dir => fs::remove_dir_all(path)?,
1095 enum SavefilenameParseResult {
1097 Auxiliary(SaveFileOrDir),
1100 aux_leaves: Vec<Vec<u8>>,
1105 pub fn savefilename(name: &InstanceName, prefix: &str, suffix: &str)
1107 [ config().save_dir().as_str(), "/", prefix ]
1108 .iter().map(Deref::deref)
1109 .chain(iter::once( name.to_string().as_str() ))
1110 .chain([ suffix ].iter().map(Deref::deref))
1114 #[throws(anyhow::Error)]
1115 fn savefilename_parse(leaf: &[u8]) -> SavefilenameParseResult {
1116 use SavefilenameParseResult::*;
1118 if leaf.starts_with(b"a-") { return Auxiliary(SaveFileOrDir::File) }
1119 if leaf.starts_with(b"b-") { return Auxiliary(SaveFileOrDir::Dir ) }
1120 let rhs = match leaf.strip_prefix(b"g-") {
1122 None => return NotGameFile,
1124 let after_ftype_prefix = rhs;
1125 let rhs = str::from_utf8(rhs)?;
1126 let rcomp = rhs.rsplitn(2, ':').next().unwrap();
1127 if rcomp.find('.').is_some() { return TempToDelete }
1129 let name = InstanceName::from_str(rhs)?;
1131 let aux_leaves = [ b"a-", b"b-" ].iter().map(|prefix| {
1132 let mut s: Vec<_> = (prefix[..]).into(); s.extend(after_ftype_prefix); s
1134 GameFile { name, aux_leaves }
1137 impl InstanceGuard<'_> {
1138 #[throws(InternalError)]
1140 &self, prefix: &str,
1141 w: fn(s: &Self, w: &mut BufWriter<fs::File>)
1142 -> Result<(),rmp_serde::encode::Error>
1144 let tmp = savefilename(&self.name, prefix,".tmp");
1145 let f = fs::File::create(&tmp)
1146 .with_context(||format!("save: create {:?}",&tmp))?;
1147 let mut f = BufWriter::new(f);
1150 .with_context(||format!("save: flush {:?}",&tmp))?;
1152 f.into_inner().map_err(|e| { let e: io::Error = e.into(); e })
1153 .with_context(||format!("save: close {:?}",&tmp))?
1155 let out = savefilename(&self.name, prefix,"");
1156 fs::rename(&tmp, &out).context("install")
1157 .with_context(||format!("save: install {:?} as {:?}", &tmp, &out))?;
1158 debug!("saved to {}", &out);
1161 #[throws(InternalError)]
1162 pub fn save_game_now(&mut self) {
1163 if self.c.aux_dirty {
1164 self.save_aux_now()?;
1166 self.save_something("g-", |s,w| {
1167 rmp_serde::encode::write_named(w, &s.c.g.gs)
1169 self.c.game_dirty = false;
1170 debug!("saved (now) {}", &self.name);
1173 #[throws(InternalError)]
1174 pub fn save_aux_now(&mut self) {
1175 self.save_something("a-", |s, w| {
1176 let ipieces = &s.c.g.ipieces;
1177 let ioccults = &s.c.g.ioccults;
1178 let pcaliases = &s.c.g.pcaliases;
1179 let ifastsplits = &s.c.g.ifastsplits;
1180 let tokens_players: Vec<(&str, PlayerId)> = {
1181 let global_players = GLOBAL.players.read();
1182 s.c.g.tokens_players.tr
1185 global_players.get(token)
1186 .map(|player| (token.0.as_str(), player.ident)))
1189 let aplayers = s.c.g.iplayers.iter().map(
1190 |(player, PlayerRecord { ipl, .. })|
1191 (player, ipl.clone())
1193 let acl = s.c.g.acl.clone().into();
1194 let links = s.c.g.links.clone();
1195 let asset_url_key = s.c.g.asset_url_key.clone();
1196 let bundle_hashes = s.c.g.bundle_hashes.clone();
1197 let isa = InstanceSaveAuxiliary {
1198 ipieces, ioccults, tokens_players, aplayers, acl, links,
1199 pcaliases, asset_url_key, bundle_hashes, ifastsplits,
1201 rmp_serde::encode::write_named(w, &isa)
1203 self.c.aux_dirty = false;
1204 info!("saved aux for {}", &self.name);
1207 #[throws(InternalError)]
1208 fn load_something<T:DeserializeOwned>(name: &InstanceName, prefix: &str)
1210 let inp = savefilename(name, prefix, "");
1211 let f = fs::File::open(&inp).with_context(|| inp.clone())?;
1212 let mut f = BufReader::new(f);
1213 let thing = rmp_serde::decode::from_read(&mut f)?;
1214 debug!("loaded from {:?}", &inp);
1218 #[throws(StartupError)]
1219 fn load_game(accounts: &AccountsGuard,
1220 games: &mut GamesGuard,
1221 name: InstanceName) -> Option<InstanceRef> {
1222 let InstanceSaveAuxiliary::
1223 <String,ActualIPieces,IOccults,PieceAliases,IFastSplits> {
1224 tokens_players, mut ipieces, mut ioccults, mut aplayers, acl, links,
1225 pcaliases, asset_url_key, bundle_hashes, mut ifastsplits,
1226 } = match Self::load_something(&name, "a-") {
1229 let ae = match &e { InternalError::Anyhow(ae) => Some(ae), _=>None }?;
1230 let ioe = ae.downcast_ref::<io::Error>()?;
1231 let is_enoent = ioe.kind() == io::ErrorKind::NotFound;
1232 is_enoent.as_option()
1240 let mut gs: GameState = Self::load_something(&name, "g-")?;
1242 fn discard_mismatches<K:slotmap::Key, V1, V2>(
1243 primary: &mut DenseSlotMap<K, V1>,
1244 secondary: &mut SecondarySlotMap<K, V2>,
1246 primary.retain(|k,_v| secondary.contains_key(k));
1247 secondary.retain(|k,_v| primary.contains_key(k));
1250 for (piece, gpc) in &mut gs.pieces.0 {
1251 if_let!{ Some(fsid) = gpc.fastsplit; else continue; }
1253 // We must recover the ipc via the fastsplit table, or delete
1255 let ilks = &mut ioccults.ilks;
1256 if let Some(recovered_ipc) = ifastsplits.recover_ipc(ilks, fsid);
1259 if let Some(old_ipc) = ipieces.get_mut(piece);
1260 // We're about to overwrite this ipc, maybe owns some occilk.
1261 // If in fact we're the same kind, we've already acquired
1262 // another refcount from recover_ipc, above.
1263 if let Some(old_iilk) = old_ipc.occilk.take();
1264 then { ilks.dispose_iilk(old_iilk); }
1266 ipieces.insert(piece, recovered_ipc);
1268 // Not available in ipieces, fastsplit family must just have
1269 // been added. This will get rid of it from gpieces, too, below.
1270 ipieces.remove(piece);
1274 ifastsplits.cleanup(&mut ioccults.ilks);
1276 discard_mismatches(&mut gs.players, &mut aplayers);
1277 discard_mismatches(&mut gs.pieces.0, &mut ipieces);
1279 let pu_bc = PlayerUpdates::start(&gs);
1281 let iplayers: SecondarySlotMap<PlayerId, PlayerRecord> = {
1284 }.filter_map(|(player, ipl)| {
1285 let u = pu_bc.for_player();
1286 let account = accounts.lookup(ipl.acctid).ok()?.0.account.clone();
1287 Some((player, PlayerRecord { u, ipl, account }))
1290 for mut p in gs.pieces.values_mut() {
1291 p.lastclient = default();
1292 if let Some(held) = p.held {
1293 if !gs.players.contains_key(held) { p.held = None }
1297 let name = Arc::new(name);
1298 let (tokens_players, acctids_players) = {
1299 let mut tokens = Vec::with_capacity(tokens_players.len());
1300 let mut acctids = Vec::with_capacity(tokens_players.len());
1301 for (token, player) in tokens_players { if_chain! {
1302 if let Some(record) = iplayers.get(player);
1304 tokens.push((token, player));
1305 acctids.push(record.ipl.acctid);
1308 (tokens, accounts.bulk_check(&acctids))
1311 let mut g = Instance {
1312 gs, iplayers, links,
1314 ipieces: IPieces(ipieces),
1319 tokens_clients: default(),
1320 tokens_players: default(),
1321 bundle_list: default(), // set by load_game_bundles
1322 local_libs: default(), // set by load_game_bundles
1323 bundle_specs: default(), // set by load_game_bundles
1329 let b = InstanceBundles::reload_game_bundles(&mut g)?;
1330 let b = Mutex::new(b);
1332 let c = InstanceContainer {
1338 let c = Mutex::new(c);
1339 let gref = InstanceRef(Arc::new(InstanceOuter { c, b }));
1340 let mut g = gref.lock().unwrap();
1343 for (piece, ipc) in ig.ipieces.0.iter() {
1344 ipc.direct_trait_access()
1345 .save_reloaded_hook(piece, &mut ig.gs, &gref)?;
1348 for (token, _) in &tokens_players {
1349 g.tokens_players.tr.insert(RawToken(token.clone()));
1351 let mut global = GLOBAL.players.write();
1352 for ((token, player), acctid) in
1353 tokens_players.into_iter()
1354 .zip(acctids_players)
1356 if let Some(acctid) = acctid;
1357 let iad = InstanceAccessDetails {
1362 then { global.insert(RawToken(token), iad); }
1366 games.insert(name.clone(), gref.clone());
1367 info!("loadewd {:?}", &name);
1372 #[throws(anyhow::Error)]
1373 pub fn load_games(accounts: &mut AccountsGuard,
1374 games: &mut GamesGuard) {
1375 enum AFState { Found(PathBuf, SaveFileOrDir), Used }
1377 use SavefilenameParseResult::*;
1378 let mut a_leaves = HashMap::new();
1379 let save_dir = config().save_dir().clone();
1380 for de in fs::read_dir(&save_dir).context(save_dir)? {
1382 let leaf = de.file_name();
1384 let leaf = leaf.as_bytes();
1385 match savefilename_parse(leaf)? {
1389 fs::remove_file(de.path())
1390 .context("stale temporary file")?;
1393 a_leaves.entry(leaf.to_owned()).or_insert_with(
1394 || Found(de.path(), fd)
1397 GameFile { aux_leaves, name } => {
1398 InstanceGuard::load_game(accounts, games, name)?;
1399 for aux_leaf in aux_leaves {
1400 a_leaves.insert(aux_leaf, Used);
1404 <Result<_,anyhow::Error>>::Ok(())
1405 })().with_context(|| format!("leaf={:?}", leaf))?;
1408 for (leaf, state) in &a_leaves {
1409 if let Found(path, fd) = state {
1411 .with_context(|| format!("leaf={:?}", leaf))?;
1414 <Result<_,anyhow::Error>>::Ok(())
1415 })().context("cleaning up stale files")?;
1416 info!("loaded games");
1419 // ---------- Tokens / TokenTable / AccessId ----------
1421 pub type TokenTable<Id> = HashMap<RawToken, InstanceAccessDetails<Id>>;
1423 pub trait AccessId: Copy + Clone + 'static {
1424 type Error: Into<Fatal>;
1425 const ERROR: Self::Error;
1426 fn global_tokens(_:PrivateCaller) -> &'static RwLock<TokenTable<Self>>;
1427 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1428 -> &mut TokenRegistry<Self>;
1431 #[derive(Debug,Copy,Clone,Eq,PartialEq,Ord,PartialOrd)]
1432 #[derive(Serialize,Deserialize)]
1434 #[error("Player not found")]
1435 pub struct PlayerNotFound;
1437 impl AccessId for PlayerId {
1438 type Error = PlayerNotFound;
1439 const ERROR: PlayerNotFound = PlayerNotFound;
1440 fn global_tokens(_: PrivateCaller) -> &'static RwLock<TokenTable<Self>> {
1443 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1444 -> &mut TokenRegistry<Self> {
1445 &mut ig.tokens_players
1448 impl AccessId for ClientId {
1450 const ERROR: Fatal = NoClient;
1451 fn global_tokens(_: PrivateCaller) -> &'static RwLock<TokenTable<Self>> {
1454 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1455 -> &mut TokenRegistry<Self> {
1456 &mut ig.tokens_clients
1461 fn new_random() -> Self {
1462 let mut rng = thread_rng();
1463 let token = RawToken (
1464 repeat_with(|| rng.sample(Alphanumeric))
1472 pub fn lookup_token<Id:AccessId>(s: &RawTokenVal)
1473 -> Result<InstanceAccessDetails<Id>, Id::Error> {
1474 Id::global_tokens(PRIVATE_Y).read().get(s).cloned()
1479 pub fn record_token<Id:AccessId> (
1480 ig: &mut InstanceGuard,
1481 iad: InstanceAccessDetails<Id>
1483 let token = RawToken::new_random();
1484 ig.token_register(token.clone(), iad);
1489 pub fn process_all_players_for_account<
1491 F: FnMut(&mut InstanceGuard<'_>, PlayerId) -> Result<(),E>
1493 (games: &mut GamesGuard, acctid: AccountId, mut f: F)
1495 for gref in games.values() {
1496 let c = gref.lock_even_destroying();
1497 let remove: Vec<_> = c.g.iplayers.iter().filter_map(|(player,pr)| {
1498 if pr.ipl.acctid == acctid { Some(player) } else { None }
1500 let mut ig = InstanceGuard { gref: gref.clone(), c };
1501 for player in remove.into_iter() {
1502 f(&mut ig, player)?;
1507 // ========== instance pieces data access ==========
1510 pub fn get(&self, piece: PieceId) -> Option<&IPiece> {
1514 pub fn as_mut(&mut self, _: ModifyingPieces) -> &mut ActualIPieces {
1518 pub fn is_empty(&self) -> bool {
1519 let IPieces(actual) = self;
1524 // ---------- gamestate pieces table ----------
1527 pub fn get_mut(&mut self, piece: PieceId) -> Option<&mut GPiece> {
1528 self.0.get_mut(piece)
1530 pub fn values_mut(&mut self) -> sm::ValuesMut<PieceId, GPiece> {
1533 pub fn as_mut(&mut self, _: ModifyingPieces) -> &mut ActualGPieces {
1537 pub fn as_mut_t(&mut self) -> &mut ActualGPieces { &mut self.0 }
1540 impl ById for GPieces {
1542 type Entry = GPiece;
1543 type Error = Inapplicable;
1545 fn byid(&self, piece: PieceId) -> &GPiece {
1546 self.get(piece).ok_or(Ia::PieceGone)?
1549 fn byid_mut(&mut self, piece: PieceId) -> &mut GPiece {
1550 self.get_mut(piece).ok_or(Ia::PieceGone)?
1554 /*impl<'p> IntoIterator for &'p Pieces {
1555 type Item = (PieceId, &'p PieceState);
1556 type IntoIter = sm::Iter<'p, PieceId, PieceState>;
1557 fn into_iter(self) -> Self::IntoIter { (&self.0).into_iter() }
1559 impl<'p> IntoIterator for &'p mut GPieces {
1560 type Item = (PieceId, &'p mut GPiece);
1561 type IntoIter = sm::IterMut<'p, PieceId, GPiece>;
1562 fn into_iter(self) -> Self::IntoIter { (&mut self.0).into_iter() }
1565 // ========== background maintenance ==========
1567 // ---------- delayed game save ----------
1569 impl InstanceGuard<'_> {
1570 pub fn save_game_later(&mut self) {
1571 if self.c.game_dirty { return }
1572 GLOBAL.dirty.lock().push_back(self.gref.clone());
1573 self.c.game_dirty = true;
1576 pub fn save_game_and_aux_later(&mut self) {
1577 if self.c.aux_dirty { return }
1578 self.save_game_later();
1579 self.c.aux_dirty = true;
1583 pub fn game_flush_task() {
1584 let mut inner_queue = VecDeque::new();
1587 mem::swap(&mut inner_queue, &mut *GLOBAL.dirty.lock());
1589 thread::sleep(GAME_SAVE_LAG);
1590 for _ in 0..inner_queue.len() {
1591 let ent = inner_queue.pop_front().unwrap();
1592 let mut ig = match ent.lock() { Ok(ig) => ig, _ => continue/*ah well*/ };
1593 if !ig.c.game_dirty { continue }
1594 match ig.save_game_now() {
1596 assert!(!ig.c.game_dirty);
1599 // todo: notify the players
1600 error!("save error! name={:?}: {}", &ig.name, &e);
1602 inner_queue.push_back(ent); // oof
1609 // ---------- client expiry ----------
1611 fn client_expire_old_clients() {
1612 let mut expire = vec![];
1613 let max_age = Instant::now() - MAX_CLIENT_INACTIVITY;
1615 trait ClientIterator {
1617 fn iter<'g>(&mut self, gref: &'g InstanceRef, max_age: Instant)
1618 -> (MutexGuard<'g, InstanceContainer>, Option<Self::Ret>) {
1619 let c = gref.lock_even_destroying();
1620 let ret = 'ret: loop {
1621 for (client, cl) in &c.g.clients {
1622 if cl.lastseen > max_age { continue }
1623 let ret = self.old(client);
1624 if ret.is_some() { break 'ret ret }
1630 fn old(&mut self, client: ClientId) -> Option<Self::Ret>;
1633 for gref in GLOBAL.games_table.read().values() {
1635 impl ClientIterator for Any {
1637 fn old(&mut self, _client: ClientId) -> Option<()> {
1641 if let (_, Some(())) = Any.iter(gref, max_age) {
1642 expire.push(gref.clone());
1645 for gref in expire.into_iter() {
1647 struct Now(HashSet<ClientId>);
1648 impl ClientIterator for Now {
1650 fn old(&mut self, client: ClientId) -> Option<Void> {
1651 self.0.insert(client);
1656 let mut now = Now(default());
1657 let (mut c, _) = now.iter(&gref, max_age);
1658 c.g.clients.retain(|c,_| !now.0.contains(&c));
1659 let mut gref = InstanceGuard { c, gref: gref.clone() };
1660 debug!("expiring client {:?}", &now);
1661 gref.tokens_deregister_for_id::<ClientId,_>(|c| now.0.contains(&c));
1665 pub fn client_periodic_expiry() {
1667 sleep(MAX_CLIENT_INACTIVITY);
1668 client_expire_old_clients();
1672 // ---------- log expiry ----------
1674 fn global_expire_old_logs() {
1675 let cutoff = Timestamp(Timestamp::now().0 - MAX_LOG_AGE.as_secs());
1677 let mut want_expire = vec![];
1679 let read = GLOBAL.games_table.read();
1680 for gref in read.values() {
1681 if gref.lock_even_destroying().g.gs.want_expire_some_logs(cutoff) {
1682 want_expire.push(gref.clone())
1687 for gref in want_expire.into_iter() {
1688 let mut g = gref.lock_even_destroying();
1689 info!("expiring old log entries in {:?}", &g.g.name);
1690 g.g.gs.do_expire_old_logs(cutoff);
1694 pub fn logs_periodic_expiry() {
1696 sleep(MAX_LOG_AGE / 10);
1697 global_expire_old_logs();