Погружаемся в воды Стикса. Часть III: Заключительное слово.

В этой статье мы подведем итог по всем нашим статьям, которые описывают протокол 9P/Styx, а также поделимся некоторыми мыслями о том, как происходила реализация протокола и что нас вдохновило заняться погружением в воды Стикса.

Протокол 9P/Styx — это довольно простой протокол, несмотря на все те трудности с которыми мы столкнулись при его реализации. Несмотря на простоту, протокол очень развитый и позволяет с минимальными усилиями организовывать распределенные системы и некоторые иные сервисы. Но, честно говоря, нас вдохновили несколько иные аспекты 9P/Styx: дело в том, практически любую достаточно утилитарную программу, можно представить как некий сервис, предоставляющий доступ к некоторой файловой системе. Такая концепция позволяет создавать то, что называется файл-серверами (в терминологии операционной системы Plan 9) и манипулировать самыми разными вещами так, как будто у нас они представлены обычными файлами !

Эталонные реализации, написанные на языке программирования C, достаточно простая и внятная документация, а также обилие возможностей при максимально простом устройстве — вот что предопределило наш выбор ! Кроме того, привязки (они же биндинги) для библиотек 9P/Styx можно найти под любой язык программирования, но только не для D: это действительно удручающее зрелище, так как есть удобные библиотеки и инструменты на всем (даже на Haskell !) и оригинальные реализации на специфическом диалекте языка C от Plan 9, но вот весь этот инструментарий в своих программах использовать не получится !

Именно с такой мотивацией, а также с глубочайшим интересом, мы шли к реализации 9P/Styx с начала нашего изучения D аж целых 8 лет ! С самого начала знакомства с Plan 9 нам захотелось иметь ее функционал на любой платформе, где есть возможность запустить компилятор D и код, скомпилированный этим компилятором, и теперь, такая возможность у нас есть !

И все благодаря нашей библиотеке styx2000, которая доступна в реестре dub.

Но… Честно говоря, смотря сейчас на нашу библиотеку, кажется, что все могло быть устроено совершенно иначе и некоторые вещи хотелось бы заменить и исправить, в частности, сделать все гораздо проще и минималистичнее. Однако, несмотря на избыточное по нашему мнению выделение отдельных типов под каждый объект протокола и использование при этом динамических массивов, дизайн модулей, который имеется сейчас — все же лучшее решение, чем мы бы могли подумать.

Также, скорее всего, стоило воздержаться от включения некоторого функционала в библиотеку (модуль styx2000.extrautil), но все же, с этим модулем гораздо лучше, чем совсем без него.

Хочется также отдельно сказать, что мы постарались реализовать весь функционал протокола и нигде ничего не урезали, и честно старались соблюдать то, что указано в документации (мы кстати планируем сделать ее перевод здесь, чтобы простимулировать появление нового программного обеспечения, использующего 9P). И даже тут наш ждали сюрпризы: некоторые детали в документации весьма спорны, а понимаешь это только уже на этапе реализации, также хочется спасибо сказать разработчикам протокола за один «приятный» момент, связанный с реализацией функционала работающего с типами сообщений STAT

Здесь нам стоит немного пояснить одну деталь — если вы видели в репозитории библиотеки на GitHub первые примеры, которые демонстрируют самый базовый функционал, то вы сейчас поймете, что эти примеры оказались реально рабочими проверками реализации. И вот почему: некоторую часть информации по протоколу 9P/Styx мы не смогли переварить, и нам откровенно не хватало информации несмотря на обилие сайтов его описывающих, и потому некоторую часть данных мы по внутреннему устройству 9P/Styx мы получили весьма банальным перехватом трафика приложений из Inferno OS с помощью утилиты в Linux под названием tcpdump.

Перехват и последующий разбор (в том числе и ручной) сообщений, позволил понять ряд моментов реализации протокола, а также позволил нам понять, что надо реализовать — и примеры по переводу сообщений Styx в текстовой вид очень сильно помогали с анализом того, что очередной раз пошло не так.

