1use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use chrono::{DateTime, Utc};
19use fs2::FileExt;
20use serde::{Deserialize, Serialize};
21use snafu::{IntoError, OptionExt, ResultExt};
22use tokio::io::AsyncWriteExt;
23
24use crate::data::import_v2::error::{
25 ImportStateIoSnafu, ImportStateLockedSnafu, ImportStateParseSnafu, ImportStateUnknownTaskSnafu,
26 Result,
27};
28use crate::data::path::encode_path_segment;
29
30const IMPORT_STATE_ROOT: &str = ".greptime";
31const IMPORT_STATE_DIR: &str = "import_state";
32static IMPORT_STATE_TMP_ID: AtomicU64 = AtomicU64::new(0);
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub(crate) enum ImportTaskStatus {
37 Pending,
38 InProgress,
39 Completed,
40 Failed,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub(crate) struct ImportTaskKey {
45 pub(crate) chunk_id: u32,
46 pub(crate) schema: String,
47}
48
49impl ImportTaskKey {
50 pub(crate) fn new(chunk_id: u32, schema: impl Into<String>) -> Self {
51 Self {
52 chunk_id,
53 schema: schema.into(),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub(crate) struct ImportTaskState {
60 pub(crate) chunk_id: u32,
61 pub(crate) schema: String,
62 pub(crate) status: ImportTaskStatus,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub(crate) error: Option<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub(crate) struct ImportState {
69 pub(crate) snapshot_id: String,
70 pub(crate) target_addr: String,
71 pub(crate) catalog: String,
72 pub(crate) schemas: Vec<String>,
73 #[serde(default)]
74 pub(crate) ddl_completed: bool,
75 pub(crate) updated_at: DateTime<Utc>,
76 pub(crate) tasks: Vec<ImportTaskState>,
81}
82
83impl ImportState {
84 pub(crate) fn new<I>(
85 snapshot_id: impl Into<String>,
86 target_addr: impl Into<String>,
87 catalog: impl Into<String>,
88 schemas: &[String],
89 tasks: I,
90 ) -> Self
91 where
92 I: IntoIterator<Item = ImportTaskKey>,
93 {
94 Self {
95 snapshot_id: snapshot_id.into(),
96 target_addr: target_addr.into(),
97 catalog: catalog.into(),
98 schemas: canonical_schema_selection(schemas),
99 ddl_completed: false,
100 updated_at: Utc::now(),
101 tasks: tasks
102 .into_iter()
103 .map(|task| ImportTaskState {
104 chunk_id: task.chunk_id,
105 schema: task.schema,
106 status: ImportTaskStatus::Pending,
107 error: None,
108 })
109 .collect(),
110 }
111 }
112
113 pub(crate) fn mark_ddl_completed(&mut self) {
114 self.ddl_completed = true;
115 self.updated_at = Utc::now();
116 }
117
118 pub(crate) fn task_status(&self, chunk_id: u32, schema: &str) -> Option<ImportTaskStatus> {
119 self.tasks
120 .iter()
121 .find(|task| task.chunk_id == chunk_id && task.schema == schema)
122 .map(|task| task.status)
123 }
124
125 pub(crate) fn set_task_status(
126 &mut self,
127 chunk_id: u32,
128 schema: &str,
129 status: ImportTaskStatus,
130 error: Option<String>,
131 ) -> Result<()> {
132 let task = self
133 .tasks
134 .iter_mut()
135 .find(|task| task.chunk_id == chunk_id && task.schema == schema)
136 .context(ImportStateUnknownTaskSnafu {
137 chunk_id,
138 schema: schema.to_string(),
139 })?;
140 task.status = status;
141 task.error = error;
142 self.updated_at = Utc::now();
143 Ok(())
144 }
145}
146
147#[derive(Debug)]
148pub(crate) struct ImportStateLockGuard {
149 file: std::fs::File,
150}
151
152impl Drop for ImportStateLockGuard {
153 fn drop(&mut self) {
154 let _ = self.file.unlock();
155 }
156}
157
158pub(crate) fn default_state_path(
159 snapshot_id: &str,
160 target_addr: &str,
161 catalog: &str,
162 schemas: &[String],
163) -> Option<PathBuf> {
164 let home = default_home_dir_with(|key| std::env::var_os(key));
165 let cwd = std::env::current_dir().ok();
166 default_state_path_with(
167 home.as_deref(),
168 cwd.as_deref(),
169 snapshot_id,
170 target_addr,
171 catalog,
172 schemas,
173 )
174}
175
176fn default_home_dir_with<F>(get: F) -> Option<PathBuf>
177where
178 F: Fn(&str) -> Option<std::ffi::OsString>,
179{
180 get("HOME")
181 .or_else(|| get("USERPROFILE"))
182 .map(PathBuf::from)
183 .or_else(|| {
184 let drive = get("HOMEDRIVE")?;
185 let path = get("HOMEPATH")?;
186 Some(PathBuf::from(drive).join(path))
187 })
188}
189
190fn default_state_path_with(
191 home: Option<&Path>,
192 cwd: Option<&Path>,
193 snapshot_id: &str,
194 target_addr: &str,
195 catalog: &str,
196 schemas: &[String],
197) -> Option<PathBuf> {
198 let file_name = import_state_file_name(snapshot_id, target_addr, catalog, schemas);
199 match (home, cwd) {
200 (Some(home), _) => Some(
201 home.join(IMPORT_STATE_ROOT)
202 .join(IMPORT_STATE_DIR)
203 .join(file_name),
204 ),
205 (None, Some(cwd)) => Some(cwd.join(file_name)),
206 (None, None) => None,
207 }
208}
209
210fn import_state_file_name(
211 snapshot_id: &str,
212 target_addr: &str,
213 catalog: &str,
214 schemas: &[String],
215) -> String {
216 format!(
217 ".import_state_{}_{}_{}.json",
218 encode_path_segment(snapshot_id),
219 encode_path_segment(target_addr),
220 import_identity_hash(catalog, schemas)
221 )
222}
223
224pub(crate) fn canonical_schema_selection(schemas: &[String]) -> Vec<String> {
225 let mut canonicalized = schemas
226 .iter()
227 .map(|schema| schema.to_ascii_lowercase())
228 .collect::<Vec<_>>();
229 canonicalized.sort();
230 canonicalized.dedup();
231 canonicalized
232}
233
234fn import_identity_hash(catalog: &str, schemas: &[String]) -> String {
239 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
240 const FNV_PRIME: u64 = 0x100000001b3;
241
242 fn hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
243 for byte in bytes {
244 hash ^= u64::from(*byte);
245 hash = hash.wrapping_mul(FNV_PRIME);
246 }
247 hash
248 }
249
250 let mut hash = FNV_OFFSET;
251 hash = hash_bytes(hash, catalog.as_bytes());
252 hash = hash_bytes(hash, &[0xff]);
255 for schema in canonical_schema_selection(schemas) {
256 hash = hash_bytes(hash, schema.as_bytes());
257 hash = hash_bytes(hash, &[0xff]);
258 }
259 format!("{hash:016x}")
260}
261
262pub(crate) async fn load_import_state(path: &Path) -> Result<Option<ImportState>> {
263 match tokio::fs::read(path).await {
264 Ok(bytes) => {
265 let mut state: ImportState =
266 serde_json::from_slice(&bytes).context(ImportStateParseSnafu)?;
267 normalize_import_state_for_resume(&mut state);
268 Ok(Some(state))
269 }
270 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
271 Err(source) => Err(source).context(ImportStateIoSnafu {
272 path: path.display().to_string(),
273 }),
274 }
275}
276
277pub(crate) async fn save_import_state(path: &Path, state: &ImportState) -> Result<()> {
279 if let Some(parent) = path.parent() {
280 tokio::fs::create_dir_all(parent)
281 .await
282 .context(ImportStateIoSnafu {
283 path: parent.display().to_string(),
284 })?;
285 }
286
287 let bytes =
288 serde_json::to_vec_pretty(state).expect("ImportState should always be serializable");
289 let tmp_path = unique_tmp_path(path);
290 let mut file = tokio::fs::File::create(&tmp_path)
291 .await
292 .context(ImportStateIoSnafu {
293 path: tmp_path.display().to_string(),
294 })?;
295 file.write_all(&bytes).await.context(ImportStateIoSnafu {
296 path: tmp_path.display().to_string(),
297 })?;
298 file.sync_all().await.context(ImportStateIoSnafu {
299 path: tmp_path.display().to_string(),
300 })?;
301 drop(file);
303
304 tokio::fs::rename(&tmp_path, path)
305 .await
306 .context(ImportStateIoSnafu {
307 path: path.display().to_string(),
308 })?;
309 sync_parent_dir(path).await?;
310 Ok(())
311}
312
313pub(crate) fn try_acquire_import_state_lock(path: &Path) -> Result<ImportStateLockGuard> {
314 if let Some(parent) = path.parent() {
315 std::fs::create_dir_all(parent).context(ImportStateIoSnafu {
316 path: parent.display().to_string(),
317 })?;
318 }
319
320 let lock_path = import_state_lock_path(path);
321 let file = std::fs::OpenOptions::new()
322 .create(true)
323 .read(true)
324 .write(true)
325 .truncate(false)
326 .open(&lock_path)
327 .context(ImportStateIoSnafu {
328 path: lock_path.display().to_string(),
329 })?;
330 file.try_lock_exclusive().map_err(|error| {
331 if is_lock_contention(&error) {
332 ImportStateLockedSnafu {
333 path: lock_path.display().to_string(),
334 }
335 .build()
336 } else {
337 ImportStateIoSnafu {
338 path: lock_path.display().to_string(),
339 }
340 .into_error(error)
341 }
342 })?;
343
344 Ok(ImportStateLockGuard { file })
345}
346
347fn is_lock_contention(error: &std::io::Error) -> bool {
348 error.kind() == std::io::ErrorKind::WouldBlock
349 || error.raw_os_error() == fs2::lock_contended_error().raw_os_error()
350}
351
352fn unique_tmp_path(path: &Path) -> PathBuf {
353 let pid = std::process::id();
354 let seq = IMPORT_STATE_TMP_ID.fetch_add(1, Ordering::Relaxed);
355 let file_name = path.file_name().unwrap_or_default().to_string_lossy();
356 path.with_file_name(format!("{file_name}.{pid}.{seq}.tmp"))
357}
358
359fn import_state_lock_path(path: &Path) -> PathBuf {
360 let file_name = path.file_name().unwrap_or_default().to_string_lossy();
361 path.with_file_name(format!("{file_name}.lock"))
362}
363
364fn normalize_import_state_for_resume(state: &mut ImportState) {
365 for task in &mut state.tasks {
366 if task.status == ImportTaskStatus::InProgress {
367 task.status = ImportTaskStatus::Pending;
368 task.error = None;
369 }
370 }
371}
372
373pub(crate) async fn delete_import_state(path: &Path) -> Result<()> {
374 match tokio::fs::remove_file(path).await {
375 Ok(()) => {
376 sync_parent_dir(path).await?;
377 Ok(())
378 }
379 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
380 Err(source) => Err(source).context(ImportStateIoSnafu {
381 path: path.display().to_string(),
382 }),
383 }
384}
385
386#[cfg(unix)]
387async fn sync_parent_dir(path: &Path) -> Result<()> {
388 let Some(parent) = path.parent() else {
389 return Ok(());
390 };
391
392 let dir = tokio::fs::File::open(parent)
393 .await
394 .context(ImportStateIoSnafu {
395 path: parent.display().to_string(),
396 })?;
397 dir.sync_all().await.context(ImportStateIoSnafu {
398 path: parent.display().to_string(),
399 })?;
400 Ok(())
401}
402
403#[cfg(not(unix))]
404async fn sync_parent_dir(_path: &Path) -> Result<()> {
405 Ok(())
406}
407
408#[cfg(test)]
409mod tests {
410 use std::process::Command;
411
412 use chrono::Utc;
413 use tempfile::tempdir;
414
415 use super::*;
416
417 const CHILD_LOCK_PATH_ENV: &str = "GREPTIME_IMPORT_STATE_LOCK_PATH";
418 const CHILD_LOCK_TEST: &str =
419 "data::import_v2::state::tests::test_try_acquire_import_state_lock_child_process";
420
421 fn schemas() -> Vec<String> {
422 vec!["public".to_string(), "analytics".to_string()]
423 }
424
425 fn tasks() -> Vec<ImportTaskKey> {
426 vec![
427 ImportTaskKey::new(1, "public"),
428 ImportTaskKey::new(2, "analytics"),
429 ]
430 }
431
432 #[test]
433 fn test_import_state_new_initializes_pending_tasks() {
434 let state = ImportState::new(
435 "snapshot-1",
436 "127.0.0.1:4000",
437 "greptime",
438 &schemas(),
439 tasks(),
440 );
441
442 assert_eq!(state.snapshot_id, "snapshot-1");
443 assert_eq!(state.target_addr, "127.0.0.1:4000");
444 assert_eq!(state.catalog, "greptime");
445 assert_eq!(state.schemas, vec!["analytics", "public"]);
446 assert_eq!(state.tasks.len(), 2);
447 assert_eq!(state.tasks[0].status, ImportTaskStatus::Pending);
448 assert_eq!(state.tasks[1].status, ImportTaskStatus::Pending);
449 }
450
451 #[test]
452 fn test_set_task_status_updates_timestamp_and_error() {
453 let mut state = ImportState::new(
454 "snapshot-1",
455 "127.0.0.1:4000",
456 "greptime",
457 &schemas(),
458 tasks(),
459 );
460 let before = state.updated_at;
461 state.updated_at = Utc::now() - chrono::Duration::seconds(10);
462
463 state
464 .set_task_status(
465 1,
466 "public",
467 ImportTaskStatus::Failed,
468 Some("timeout".to_string()),
469 )
470 .unwrap();
471 assert_eq!(
472 state.task_status(1, "public"),
473 Some(ImportTaskStatus::Failed)
474 );
475 assert_eq!(state.tasks[0].error.as_deref(), Some("timeout"));
476 assert!(state.updated_at > before);
477 }
478
479 #[test]
480 fn test_set_task_status_rejects_unknown_task() {
481 let mut state = ImportState::new(
482 "snapshot-1",
483 "127.0.0.1:4000",
484 "greptime",
485 &schemas(),
486 tasks(),
487 );
488
489 let error = state
490 .set_task_status(99, "public", ImportTaskStatus::Completed, None)
491 .unwrap_err();
492
493 assert!(matches!(
494 error,
495 crate::data::import_v2::error::Error::ImportStateUnknownTask { chunk_id, schema, .. }
496 if chunk_id == 99 && schema == "public"
497 ));
498 }
499
500 #[tokio::test]
501 async fn test_save_and_load_import_state_round_trip() {
502 let dir = tempdir().unwrap();
503 let path = dir.path().join("import_state.json");
504 let mut state = ImportState::new(
505 "snapshot-1",
506 "127.0.0.1:4000",
507 "greptime",
508 &schemas(),
509 tasks(),
510 );
511 state
512 .set_task_status(2, "analytics", ImportTaskStatus::Completed, None)
513 .unwrap();
514
515 save_import_state(&path, &state).await.unwrap();
516 let loaded = load_import_state(&path).await.unwrap().unwrap();
517
518 assert_eq!(loaded.snapshot_id, state.snapshot_id);
519 assert_eq!(loaded.target_addr, state.target_addr);
520 assert_eq!(loaded.catalog, state.catalog);
521 assert_eq!(loaded.schemas, state.schemas);
522 assert_eq!(loaded.tasks, state.tasks);
523 }
524
525 #[tokio::test]
526 async fn test_save_import_state_overwrites_existing_file() {
527 let dir = tempdir().unwrap();
528 let path = dir.path().join("import_state.json");
529 let mut state = ImportState::new(
530 "snapshot-1",
531 "127.0.0.1:4000",
532 "greptime",
533 &schemas(),
534 tasks(),
535 );
536 save_import_state(&path, &state).await.unwrap();
537
538 state
539 .set_task_status(1, "public", ImportTaskStatus::Completed, None)
540 .unwrap();
541 save_import_state(&path, &state).await.unwrap();
542
543 let loaded = load_import_state(&path).await.unwrap().unwrap();
544 assert_eq!(
545 loaded.task_status(1, "public"),
546 Some(ImportTaskStatus::Completed)
547 );
548 }
549
550 #[test]
551 fn test_load_import_state_resets_in_progress_to_pending() {
552 let mut state = ImportState::new(
553 "snapshot-1",
554 "127.0.0.1:4000",
555 "greptime",
556 &schemas(),
557 tasks(),
558 );
559 state
560 .set_task_status(
561 2,
562 "analytics",
563 ImportTaskStatus::InProgress,
564 Some("running".to_string()),
565 )
566 .unwrap();
567
568 normalize_import_state_for_resume(&mut state);
569
570 assert_eq!(
571 state.task_status(1, "public"),
572 Some(ImportTaskStatus::Pending)
573 );
574 assert_eq!(
575 state.task_status(2, "analytics"),
576 Some(ImportTaskStatus::Pending)
577 );
578 assert_eq!(state.tasks[1].error, None);
579 }
580
581 #[test]
582 fn test_unique_tmp_path_generates_distinct_paths() {
583 let dir = tempdir().unwrap();
584 let path = dir.path().join("import_state.json");
585
586 let first = unique_tmp_path(&path);
587 let second = unique_tmp_path(&path);
588
589 assert_ne!(first, second);
590 assert!(first.starts_with(dir.path()));
591 assert!(second.starts_with(dir.path()));
592 assert!(
593 first
594 .file_name()
595 .unwrap()
596 .to_string_lossy()
597 .ends_with(".tmp")
598 );
599 assert!(
600 second
601 .file_name()
602 .unwrap()
603 .to_string_lossy()
604 .ends_with(".tmp")
605 );
606 }
607
608 #[test]
609 fn test_lock_contention_detection_accepts_platform_error() {
610 let error = fs2::lock_contended_error();
611
612 assert!(is_lock_contention(&error));
613 }
614
615 #[test]
616 fn test_try_acquire_import_state_lock_rejects_second_holder() {
617 let dir = tempdir().unwrap();
618 let path = dir.path().join("import_state.json");
619
620 let _first = try_acquire_import_state_lock(&path).unwrap();
621 let output = Command::new(std::env::current_exe().unwrap())
623 .arg(CHILD_LOCK_TEST)
624 .arg("--ignored")
625 .arg("--exact")
626 .env(CHILD_LOCK_PATH_ENV, &path)
627 .output()
628 .unwrap();
629
630 assert!(
631 output.status.success(),
632 "child lock test failed\nstdout:\n{}\nstderr:\n{}",
633 String::from_utf8_lossy(&output.stdout),
634 String::from_utf8_lossy(&output.stderr)
635 );
636 let stdout = String::from_utf8_lossy(&output.stdout);
637 assert!(
638 stdout.contains("1 passed"),
639 "child lock test did not run the expected ignored test\nstdout:\n{stdout}"
640 );
641 }
642
643 #[test]
644 #[ignore = "spawned by test_try_acquire_import_state_lock_rejects_second_holder"]
645 fn test_try_acquire_import_state_lock_child_process() {
646 let path = std::env::var_os(CHILD_LOCK_PATH_ENV)
647 .expect("child lock path must be set by the parent test");
648 let path = PathBuf::from(path);
649 let error = try_acquire_import_state_lock(&path).unwrap_err();
650
651 assert!(matches!(
652 error,
653 crate::data::import_v2::error::Error::ImportStateLocked { .. }
654 ));
655 }
656
657 #[tokio::test]
658 async fn test_delete_import_state_ignores_missing_file() {
659 let dir = tempdir().unwrap();
660 let path = dir.path().join("missing.json");
661
662 delete_import_state(&path).await.unwrap();
663 }
664
665 #[test]
666 fn test_default_state_path_prefers_home_and_encodes_snapshot_id() {
667 let home = tempdir().unwrap();
668 let cwd = tempdir().unwrap();
669
670 let path = default_state_path_with(
671 Some(home.path()),
672 Some(cwd.path()),
673 "../snapshot",
674 "127.0.0.1:4000",
675 "greptime",
676 &schemas(),
677 )
678 .unwrap();
679
680 assert_eq!(
681 path.parent().unwrap(),
682 home.path().join(IMPORT_STATE_ROOT).join(IMPORT_STATE_DIR)
683 );
684 let file_name = path.file_name().unwrap().to_string_lossy();
685 assert!(file_name.starts_with(".import_state_%2E%2E%2Fsnapshot_127%2E0%2E0%2E1%3A4000_"));
686 assert!(file_name.ends_with(".json"));
687 }
688
689 #[test]
690 fn test_default_state_path_falls_back_to_cwd_when_home_missing() {
691 let cwd = tempdir().unwrap();
692
693 let path = default_state_path_with(
694 None,
695 Some(cwd.path()),
696 "snapshot-1",
697 "target-a",
698 "greptime",
699 &schemas(),
700 )
701 .unwrap();
702
703 assert_eq!(path.parent().unwrap(), cwd.path());
704 let file_name = path.file_name().unwrap().to_string_lossy();
705 assert!(file_name.starts_with(".import_state_snapshot-1_target-a_"));
706 assert!(file_name.ends_with(".json"));
707 }
708
709 #[test]
710 fn test_default_state_path_isolated_by_target_addr() {
711 let cwd = tempdir().unwrap();
712
713 let first = default_state_path_with(
714 None,
715 Some(cwd.path()),
716 "snapshot-1",
717 "127.0.0.1:4000",
718 "greptime",
719 &schemas(),
720 )
721 .unwrap();
722 let second = default_state_path_with(
723 None,
724 Some(cwd.path()),
725 "snapshot-1",
726 "127.0.0.1:4001",
727 "greptime",
728 &schemas(),
729 )
730 .unwrap();
731
732 assert_ne!(first, second);
733 }
734
735 #[test]
736 fn test_default_state_path_isolated_by_catalog_and_schemas() {
737 let cwd = tempdir().unwrap();
738 let public_only = vec!["public".to_string()];
739 let analytics_only = vec!["analytics".to_string()];
740
741 let first = default_state_path_with(
742 None,
743 Some(cwd.path()),
744 "snapshot-1",
745 "127.0.0.1:4000",
746 "greptime",
747 &public_only,
748 )
749 .unwrap();
750 let second = default_state_path_with(
751 None,
752 Some(cwd.path()),
753 "snapshot-1",
754 "127.0.0.1:4000",
755 "other",
756 &public_only,
757 )
758 .unwrap();
759 let third = default_state_path_with(
760 None,
761 Some(cwd.path()),
762 "snapshot-1",
763 "127.0.0.1:4000",
764 "greptime",
765 &analytics_only,
766 )
767 .unwrap();
768
769 assert_ne!(first, second);
770 assert_ne!(first, third);
771 }
772
773 #[test]
774 fn test_default_home_dir_prefers_home() {
775 let detected = default_home_dir_with(|key| match key {
776 "HOME" => Some(std::ffi::OsString::from("/tmp/home")),
777 "USERPROFILE" => Some(std::ffi::OsString::from("/tmp/userprofile")),
778 _ => None,
779 });
780
781 assert_eq!(detected, Some(PathBuf::from("/tmp/home")));
782 }
783
784 #[test]
785 fn test_default_home_dir_falls_back_to_userprofile() {
786 let detected = default_home_dir_with(|key| match key {
787 "USERPROFILE" => Some(std::ffi::OsString::from("/tmp/userprofile")),
788 _ => None,
789 });
790
791 assert_eq!(detected, Some(PathBuf::from("/tmp/userprofile")));
792 }
793
794 #[test]
795 fn test_default_home_dir_falls_back_to_home_drive_and_path() {
796 let detected = default_home_dir_with(|key| match key {
797 "HOMEDRIVE" => Some(std::ffi::OsString::from("/tmp")),
798 "HOMEPATH" => Some(std::ffi::OsString::from("windows-home")),
799 _ => None,
800 });
801
802 assert_eq!(detected, Some(PathBuf::from("/tmp").join("windows-home")));
803 }
804}