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 new_info_pane = Arc::new(self.player_info_pane()?);
718 let update = PreparedUpdateEntry::SetPlayer {
719 player, new_info_pane,
720 data: DataLoadPlayer::from_player(self, player),
723 self.save_game_now()?;
724 self.save_aux_now()?;
725 Ok::<_,InternalError>(update)
727 self.c.g.iplayers.remove(player);
728 self.c.g.gs.players.remove(player);
729 // Perhaps we leave the g-* file with this player recorded,
730 // but this will be ignored when we load.
735 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
736 (player, update, logentry)
740 pub fn check_new_nick(&mut self, new_nick: &str) {
741 if self.c.g.gs.players.values().any(|old| old.nick == new_nick) {
742 Err(ME::NickCollision)?;
746 pub fn remove_clients(&mut self,
747 players: &HashSet<PlayerId>,
748 signal: ErrorSignaledViaUpdate<PUE_P, String>) {
749 let mut clients_to_remove = HashSet::new();
750 self.clients.retain(|k,v| {
751 let remove = players.contains(&v.player);
752 if remove { clients_to_remove.insert(k); }
756 let gen = self.c.g.gs.gen;
757 for &player in players {
758 if let Some(iplayer) = self.iplayers.get_mut(player) {
759 iplayer.u.push(PreparedUpdate {
761 when: Instant::now(),
762 us: vec![ PreparedUpdateEntry::Error(
768 self.tokens_deregister_for_id(|id| clients_to_remove.contains(&id));
771 // #[throws(InternalError)]
772 // https://github.com/withoutboats/fehler/issues/62
773 pub fn players_remove(&mut self, old_players_set: &HashSet<PlayerId>)
776 (Option<GPlayer>, Option<IPlayer>, PreparedUpdateEntry)
779 // We have to filter this player out of everything
782 // We make a copy so if the save fails, we can put everything back
784 let mut players = self.c.g.gs.players.clone();
785 let old_players: Vec<_> = old_players_set.iter().cloned().collect();
786 let old_gpls: Vec<_> = old_players.iter().cloned().map(|oldplayer| {
787 players.remove(oldplayer)
791 let mut gs = GameState {
792 // These parts are straightforward and correct
793 table_colour: self.c.g.gs.table_colour.clone(),
794 table_size: self.c.g.gs.table_size,
795 gen: self.c.g.gs.gen,
796 max_z: self.gs.max_z.clone(),
798 // These have special handling
804 let held_by_old = |p: &GPiece| if_chain! {
805 if let Some(held) = p.held;
806 if old_players_set.contains(&held);
811 let mut updated_pieces = vec![];
813 // drop order is reverse of creation order, so create undo
814 // after all the things it will reference
815 let mut undo: Vec<Box<dyn FnOnce(&mut InstanceGuard)>> = vec![];
818 for (piece,p) in &mut self.c.g.gs.pieces {
821 updated_pieces.push(piece);
824 undo.push(Box::new(|ig| for &piece in &updated_pieces {
826 held_by_old(ig.c.g.gs.pieces.get_mut(piece)?);
831 // Installs gs as the new game state, stealing the log and pieces
832 let mut swap_things = |ig: &mut InstanceGuard| {
833 mem::swap(&mut ig.c.g.gs.log, &mut gs.log );
834 mem::swap(&mut ig.c.g.gs.pieces, &mut gs.pieces);
835 mem::swap(&mut ig.c.g.gs.occults,&mut gs.occults);
836 mem::swap(&mut ig.c.g.gs, &mut gs, );
839 undo.push(Box::new(swap_things));
841 let new_info_pane = Arc::new(self.player_info_pane()?);
843 self.save_game_now().map_err(|e|{
845 #[allow(clippy::iter_with_drain)] // don't eat undo, so we can drop it
846 for u in undo.drain(..).rev() {
852 // point of no return
856 for &piece in &updated_pieces {
858 self.c.g.gs.pieces.get_mut(piece)?.gen = self.c.g.gs.gen;
862 let estimate = updated_pieces.len() + 1;
863 let mut buf = PrepareUpdatesBuffer::new(self, Some(estimate));
864 for &piece in &updated_pieces {
865 buf.piece_update(piece, &None, PieceUpdateOp::Modify(()).into());
869 self.remove_clients(old_players_set, ESVU::PlayerRemoved);
870 self.tokens_deregister_for_id(
871 |id:PlayerId| old_players_set.contains(&id)
873 let old_ipls: Vec<_> = old_players.iter().cloned().map(
874 |oldplayer| self.iplayers.remove(oldplayer)
877 self.save_aux_now().unwrap_or_else(
879 "trouble garbage collecting accesses for deleted player: {:?}",
883 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
885 let updates = old_players.iter().cloned().map(
886 |player| PreparedUpdateEntry::RemovePlayer {
888 new_info_pane: new_info_pane.clone(),
901 #[throws(InternalError)]
902 pub fn invalidate_tokens(&mut self, player: PlayerId) {
903 let old_tokens = TokenRegistry {
904 tr: self.tokens_players.tr.clone(),
905 id: self.tokens_players.id,
907 self.tokens_deregister_for_id(|id:PlayerId| id==player);
908 self.save_aux_now().map_err(|e|{
909 // oof, the tokens are already out of the global map, but
910 // not saved, so they might come back. We need to leave
911 // them here so they can be deleted later.
912 self.tokens_players = old_tokens;
915 // ppoint of no return
917 self.remove_clients(&[player].iter().cloned().collect(),
919 })(); // <- No ?, ensures that IEFE is infallible (barring panics)
923 fn player_access_reset_redeliver(&mut self,
924 accounts: &mut AccountsGuard,
926 _auth: Authorisation<AccountName>,
928 -> AccessTokenReport {
929 let acctid = self.iplayers.byid(player)?.ipl.acctid;
932 let (acct, _) = accounts.lookup(acctid)?;
933 let access = acct.access.clone();
934 let desc = access.describe_html();
935 let now = Timestamp::now();
936 let revk = TokenRevelationKey {
937 account: (*acct.account).clone(),
940 self.iplayers.byid_mut(player)?.ipl.tokens_revealed.entry(revk)
941 .or_insert(TokenRevelationValue {
949 let current_tokens: ArrayVec<&RawToken,2> = {
950 let players = GLOBAL.players.read();
951 self.tokens_players.tr.iter().
953 let iad = players.get(token)?;
954 if iad.ident != player { return None }
955 if ! Arc::ptr_eq(&iad.gref.0, &self.gref.0) { return None }
962 let reset = reset || current_tokens.is_empty();
964 let token: RawToken = if reset {
965 drop(current_tokens);
967 self.invalidate_tokens(player)?;
968 self.save_aux_now()?;
974 RawToken::new_random()
977 let iad = InstanceAccessDetails {
978 gref: self.gref.clone(),
982 self.token_register(token.clone(), iad);
983 self.save_aux_now()?;
989 let token = match current_tokens.as_slice() {
990 [] => panic!(), // this possibility was excluded earlier
993 warn!("duplicate token for {}", player);
994 throw!(ME::ServerFailure("duplicate token".to_string()));
1001 let ipl = &self.c.g.iplayers.byid(player)?.ipl;
1002 let gpl = self.c.g.gs.players.byid(player)?;
1004 let url = format!("{}/?{}",
1005 &config().public_url.trim_end_matches('/'),
1007 let info = AccessTokenInfo { url };
1008 let report = access.deliver(accounts, &self.c.g, gpl, ipl, info)?;
1012 #[throws(MgmtError)]
1013 pub fn player_access_reset(&mut self,
1014 accounts: &mut AccountsGuard,
1016 auth: Authorisation<AccountName>)
1017 -> AccessTokenReport {
1018 self.player_access_reset_redeliver(accounts, player, auth, true)?
1021 #[throws(MgmtError)]
1022 pub fn player_access_redeliver(&mut self,
1023 accounts: &mut AccountsGuard,
1025 auth: Authorisation<AccountName>)
1026 -> AccessTokenReport {
1027 self.player_access_reset_redeliver(accounts, player, auth, false)?
1030 pub fn modify_pieces(&mut self) -> ModifyingPieces {
1031 self.save_game_and_aux_later();
1032 // want this to be borrowed from self, so that we tie it properly
1033 // to the same game. But in practice we don't expect to write
1034 // bugs where we get different games mixed up. Borrowing self
1035 // from the caller's pov is troublesome beczuse ultimately the
1036 // caller will need to manipulate various fields of Instance (so
1037 // we mustn't have a borrow of it).
1041 pub fn modify_pieces_not_necessarily_saving_aux(&mut self)
1042 -> ModifyingPieces {
1043 self.save_game_later();
1047 fn token_register<Id:AccessId>(
1050 iad: InstanceAccessDetails<Id>
1052 Id::tokens_registry(&mut self.c.g, PRIVATE_Y).tr.insert(token.clone());
1053 Id::global_tokens(PRIVATE_Y).write().insert(token, iad);
1056 fn forget_all_tokens<Id:AccessId>(tokens: &mut TokenRegistry<Id>) {
1057 let global: &RwLock<TokenTable<Id>> = AccessId::global_tokens(PRIVATE_Y);
1058 let mut global = global.write();
1059 for t in tokens.tr.drain() { global.remove(&t); }
1062 fn tokens_deregister_for_id<Id:AccessId, F: Fn(Id) -> bool
1063 > (&mut self, oldid: F) {
1064 let mut tokens = AccessId::global_tokens(PRIVATE_Y).write();
1065 tokens.retain(|k,v| if_chain! {
1067 if Id::tokens_registry(self, PRIVATE_Y).tr.remove(k);
1075 // ---------- save/load ----------
1077 #[derive(Copy,Clone,Debug)]
1078 enum SaveFileOrDir { File, Dir }
1080 impl SaveFileOrDir {
1081 #[throws(io::Error)]
1082 fn remove<P:AsRef<std::path::Path>>(self, path: P) {
1084 SaveFileOrDir::File => fs::remove_file(path)?,
1085 SaveFileOrDir::Dir => fs::remove_dir_all(path)?,
1091 enum SavefilenameParseResult {
1093 Auxiliary(SaveFileOrDir),
1096 aux_leaves: Vec<Vec<u8>>,
1101 pub fn savefilename(name: &InstanceName, prefix: &str, suffix: &str)
1103 [ config().save_dir().as_str(), "/", prefix ]
1104 .iter().map(Deref::deref)
1105 .chain(iter::once( name.to_string().as_str() ))
1106 .chain([ suffix ].iter().map(Deref::deref))
1110 #[throws(anyhow::Error)]
1111 fn savefilename_parse(leaf: &[u8]) -> SavefilenameParseResult {
1112 use SavefilenameParseResult::*;
1114 if leaf.starts_with(b"a-") { return Auxiliary(SaveFileOrDir::File) }
1115 if leaf.starts_with(b"b-") { return Auxiliary(SaveFileOrDir::Dir ) }
1116 let rhs = match leaf.strip_prefix(b"g-") {
1118 None => return NotGameFile,
1120 let after_ftype_prefix = rhs;
1121 let rhs = str::from_utf8(rhs)?;
1122 let rcomp = rhs.rsplitn(2, ':').next().unwrap();
1123 if rcomp.find('.').is_some() { return TempToDelete }
1125 let name = InstanceName::from_str(rhs)?;
1127 let aux_leaves = [ b"a-", b"b-" ].iter().map(|prefix| {
1128 let mut s: Vec<_> = (prefix[..]).into(); s.extend(after_ftype_prefix); s
1130 GameFile { name, aux_leaves }
1133 impl InstanceGuard<'_> {
1134 #[throws(InternalError)]
1136 &self, prefix: &str,
1137 w: fn(s: &Self, w: &mut BufWriter<fs::File>)
1138 -> Result<(),rmp_serde::encode::Error>
1140 let tmp = savefilename(&self.name, prefix,".tmp");
1141 let f = fs::File::create(&tmp)
1142 .with_context(||format!("save: create {:?}",&tmp))?;
1143 let mut f = BufWriter::new(f);
1146 .with_context(||format!("save: flush {:?}",&tmp))?;
1148 f.into_inner().map_err(|e| { let e: io::Error = e.into(); e })
1149 .with_context(||format!("save: close {:?}",&tmp))?
1151 let out = savefilename(&self.name, prefix,"");
1152 fs::rename(&tmp, &out).context("install")
1153 .with_context(||format!("save: install {:?} as {:?}", &tmp, &out))?;
1154 debug!("saved to {}", &out);
1157 #[throws(InternalError)]
1158 pub fn save_game_now(&mut self) {
1159 if self.c.aux_dirty {
1160 self.save_aux_now()?;
1162 self.save_something("g-", |s,w| {
1163 rmp_serde::encode::write_named(w, &s.c.g.gs)
1165 self.c.game_dirty = false;
1166 debug!("saved (now) {}", &self.name);
1169 #[throws(InternalError)]
1170 pub fn save_aux_now(&mut self) {
1171 self.save_something("a-", |s, w| {
1172 let ipieces = &s.c.g.ipieces;
1173 let ioccults = &s.c.g.ioccults;
1174 let pcaliases = &s.c.g.pcaliases;
1175 let ifastsplits = &s.c.g.ifastsplits;
1176 let tokens_players: Vec<(&str, PlayerId)> = {
1177 let global_players = GLOBAL.players.read();
1178 s.c.g.tokens_players.tr
1181 global_players.get(token)
1182 .map(|player| (token.0.as_str(), player.ident)))
1185 let aplayers = s.c.g.iplayers.iter().map(
1186 |(player, PlayerRecord { ipl, .. })|
1187 (player, ipl.clone())
1189 let acl = s.c.g.acl.clone().into();
1190 let links = s.c.g.links.clone();
1191 let asset_url_key = s.c.g.asset_url_key.clone();
1192 let bundle_hashes = s.c.g.bundle_hashes.clone();
1193 let isa = InstanceSaveAuxiliary {
1194 ipieces, ioccults, tokens_players, aplayers, acl, links,
1195 pcaliases, asset_url_key, bundle_hashes, ifastsplits,
1197 rmp_serde::encode::write_named(w, &isa)
1199 self.c.aux_dirty = false;
1200 info!("saved aux for {}", &self.name);
1203 #[throws(InternalError)]
1204 fn load_something<T:DeserializeOwned>(name: &InstanceName, prefix: &str)
1206 let inp = savefilename(name, prefix, "");
1207 let f = fs::File::open(&inp).with_context(|| inp.clone())?;
1208 let mut f = BufReader::new(f);
1209 let thing = rmp_serde::decode::from_read(&mut f)?;
1210 debug!("loaded from {:?}", &inp);
1214 #[throws(StartupError)]
1215 fn load_game(accounts: &AccountsGuard,
1216 games: &mut GamesGuard,
1217 name: InstanceName) -> Option<InstanceRef> {
1218 let InstanceSaveAuxiliary::
1219 <String,ActualIPieces,IOccults,PieceAliases,IFastSplits> {
1220 tokens_players, mut ipieces, mut ioccults, mut aplayers, acl, links,
1221 pcaliases, asset_url_key, bundle_hashes, mut ifastsplits,
1222 } = match Self::load_something(&name, "a-") {
1225 let ae = match &e { InternalError::Anyhow(ae) => Some(ae), _=>None }?;
1226 let ioe = ae.downcast_ref::<io::Error>()?;
1227 let is_enoent = ioe.kind() == io::ErrorKind::NotFound;
1228 is_enoent.as_option()
1236 let mut gs: GameState = Self::load_something(&name, "g-")?;
1238 fn discard_mismatches<K:slotmap::Key, V1, V2>(
1239 primary: &mut DenseSlotMap<K, V1>,
1240 secondary: &mut SecondarySlotMap<K, V2>,
1242 primary.retain(|k,_v| secondary.contains_key(k));
1243 secondary.retain(|k,_v| primary.contains_key(k));
1246 for (piece, gpc) in &mut gs.pieces.0 {
1247 if_let!{ Some(fsid) = gpc.fastsplit; else continue; }
1249 // We must recover the ipc via the fastsplit table, or delete
1251 let ilks = &mut ioccults.ilks;
1252 if let Some(recovered_ipc) = ifastsplits.recover_ipc(ilks, fsid);
1255 if let Some(old_ipc) = ipieces.get_mut(piece);
1256 // We're about to overwrite this ipc, maybe owns some occilk.
1257 // If in fact we're the same kind, we've already acquired
1258 // another refcount from recover_ipc, above.
1259 if let Some(old_iilk) = old_ipc.occilk.take();
1260 then { ilks.dispose_iilk(old_iilk); }
1262 ipieces.insert(piece, recovered_ipc);
1264 // Not available in ipieces, fastsplit family must just have
1265 // been added. This will get rid of it from gpieces, too, below.
1266 ipieces.remove(piece);
1270 ifastsplits.cleanup(&mut ioccults.ilks);
1272 discard_mismatches(&mut gs.players, &mut aplayers);
1273 discard_mismatches(&mut gs.pieces.0, &mut ipieces);
1275 let pu_bc = PlayerUpdates::start(&gs);
1277 let iplayers: SecondarySlotMap<PlayerId, PlayerRecord> = {
1280 }.filter_map(|(player, ipl)| {
1281 let u = pu_bc.for_player();
1282 let account = accounts.lookup(ipl.acctid).ok()?.0.account.clone();
1283 Some((player, PlayerRecord { u, ipl, account }))
1286 for mut p in gs.pieces.values_mut() {
1287 p.lastclient = default();
1288 if let Some(held) = p.held {
1289 if !gs.players.contains_key(held) { p.held = None }
1293 let name = Arc::new(name);
1294 let (tokens_players, acctids_players) = {
1295 let mut tokens = Vec::with_capacity(tokens_players.len());
1296 let mut acctids = Vec::with_capacity(tokens_players.len());
1297 for (token, player) in tokens_players { if_chain! {
1298 if let Some(record) = iplayers.get(player);
1300 tokens.push((token, player));
1301 acctids.push(record.ipl.acctid);
1304 (tokens, accounts.bulk_check(&acctids))
1307 let mut g = Instance {
1308 gs, iplayers, links,
1310 ipieces: IPieces(ipieces),
1315 tokens_clients: default(),
1316 tokens_players: default(),
1317 bundle_list: default(), // set by load_game_bundles
1318 local_libs: default(), // set by load_game_bundles
1319 bundle_specs: default(), // set by load_game_bundles
1325 let b = InstanceBundles::reload_game_bundles(&mut g)?;
1326 let b = Mutex::new(b);
1328 let c = InstanceContainer {
1334 let c = Mutex::new(c);
1335 let gref = InstanceRef(Arc::new(InstanceOuter { c, b }));
1336 let mut g = gref.lock().unwrap();
1339 for (piece, ipc) in ig.ipieces.0.iter() {
1340 ipc.direct_trait_access()
1341 .save_reloaded_hook(piece, &mut ig.gs, &gref)?;
1344 for (token, _) in &tokens_players {
1345 g.tokens_players.tr.insert(RawToken(token.clone()));
1347 let mut global = GLOBAL.players.write();
1348 for ((token, player), acctid) in
1349 tokens_players.into_iter()
1350 .zip(acctids_players)
1352 if let Some(acctid) = acctid;
1353 let iad = InstanceAccessDetails {
1358 then { global.insert(RawToken(token), iad); }
1362 games.insert(name.clone(), gref.clone());
1363 info!("loadewd {:?}", &name);
1368 #[throws(anyhow::Error)]
1369 pub fn load_games(accounts: &mut AccountsGuard,
1370 games: &mut GamesGuard) {
1371 enum AFState { Found(PathBuf, SaveFileOrDir), Used }
1373 use SavefilenameParseResult::*;
1374 let mut a_leaves = HashMap::new();
1375 let save_dir = config().save_dir().clone();
1376 for de in fs::read_dir(&save_dir).context(save_dir)? {
1378 let leaf = de.file_name();
1380 let leaf = leaf.as_bytes();
1381 match savefilename_parse(leaf)? {
1385 fs::remove_file(de.path())
1386 .context("stale temporary file")?;
1389 a_leaves.entry(leaf.to_owned()).or_insert_with(
1390 || Found(de.path(), fd)
1393 GameFile { aux_leaves, name } => {
1394 InstanceGuard::load_game(accounts, games, name)?;
1395 for aux_leaf in aux_leaves {
1396 a_leaves.insert(aux_leaf, Used);
1400 <Result<_,anyhow::Error>>::Ok(())
1401 })().with_context(|| format!("leaf={:?}", leaf))?;
1404 for (leaf, state) in &a_leaves {
1405 if let Found(path, fd) = state {
1407 .with_context(|| format!("leaf={:?}", leaf))?;
1410 <Result<_,anyhow::Error>>::Ok(())
1411 })().context("cleaning up stale files")?;
1412 info!("loaded games");
1415 // ---------- Tokens / TokenTable / AccessId ----------
1417 pub type TokenTable<Id> = HashMap<RawToken, InstanceAccessDetails<Id>>;
1419 pub trait AccessId: Copy + Clone + 'static {
1420 type Error: Into<Fatal>;
1421 const ERROR: Self::Error;
1422 fn global_tokens(_:PrivateCaller) -> &'static RwLock<TokenTable<Self>>;
1423 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1424 -> &mut TokenRegistry<Self>;
1427 #[derive(Debug,Copy,Clone,Eq,PartialEq,Ord,PartialOrd)]
1428 #[derive(Serialize,Deserialize)]
1430 #[error("Player not found")]
1431 pub struct PlayerNotFound;
1433 impl AccessId for PlayerId {
1434 type Error = PlayerNotFound;
1435 const ERROR: PlayerNotFound = PlayerNotFound;
1436 fn global_tokens(_: PrivateCaller) -> &'static RwLock<TokenTable<Self>> {
1439 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1440 -> &mut TokenRegistry<Self> {
1441 &mut ig.tokens_players
1444 impl AccessId for ClientId {
1446 const ERROR: Fatal = NoClient;
1447 fn global_tokens(_: PrivateCaller) -> &'static RwLock<TokenTable<Self>> {
1450 fn tokens_registry(ig: &mut Instance, _:PrivateCaller)
1451 -> &mut TokenRegistry<Self> {
1452 &mut ig.tokens_clients
1457 fn new_random() -> Self {
1458 let mut rng = thread_rng();
1459 let token = RawToken (
1460 repeat_with(|| rng.sample(Alphanumeric))
1468 pub fn lookup_token<Id:AccessId>(s: &RawTokenVal)
1469 -> Result<InstanceAccessDetails<Id>, Id::Error> {
1470 Id::global_tokens(PRIVATE_Y).read().get(s).cloned()
1475 pub fn record_token<Id:AccessId> (
1476 ig: &mut InstanceGuard,
1477 iad: InstanceAccessDetails<Id>
1479 let token = RawToken::new_random();
1480 ig.token_register(token.clone(), iad);
1485 pub fn process_all_players_for_account<
1487 F: FnMut(&mut InstanceGuard<'_>, PlayerId) -> Result<(),E>
1489 (games: &mut GamesGuard, acctid: AccountId, mut f: F)
1491 for gref in games.values() {
1492 let c = gref.lock_even_destroying();
1493 let remove: Vec<_> = c.g.iplayers.iter().filter_map(|(player,pr)| {
1494 if pr.ipl.acctid == acctid { Some(player) } else { None }
1496 let mut ig = InstanceGuard { gref: gref.clone(), c };
1497 for player in remove.into_iter() {
1498 f(&mut ig, player)?;
1503 // ========== instance pieces data access ==========
1506 pub fn get(&self, piece: PieceId) -> Option<&IPiece> {
1510 pub fn as_mut(&mut self, _: ModifyingPieces) -> &mut ActualIPieces {
1514 pub fn is_empty(&self) -> bool {
1515 let IPieces(actual) = self;
1520 // ---------- gamestate pieces table ----------
1523 pub fn get_mut(&mut self, piece: PieceId) -> Option<&mut GPiece> {
1524 self.0.get_mut(piece)
1526 pub fn values_mut(&mut self) -> sm::ValuesMut<PieceId, GPiece> {
1529 pub fn as_mut(&mut self, _: ModifyingPieces) -> &mut ActualGPieces {
1533 pub fn as_mut_t(&mut self) -> &mut ActualGPieces { &mut self.0 }
1536 impl ById for GPieces {
1538 type Entry = GPiece;
1539 type Error = Inapplicable;
1541 fn byid(&self, piece: PieceId) -> &GPiece {
1542 self.get(piece).ok_or(Ia::PieceGone)?
1545 fn byid_mut(&mut self, piece: PieceId) -> &mut GPiece {
1546 self.get_mut(piece).ok_or(Ia::PieceGone)?
1550 /*impl<'p> IntoIterator for &'p Pieces {
1551 type Item = (PieceId, &'p PieceState);
1552 type IntoIter = sm::Iter<'p, PieceId, PieceState>;
1553 fn into_iter(self) -> Self::IntoIter { (&self.0).into_iter() }
1555 impl<'p> IntoIterator for &'p mut GPieces {
1556 type Item = (PieceId, &'p mut GPiece);
1557 type IntoIter = sm::IterMut<'p, PieceId, GPiece>;
1558 fn into_iter(self) -> Self::IntoIter { (&mut self.0).into_iter() }
1561 // ========== background maintenance ==========
1563 // ---------- delayed game save ----------
1565 impl InstanceGuard<'_> {
1566 pub fn save_game_later(&mut self) {
1567 if self.c.game_dirty { return }
1568 GLOBAL.dirty.lock().push_back(self.gref.clone());
1569 self.c.game_dirty = true;
1572 pub fn save_game_and_aux_later(&mut self) {
1573 if self.c.aux_dirty { return }
1574 self.save_game_later();
1575 self.c.aux_dirty = true;
1579 pub fn game_flush_task() {
1580 let mut inner_queue = VecDeque::new();
1583 mem::swap(&mut inner_queue, &mut *GLOBAL.dirty.lock());
1585 thread::sleep(GAME_SAVE_LAG);
1586 for _ in 0..inner_queue.len() {
1587 let ent = inner_queue.pop_front().unwrap();
1588 let mut ig = match ent.lock() { Ok(ig) => ig, _ => continue/*ah well*/ };
1589 if !ig.c.game_dirty { continue }
1590 match ig.save_game_now() {
1592 assert!(!ig.c.game_dirty);
1595 // todo: notify the players
1596 error!("save error! name={:?}: {}", &ig.name, &e);
1598 inner_queue.push_back(ent); // oof
1605 // ---------- client expiry ----------
1607 fn client_expire_old_clients() {
1608 let mut expire = vec![];
1609 let max_age = Instant::now() - MAX_CLIENT_INACTIVITY;
1611 trait ClientIterator {
1613 fn iter<'g>(&mut self, gref: &'g InstanceRef, max_age: Instant)
1614 -> (MutexGuard<'g, InstanceContainer>, Option<Self::Ret>) {
1615 let c = gref.lock_even_destroying();
1616 let ret = 'ret: loop {
1617 for (client, cl) in &c.g.clients {
1618 if cl.lastseen > max_age { continue }
1619 let ret = self.old(client);
1620 if ret.is_some() { break 'ret ret }
1626 fn old(&mut self, client: ClientId) -> Option<Self::Ret>;
1629 for gref in GLOBAL.games_table.read().values() {
1631 impl ClientIterator for Any {
1633 fn old(&mut self, _client: ClientId) -> Option<()> {
1637 if let (_, Some(())) = Any.iter(gref, max_age) {
1638 expire.push(gref.clone());
1641 for gref in expire.into_iter() {
1643 struct Now(HashSet<ClientId>);
1644 impl ClientIterator for Now {
1646 fn old(&mut self, client: ClientId) -> Option<Void> {
1647 self.0.insert(client);
1652 let mut now = Now(default());
1653 let (mut c, _) = now.iter(&gref, max_age);
1654 c.g.clients.retain(|c,_| !now.0.contains(&c));
1655 let mut gref = InstanceGuard { c, gref: gref.clone() };
1656 debug!("expiring client {:?}", &now);
1657 gref.tokens_deregister_for_id::<ClientId,_>(|c| now.0.contains(&c));
1661 pub fn client_periodic_expiry() {
1663 sleep(MAX_CLIENT_INACTIVITY);
1664 client_expire_old_clients();
1668 // ---------- log expiry ----------
1670 fn global_expire_old_logs() {
1671 let cutoff = Timestamp(Timestamp::now().0 - MAX_LOG_AGE.as_secs());
1673 let mut want_expire = vec![];
1675 let read = GLOBAL.games_table.read();
1676 for gref in read.values() {
1677 if gref.lock_even_destroying().g.gs.want_expire_some_logs(cutoff) {
1678 want_expire.push(gref.clone())
1683 for gref in want_expire.into_iter() {
1684 let mut g = gref.lock_even_destroying();
1685 info!("expiring old log entries in {:?}", &g.g.name);
1686 g.g.gs.do_expire_old_logs(cutoff);
1690 pub fn logs_periodic_expiry() {
1692 sleep(MAX_LOG_AGE / 10);
1693 global_expire_old_logs();