Возвращаясь к подвоху с реализацией Stat, мы поясним, что больше всего нас впечатлило. Дело в том, что функции работающие с этой частью протокола в C, имеют значительный баг и из-за этого приходится немного отступить от той структуры сообщений с типом STAT, которые были описаны в документации. И самое плохое, что это тоже было в документации, но описано это как незначительный баг, без намека на то, что используется совсем отличная структура для сообщения, чем ранее описанная для сообщения с типом STAT ! Нам же подобная ситуация стоила почти недели разбирательства с кучей перехваченных данных, документацией, а также еще и проблемами в разборе полей структуры !

И это было на тот момент еще только начало наших опытов и одновременно наших проблем. Дело в том, что помимо собственно реализации, нам требовался полигон для тестирования. И с ним как раз была уйма проблем: самым идеальным вариантом для тестирования 9P/Styx была Inferno OS, но ее сборка в современных условиях — это головная боль, особенно если у вас, что-то отличное от классических x86 и x86_64 !

Проблемы со сборкой мы решили относительно быстро, достав старые свои же архивы с преднастроенной средой для сборки (не, не Docker, даже не думайте) и собрав все за полтора часа на ноутбуке и двух Raspberry Pi, один из которых находился у второго автора блога, а у первого был к нему только доступ по сети… Это сделано было специально, чтобы можно было протестировать работу приложений по реальной сети, а не только через локальное соединение и размещение сервера и клиента на одном ПК.

После решения всех этих проблем и когда библиотека уже была готова к использованию, мы столкнулись вообще с парадоксальной ситуацией: все готово, все работает, однако, мы сами не создали ни одного более-менее работоспособного сервера и клиента, поскольку понятия не имели как это в принципе написать! Использование сторонних примеров и их изучение нам не помогло никак, поскольку наша библиотека имеет настолько отличающуюся структуру, что использование сторонних примеров и техник из них ставит еще в больший тупик, чем тот который изначально был.

Выход из подобной ситуации мы искали очень долго, и до сих пор не уверены, что мы пришли к успешному решению. Частично с примерами нам помогла разработка уже упоминавшегося styx2000.extrautil, а также всеми силами посодействовали эксперименты с программами Inferno OS styxmon и styxchat.

Но, больше всего помог эксперимент с раздачей пустой папки и еще один эксперимент, код которого мы сейчас приведем без пояснений к нему:

#!/usr/bin/env dub
/+ dub.sdl:
	dependency "styx2000" version="~>0.0.9"
+/

private
{
    import std.algorithm : remove;
    import std.conv : to;
    import std.file : dirEntries, DirEntry, SpanMode, exists;
    import std.path : buildPath, baseName;
    import std.socket;
    import std.stdio : writeln, writefln;

    import styx2000.extrautil;
    
    import styx2000.protoconst;
    import styx2000.protomsg;
    import styx2000.protomsg.typeconv : toStyxObject;
    import styx2000.protobj;
}

auto toDir(DirEntry de, ushort type, uint dev, ulong path, string uid, string gid, string muid = "")
{
	auto qtype = (de.isDir) ? STYX_QID_TYPE.QTDIR : STYX_QID_TYPE.QTFILE;
	auto vers = de.timeLastAccessed.toUnixTime;
	auto qid = new Qid(qtype, cast(uint) vers, path);
	//// wrong type here !
	auto mode = new Perm((type << 24) | (de.attributes & 0x1ff));
	auto atime = cast(uint) vers;
	auto length = (de.isDir) ? 0 : de.size;
	auto mtime = cast(uint) de.timeLastModified.toUnixTime;
	auto name = baseName(de.name);
	
	Dir stat = new Dir(type, dev, qid, mode, atime, mtime, length, name, uid, gid, muid);
	
	return stat;
}

