def test_unconfigured_mounts_dont_crash(self): # If Eden advertises that a mount is active, but it is not in the # configuration, then at least don't throw an exception. tmp_dir = tempfile.mkdtemp(prefix='eden_test.') try: edenfs_path1 = os.path.join(tmp_dir, 'path1') edenfs_path2 = os.path.join(tmp_dir, 'path2') mount_paths = OrderedDict() mount_paths[edenfs_path1] = { 'bind-mounts': {}, 'mount': edenfs_path1, 'scm_type': 'hg', 'snapshot': 'abcd' * 10, 'client-dir': '/I_DO_NOT_EXIST1' } # path2 is not configured in the config... config = FakeConfig(mount_paths, is_healthy=True) # ... but is advertised by the daemon... config.get_thrift_client()._mounts = [ eden_ttypes.MountInfo(mountPoint=edenfs_path1), eden_ttypes.MountInfo(mountPoint=edenfs_path2), ] # ... and is in the system mount table. mount_table = FakeMountTable() mount_table.stats[edenfs_path1] = mtab.MTStat(st_uid=os.getuid(), st_dev=11) mount_table.stats[edenfs_path2] = mtab.MTStat(st_uid=os.getuid(), st_dev=12) os.mkdir(edenfs_path1) hg_dir = os.path.join(edenfs_path1, '.hg') os.mkdir(hg_dir) dirstate = os.path.join(hg_dir, 'dirstate') dirstate_hash = b'\xab\xcd' * 10 parents = (dirstate_hash, b'\x00' * 20) with open(dirstate, 'wb') as f: eden.dirstate.write(f, parents, tuples_dict={}, copymap={}) dry_run = False out = io.StringIO() exit_code = doctor.cure_what_ails_you(config, dry_run, out, mount_table) finally: shutil.rmtree(tmp_dir) self.assertEqual( f'''\ Performing 3 checks for {edenfs_path1}. All is well. ''', out.getvalue()) self.assertEqual(0, exit_code)
def test_not_all_mounts_have_watchman_watcher(self, mock_watchman): edenfs_path = '/path/to/eden-mount' edenfs_path_not_watched = '/path/to/eden-mount-not-watched' side_effects: List[Dict[str, Any]] = [] calls = [] calls.append(call(['watch-list'])) side_effects.append({'roots': [edenfs_path]}) calls.append(call(['watch-project', edenfs_path])) side_effects.append({'watcher': 'eden'}) calls.append(call(['debug-get-subscriptions', edenfs_path])) side_effects.append({}) mock_watchman.side_effect = side_effects out = io.StringIO() dry_run = False mount_paths = OrderedDict() mount_paths[edenfs_path] = { 'bind-mounts': {}, 'mount': edenfs_path, 'scm_type': 'git', 'snapshot': 'abcd' * 10, 'client-dir': '/I_DO_NOT_EXIST' } mount_paths[edenfs_path_not_watched] = { 'bind-mounts': {}, 'mount': edenfs_path_not_watched, 'scm_type': 'git', 'snapshot': 'abcd' * 10, 'client-dir': '/I_DO_NOT_EXIST' } config = FakeConfig(mount_paths, is_healthy=True) config.get_thrift_client()._mounts = [ eden_ttypes.MountInfo(mountPoint=edenfs_path), eden_ttypes.MountInfo(mountPoint=edenfs_path_not_watched), ] mount_table = FakeMountTable() mount_table.stats['/path/to/eden-mount'] = mtab.MTStat( st_uid=os.getuid(), st_dev=10) mount_table.stats['/path/to/eden-mount-not-watched'] = mtab.MTStat( st_uid=os.getuid(), st_dev=11) exit_code = doctor.cure_what_ails_you(config, dry_run, out, mount_table=mount_table) self.assertEqual( 'Performing 2 checks for /path/to/eden-mount.\n' 'Performing 2 checks for /path/to/eden-mount-not-watched.\n' 'All is well.\n', out.getvalue()) mock_watchman.assert_has_calls(calls) self.assertEqual(0, exit_code)
def clone(self, repo_name, path, snapshot_id): if path in self._get_directory_map(): raise Exception('mount path %s already exists.' % path) # Make sure that path is a valid destination for the clone. st = None try: st = os.stat(path) except OSError as ex: if ex.errno == errno.ENOENT: # Note that this could also throw if path is /a/b/c and /a # exists, but it is a file. util.mkdir_p(path) else: raise # Note that st will be None if `mkdir_p` was run in the catch block. if st: if stat.S_ISDIR(st.st_mode): # If an existing directory was specified, then verify it is # empty. if len(os.listdir(path)) > 0: raise OSError(errno.ENOTEMPTY, os.strerror(errno.ENOTEMPTY), path) else: # Throw because it exists, but it is not a directory. raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), path) # Create client directory dir_name = hashlib.sha1(path.encode('utf-8')).hexdigest() client_dir = os.path.join(self._get_clients_dir(), dir_name) util.mkdir_p(client_dir) # Store repository name in local edenrc config file self._store_repo_name(client_dir, repo_name) # Store snapshot ID if snapshot_id: client_snapshot = os.path.join(client_dir, SNAPSHOT) with open(client_snapshot, 'w') as f: f.write(snapshot_id + '\n') else: raise Exception('snapshot id not provided') # Create bind mounts directories repo_data = self.get_repo_data(repo_name) bind_mounts_dir = os.path.join(client_dir, 'bind-mounts') util.mkdir_p(bind_mounts_dir) for mount in repo_data['bind-mounts']: util.mkdir_p(os.path.join(bind_mounts_dir, mount)) # Prepare to mount mount_info = eden_ttypes.MountInfo(mountPoint=path, edenClientPath=client_dir) with self.get_thrift_client() as client: client.mount(mount_info) # Add mapping of mount path to client directory in config.json self._add_path_to_directory_map(path, dir_name)
def mount(self, path: str) -> int: # Load the config info for this client, to make sure we # know about the client. path = os.path.realpath(path) client_dir = self._get_client_dir_for_mount_point(path) # Call _get_client_config() for the side-effect of it raising an # Exception if the config is in an invalid state. self._get_client_config(client_dir) # Make sure the mount path exists util.mkdir_p(path) # Check if it is already mounted. try: root = os.path.join(path, ".eden", "root") target = readlink_retry_estale(root) if target == path: print_stderr( "ERROR: Mount point in use! " "{} is already mounted by Eden.", path) return 1 else: # If we are here, MOUNT/.eden/root is a symlink, but it does not # point to MOUNT. This suggests `path` is a subdirectory of an # existing mount, though we should never reach this point # because _get_client_dir_for_mount_point() above should have # already thrown an exception. We return non-zero here just in # case. print_stderr( "ERROR: Mount point in use! " "{} is already mounted by Eden as part of {}.", path, root, ) return 1 except OSError as ex: err = ex.errno if err != errno.ENOENT and err != errno.EINVAL: raise # Ask eden to mount the path mount_info = eden_ttypes.MountInfo( mountPoint=os.fsencode(path), edenClientPath=os.fsencode(client_dir)) with self.get_thrift_client() as client: client.mount(mount_info) return 0
def mount(self, path): # Load the config info for this client, to make sure we # know about the client. path = os.path.realpath(path) client_dir = self._get_client_dir_for_mount_point(path) self._get_repo_name(client_dir) # Make sure the mount path exists util.mkdir_p(path) # Ask eden to mount the path mount_info = eden_ttypes.MountInfo(mountPoint=path, edenClientPath=client_dir) with self.get_thrift_client() as client: client.mount(mount_info)
def clone(self, client_config: ClientConfig, path: str, snapshot_id: str) -> None: if path in self._get_directory_map(): raise Exception("""\ mount path %s is already configured (see `eden list`). \ Do you want to run `eden mount %s` instead?""" % (path, path)) # Create the mount point directory self._create_mount_point_dir(path) # Create client directory clients_dir = self._get_clients_dir() util.mkdir_p(clients_dir) # This directory probably already exists. client_dir = self._create_client_dir_for_path(clients_dir, path) # Store snapshot ID if snapshot_id: client_snapshot = os.path.join(client_dir, SNAPSHOT) with open(client_snapshot, "wb") as f: f.write(SNAPSHOT_MAGIC) f.write(binascii.unhexlify(snapshot_id)) else: raise Exception("snapshot id not provided") # Create bind mounts directories bind_mounts_dir = os.path.join(client_dir, "bind-mounts") util.mkdir_p(bind_mounts_dir) for mount in client_config.bind_mounts: util.mkdir_p(os.path.join(bind_mounts_dir, mount)) config_path = os.path.join(client_dir, MOUNT_CONFIG) self._save_client_config(client_config, config_path) # Prepare to mount mount_info = eden_ttypes.MountInfo( mountPoint=os.fsencode(path), edenClientPath=os.fsencode(client_dir)) with self.get_thrift_client() as client: client.mount(mount_info) self._run_post_clone_hooks(path, client_dir, client_config) # Add mapping of mount path to client directory in config.json self._add_path_to_directory_map(path, os.path.basename(client_dir))
def create_test_mount( self, path: str, snapshot: Optional[str] = None, client_name: Optional[str] = None, scm_type: str = "hg", active: bool = True, setup_path: bool = True, dirstate_parent: Union[str, Tuple[str, str], None] = None, backing_repo: Optional[Path] = None, ) -> EdenCheckout: """ Define a configured mount. If active is True and status was set to ALIVE when creating the FakeClient then the mount will appear as a normal active mount. It will be reported in the thrift results and the mount table, and the mount directory will be populated with a .hg/ or .git/ subdirectory. The setup_path argument can be set to False to prevent creating the fake mount directory on disk. Returns the absolute path to the mount directory. """ full_path = os.path.join(self._tmp_dir, path) if full_path in self._checkouts_by_path: raise Exception(f"duplicate mount definition: {full_path}") if snapshot is None: snapshot = self.default_commit_hash if client_name is None: client_name = path.replace("/", "_") backing_repo_path = (backing_repo if backing_repo is not None else self.default_backing_repo) state_dir = self.clients_path / client_name assert full_path not in self._checkouts_by_path config = CheckoutConfig( backing_repo=backing_repo_path, scm_type=scm_type, guid=uuid.uuid4(), mount_protocol="prjfs" if sys.platform == "win32" else "fuse", default_revision=snapshot, redirections={}, active_prefetch_profiles=[], ) checkout = FakeCheckout(state_dir=state_dir, config=config, snapshot=snapshot) self._checkouts_by_path[full_path] = checkout # Write out the config file and snapshot file state_dir.mkdir() eden_checkout = EdenCheckout(typing.cast(EdenInstance, self), Path(full_path), state_dir) eden_checkout.save_config(config) eden_checkout.save_snapshot(snapshot) if active and self._status == fb303_status.ALIVE: # Report the mount in /proc/mounts dev_id = self._next_dev_id self._next_dev_id += 1 self.mount_table.stats[full_path] = mtab.MTStat( st_uid=os.getuid(), st_dev=dev_id, st_mode=(stat.S_IFDIR | 0o755)) # Tell the thrift client to report the mount as active self._fake_client._mounts.append( eden_ttypes.MountInfo( mountPoint=os.fsencode(full_path), edenClientPath=os.fsencode(state_dir), state=eden_ttypes.MountState.RUNNING, )) # Set up directories on disk that look like the mounted checkout if setup_path: os.makedirs(full_path) if scm_type == "hg": self._setup_hg_path(full_path, checkout, dirstate_parent) elif scm_type == "git": os.mkdir(os.path.join(full_path, ".git")) return EdenCheckout(typing.cast(EdenInstance, self), Path(full_path), Path(state_dir))
def clone(self, client_config: ClientConfig, path: str, snapshot_id: str): if path in self._get_directory_map(): raise Exception('''\ mount path %s is already configured (see `eden list`). \ Do you want to run `eden mount %s` instead?''' % (path, path)) # Make sure that path is a valid destination for the clone. st = None try: st = os.stat(path) except OSError as ex: if ex.errno == errno.ENOENT: # Note that this could also throw if path is /a/b/c and /a # exists, but it is a file. util.mkdir_p(path) else: raise # Note that st will be None if `mkdir_p` was run in the catch block. if st: if stat.S_ISDIR(st.st_mode): # If an existing directory was specified, then verify it is # empty. if len(os.listdir(path)) > 0: raise OSError(errno.ENOTEMPTY, os.strerror(errno.ENOTEMPTY), path) else: # Throw because it exists, but it is not a directory. raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), path) # Create client directory clients_dir = self._get_clients_dir() util.mkdir_p(clients_dir) # This directory probably already exists. client_dir = self._create_client_dir_for_path(clients_dir, path) # Store snapshot ID if snapshot_id: client_snapshot = os.path.join(client_dir, SNAPSHOT) with open(client_snapshot, 'wb') as f: f.write(SNAPSHOT_MAGIC) f.write(binascii.unhexlify(snapshot_id)) else: raise Exception('snapshot id not provided') # Create bind mounts directories bind_mounts_dir = os.path.join(client_dir, 'bind-mounts') util.mkdir_p(bind_mounts_dir) for mount in client_config.bind_mounts: util.mkdir_p(os.path.join(bind_mounts_dir, mount)) config_path = os.path.join(client_dir, MOUNT_CONFIG) self._save_client_config(client_config, config_path) # Prepare to mount mount_info = eden_ttypes.MountInfo(mountPoint=path, edenClientPath=client_dir) with self.get_thrift_client() as client: client.mount(mount_info) self._run_post_clone_hooks(path, client_dir, client_config) # Add mapping of mount path to client directory in config.json self._add_path_to_directory_map(path, os.path.basename(client_dir))
def test_end_to_end_test_with_various_scenarios(self, mock_watchman): side_effects: List[Dict[str, Any]] = [] calls = [] tmp_dir = tempfile.mkdtemp(prefix='eden_test.') try: # In edenfs_path1, we will break the snapshot check. edenfs_path1 = os.path.join(tmp_dir, 'path1') # In edenfs_path2, we will break the inotify check and the Nuclide # subscriptions check. edenfs_path2 = os.path.join(tmp_dir, 'path2') calls.append(call(['watch-list'])) side_effects.append({'roots': [edenfs_path1, edenfs_path2]}) calls.append(call(['watch-project', edenfs_path1])) side_effects.append({'watcher': 'eden'}) calls.append(call(['debug-get-subscriptions', edenfs_path1])) side_effects.append( _create_watchman_subscription( filewatcher_subscription=f'filewatcher-{edenfs_path1}', )) calls.append(call(['watch-project', edenfs_path2])) side_effects.append({'watcher': 'inotify'}) calls.append(call(['watch-del', edenfs_path2])) side_effects.append({'watch-del': True, 'root': edenfs_path2}) calls.append(call(['watch-project', edenfs_path2])) side_effects.append({'watcher': 'eden'}) calls.append(call(['debug-get-subscriptions', edenfs_path2])) side_effects.append( _create_watchman_subscription(filewatcher_subscription=None)) mock_watchman.side_effect = side_effects out = io.StringIO() dry_run = False mount_paths = OrderedDict() edenfs_path1_snapshot_hex = 'abcd' * 10 mount_paths[edenfs_path1] = { 'bind-mounts': {}, 'mount': edenfs_path1, 'scm_type': 'hg', 'snapshot': edenfs_path1_snapshot_hex, 'client-dir': '/I_DO_NOT_EXIST1' } mount_paths[edenfs_path2] = { 'bind-mounts': {}, 'mount': edenfs_path2, 'scm_type': 'git', 'snapshot': 'dcba' * 10, 'client-dir': '/I_DO_NOT_EXIST2' } config = FakeConfig(mount_paths, is_healthy=True) config.get_thrift_client()._mounts = [ eden_ttypes.MountInfo(mountPoint=edenfs_path1), eden_ttypes.MountInfo(mountPoint=edenfs_path2), ] os.mkdir(edenfs_path1) hg_dir = os.path.join(edenfs_path1, '.hg') os.mkdir(hg_dir) dirstate = os.path.join(hg_dir, 'dirstate') dirstate_hash = b'\x12\x34\x56\x78' * 5 parents = (dirstate_hash, b'\x00' * 20) with open(dirstate, 'wb') as f: eden.dirstate.write(f, parents, tuples_dict={}, copymap={}) mount_table = FakeMountTable() mount_table.stats[edenfs_path1] = mtab.MTStat(st_uid=os.getuid(), st_dev=11) mount_table.stats[edenfs_path2] = mtab.MTStat(st_uid=os.getuid(), st_dev=12) exit_code = doctor.cure_what_ails_you(config, dry_run, out, mount_table) finally: shutil.rmtree(tmp_dir) self.assertEqual( f'''\ Performing 3 checks for {edenfs_path1}. p1 for {edenfs_path1} is {'12345678' * 5}, but Eden's internal hash in its SNAPSHOT file is {edenfs_path1_snapshot_hex}. Performing 2 checks for {edenfs_path2}. Previous Watchman watcher for {edenfs_path2} was "inotify" but is now "eden". Nuclide appears to be used to edit {edenfs_path2}, but a key Watchman subscription appears to be missing. This can cause file changes to fail to show up in Nuclide. Currently, the only workaround this is to run "Nuclide Remote Projects: Kill And Restart" from the command palette in Atom. Number of fixes made: 1. Number of issues that could not be fixed: 2. ''', out.getvalue()) mock_watchman.assert_has_calls(calls) self.assertEqual(1, exit_code)