auto toDirStat(ushort type, uint dev, string path, string uid, string gid, string muid = "")
{
	auto d = dirEntries(path, SpanMode.shallow);
	
	Dir[] dirs;
	
	dirs ~= toDir(DirEntry(path), type, dev, hash8(path), uid, gid, muid);
	
	foreach (e; d)
	{
		dirs ~= toDir(e, type, dev, hash8(e.name), uid, gid, muid);
	}

	DirStat dirstat = new DirStat;
	dirstat.setDirs(dirs);
	
	return dirstat;
}

auto readAt(string filepath, ulong offset, uint count)
{
	import std.stdio;
	File file;
	file.open(filepath, `rb`);
	auto size = file.size;
	if (offset >= size)
	{
		return cast(StyxObject[])[
			new Count(0),
			new Data
		];
	}
	else
	{
		ubyte[] data = new ubyte[count];
		file.seek(offset);
		file.rawRead(data);
		uint realCount;
		
		if ((size - offset) < count)
		{
			realCount = cast(uint) (size - offset);
		}
		else
		{
			realCount = count;
		}
		return cast(StyxObject[])[
			new Count(cast(uint) realCount),
			new Data(data[0..realCount])
		];
	}
}

auto readAt(ubyte[] data, ulong offset, uint count)
{
	auto size = data.length;
	if (offset >= size)
	{
		return cast(StyxObject[])[
			new Count(0),
			new Data
		];
	}
	else
	{
		ubyte[]  bdata;
		
		if ((offset + count) > data.length)
		{
			if (offset >= data.length)
			{
				return cast(StyxObject[])[
					new Count(0),
					new Data
				];
			}
			else
			{
				bdata = data[offset..$];
			}
		}
		else
		{
			bdata = data[offset..offset+count];
		}
		
		return cast(StyxObject[])[
			new Count(cast(uint) bdata.length),
			new Data(bdata)
		];
	}
}

class ServeFiles
{
	private 
	{
		// directory 
		string dir;
		// user and their group
		string uid, gid;
		// active file
		string[uint] fids;
	}
	
	StyxMessage handle(StyxMessage query)
	{
		StyxMessage reply;
		
		auto type = query[1].toType.getType;
		auto tag = 	query[2].toTag.getTag;
		
		switch (type)
		{
			case STYX_MESSAGE_TYPE.T_VERSION:
				reply = createRmsgVersion;
				break;
			case STYX_MESSAGE_TYPE.T_ATTACH:
				auto fid = query[3].toFid.getFid;
				reply = handleAttach(tag, fid);
				break;
			case STYX_MESSAGE_TYPE.T_CLUNK:
				auto fid = query[3].toFid.getFid;
				fids.remove(fid);
				reply = createRmsgClunk(tag);
				break;
			case STYX_MESSAGE_TYPE.T_FLUSH:
				reply = createRmsgFlush(tag);
				break;
			default:
				auto args = query[3..$].dup;
				reply = handleFCall(type, tag, args);
				break;
		}
		
		return reply;
	}

	// attach
	StyxMessage handleAttach(ushort tag, uint fid) {
		fids[fid] = dir;
		return cast(StyxMessage) [
			new Size,
			new Type(STYX_MESSAGE_TYPE.R_ATTACH),
			new Tag(tag),
			new Qid(STYX_QID_TYPE.QTDIR)
		];
	}
	
	// file operations
	StyxMessage handleFCall(STYX_MESSAGE_TYPE type, ushort tag, StyxObject[] args)
	{
		auto rtype = (new Type(type)).toRtype.getType;
		StyxMessage msg = createHeader(0, rtype, tag);
		
		switch (type)
		{
			case STYX_MESSAGE_TYPE.T_WALK:
				auto fid = args[0].toFid.getFid;
				auto newfid = args[1].toNewFid.getFid;
				auto nwname = args[2].toNwname.getName;
				msg = processWalk(tag, fid, newfid, nwname);
				break;
			case STYX_MESSAGE_TYPE.T_STAT:
				auto fid = args[0].toFid.getFid;
				msg = processStat(tag, fid);
				break;
			case STYX_MESSAGE_TYPE.T_OPEN:
				auto fid = args[0].toFid.getFid;
				auto mode = args[1].toMode.getMode;
				msg = processOpen(tag, fid, mode);
				break;
			case STYX_MESSAGE_TYPE.T_READ:
				auto fid = args[0].toFid.getFid;
				writeln(`SOURCE FID `, fid);
				writeln(fids);
				auto offset = args[1].toOffset.getOffset;
				auto count = args[2].toCount.getCount;
				msg = processRead(tag, fid, offset, count);
				break;
			default:
				msg = createRmsgError(tag, `Wrong message: ` ~ type.to!string);
				break;
		}
		
		writeln(fids);
		return msg;
	}
	
	auto processWalk(ushort tag, uint fid, uint newfid, string[] nwname)
	{
		StyxMessage msg = createHeader(0, STYX_MESSAGE_TYPE.R_WALK, tag);
		
		if (nwname.length == 0)
		{
			msg ~= cast(StyxMessage) [
				new Nwqid
			];
			fids[newfid] = dir;
		}
		else
		{
			auto path = buildPath(dir ~ nwname);
			
			if (path.exists)
			{
				DirEntry de = DirEntry(path);
				auto qtype = (de.isDir) ? STYX_QID_TYPE.QTDIR : STYX_QID_TYPE.QTFILE;
				auto qvers = cast(uint) de.timeLastModified.toUnixTime;
				auto qpath = hash8(path);
				auto nwqid = new Nwqid([new Qid(qtype, qvers, qpath)]);
				msg ~= cast(StyxObject) nwqid;
				if (path != dir)
				{
					fids[newfid] = path;
				}
			}
			else
			{
				msg = createRmsgError(tag, `File  ` ~ path ~ ` doesn't exist`);
			}
		}
		
		return msg;
	}
	
	auto processStat(ushort tag, uint fid)
	{
		StyxMessage msg = createHeader(0, STYX_MESSAGE_TYPE.R_STAT, tag);
		
		if (fid in fids)
		{
			auto path = fids[fid];
			DirEntry de = DirEntry(path);
			
			auto qtype = (de.isDir) ? STYX_QID_TYPE.QTDIR : STYX_QID_TYPE.QTFILE;
			auto qvers = cast(uint) de.timeLastModified.toUnixTime;
			auto qpath = hash8(path);
			auto qid = new Qid(qtype, qvers, qpath);
			auto mode = new Perm((qtype << 24) | (de.attributes & 0x1ff));
			auto length = (de.isDir) ? 0 : de.size;
			auto name = baseName(de.name);
	
			auto stat = new Stat(0, 0, qid, mode, qvers, qvers, length, name, uid, gid);
			msg ~= cast(StyxObject) stat;
		}
		else
		{
			msg = createRmsgError(tag, `Stat for path is wrong`);
		}
		
		return msg;
	}
	
	auto processOpen(ushort tag, uint fid, STYX_FILE_MODE mode)
	{
		StyxMessage msg = createHeader(0, STYX_MESSAGE_TYPE.R_OPEN, tag);
		
		if (fid in fids)
		{
			auto path = fids[fid];
			DirEntry de = DirEntry(path);
			auto qtype = (de.isDir) ? STYX_QID_TYPE.QTDIR : STYX_QID_TYPE.QTFILE;
			auto qvers = cast(uint) de.timeLastModified.toUnixTime;
			auto qpath = hash8(path);

			msg ~= cast(StyxObject[]) [
				new Qid(qtype, qvers, qpath),
				new Iounit(8168)
			];
		}
		else
		{
			msg = createRmsgError(tag, `Open for path is wrong`);
		}
		
		return msg;		
	}
	
	auto processRead(ushort tag, uint fid, ulong offset, uint count)
	{
		StyxMessage msg = createHeader(0, STYX_MESSAGE_TYPE.R_READ, tag);
		
		if (fid in fids)
		{
			auto path = fids[fid];
			
			if (DirEntry(path).isDir)
			{
				DirStat ds = new DirStat;
				Dir[] dirs;
				
				foreach (e; dirEntries(path, SpanMode.shallow))
				{
					auto qtype = (e.isDir) ? STYX_QID_TYPE.QTDIR : STYX_QID_TYPE.QTFILE;
					auto qvers = cast(uint) e.timeLastModified.toUnixTime;
					auto qpath = hash8(e.name);
					auto qid = new Qid(qtype, qvers, qpath);
					auto mode = new Perm((qtype << 24) | (e.attributes & 0x1ff));
					auto length = (e.isDir) ? 0 : e.size;
					auto name = baseName(e.name);
	
					auto d = new Dir(0, 0, qid, mode, qvers, qvers, length, name, uid, gid);
					dirs ~= d;
				}
				
				ds.setDirs(dirs);
				ubyte[] data = ds.pack;
				msg ~= readAt(data, offset, count);
			}
			else
			{
				msg ~= readAt(path, offset, count);
			}
		}
		else
		{
			msg = createRmsgError(tag, `Read for path is wrong`);
		}
		
		return msg;
	}
		
	this(string dir, string uid, string gid)
	{
		this.dir = dir;
		this.uid = uid;
		this.gid = gid;
	}
}

void main()
{
    auto listener = new Socket(AddressFamily.INET, SocketType.STREAM);
    listener.bind(new InternetAddress("<адрес компьютера в сети>", 4445));
    listener.listen(10);
    
    SocketSet readSet = new SocketSet;
    Socket[] connectedClients; 
    ubyte[8192] buffer;
    
    bool isRunning = true;
    
    ServeFiles fs = new ServeFiles(
		`<папка для раздачи>`,
		`<имя пользователя - владельца>`,
		`<имя группы пользователя>`
    );
    
    while (isRunning)
    {
        readSet.reset;
        readSet.add(listener);
        foreach (client; connectedClients)
        {
            readSet.add(client);
        }

        if (Socket.select(readSet, null, null))
        {
            foreach (client; connectedClients)
            {
                if (readSet.isSet(client))
                {
                    auto got = client.receive(buffer);
                    
                    foreach (msg; buffer[0..got].dup.byRawMessage)
                    {
						auto tmsg = msg.decode;
						writeln("<Tmsg> ", tmsg);
						writeln("<Rmsg> ", fs.handle(tmsg));
						auto rmsg = fs.handle(tmsg).encode;
						client.send(rmsg);
					}
                }
            }
            
            if (readSet.isSet(listener))
            {
                auto newSocket = listener.accept;
                connectedClients ~= newSocket; 
            }
        }
    }
}

То, что вы сейчас увидели — ограниченный сервер для раздачи уже не пустой папки, в которой можно писать в файлы и просматривать папку (а также и файлы в ней). Многое в этом примере недоработано и неясно, поскольку мы до сих пор не знаем, какой минимальный набор сообщений надо реализовать для сервера, чтобы работали все возможности, которые поддерживаются обычной файловой системой. Однако, папка, которую мы раздавали таким образом, видится в Plan 9!

Но и тут мы столкнулись с проблемой, которую непонятно даже как решать, и мы всецело рассчитываем на то, что кто-то заинтересуется темой и создаст рабочее решение. Дело в том, что мы столкнулись с проблемой, корень которой скорее всего лежит вне Plan 9 и нашей библиотекой — у нас появились непонятные осложнения с монтированием файловой системы в Linux. Что бы мы ни пробовали в этом случае, и 9pfuse и mount приводили либо к зависанию папки при просмотре в файловом менеджере, либо к чтению папки вместо файлов. при том, что в Plan 9 и Inferno работало все как надо !

Вот с такими трудностями и эмоциями, мы столкнулись при реализации 9P/Styx, но мы все равно рады тому, что получилось. Кроме того, мы надеемся, что вас заинтересовало наше изложение устройства протокола и что у вас будут проекты со Styx.

Убедительная просьба, если вы получите рабочий прототип раздачи файла или папки через 9P/Styx, сообщите нам об этом (в комментариях или в любой из наших каналов), а начать свои опыты вы можете с этого репозитория: сервер 9P/Styx.

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий