草庐IT

驱动开发:通过Async反向与内核通信

LyShark 2023-04-17 原文

在前几篇文章中给大家具体解释了驱动与应用层之间正向通信的一些经典案例,本章将继续学习驱动通信,不过这次我们学习的是通过运用Async异步模式实现的反向通信,反向通信机制在开发中时常被用到,例如一个杀毒软件如果监控到有异常进程运行或有异常注册表被改写后,该驱动需要主动的通知应用层进程让其知道,这就需要用到驱动反向通信的相关知识点,如下将循序渐进的实现一个反向通信案例。

在开始学习Async反向通信之前先来研究一个Sync正向通信案例,不论是正向反向通信其在通信模式上与《驱动开发:通过ReadFile与内核层通信》所介绍的通信模式基本一致,都是通过ReadFile触发驱动中的IRP_MJ_READ读取派遣,唯一的区别是在传输数据时使用了MmGetSystemAddressForMdl方式,它将给定MDL描述的物理页面映射到系统空间,并调用RtlCopyMemory()将全局字符串复制到这个空间内,这样客户端就可以循环读取内核传出的数据。

我们来看驱动端代码是如何实现的这个功能,代码并没有什么特殊的无法理解的点,只是需要注意我们在驱动入口调用IoCreateDevice()时传入了第二个参数FILE_DEVICE_EXTENSION,该参数的作用是,创建设备时,指定设备扩展内存的大小,传一个值进去,就会给设备分配一块非页面内存。

#include <ntddk.h>
#include <stdio.h>

// 保存一段非分页内存,用于给全局变量使用
#define FILE_DEVICE_EXTENSION 4096

// 定义全局字符串
static int global_count = 0;
static char global_char[5][128] = { 0 };

// 驱动绑定默认派遣函数
NTSTATUS _DefaultDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 驱动创建后触发
NTSTATUS _SyncCreateCloseDispatch(PDEVICE_OBJECT _pDevcieObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_SUCCESS;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 应用层读数据后触发
NTSTATUS _SyncReadDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(_pIrp);
	PVOID pBuffer = NULL;
	ULONG uBufferLen = 0;

	do
	{
		// 读写请求使用的是直接I/O方式
		pBuffer = MmGetSystemAddressForMdl(_pIrp->MdlAddress);
		if (pBuffer == NULL)
		{
			status = STATUS_UNSUCCESSFUL;
			break;
		}
		uBufferLen = pIrpStack->Parameters.Read.Length;
		DbgPrint("读字节长度: %d \n", uBufferLen);

		// 最大支持20字节读请求
		uBufferLen = uBufferLen >= 20 ? 20 : uBufferLen;

		// 输出五次字符串
		if (global_count < 5)
		{
			RtlCopyMemory(pBuffer, global_char[global_count], uBufferLen);
			global_count = global_count + 1;
		}

	} while (FALSE);

	// 填写返回状态及返回大小
	_pIrp->IoStatus.Status = status;
	_pIrp->IoStatus.Information = uBufferLen;

	// 完成IRP
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return status;
}

// 卸载驱动
VOID _UnloadDispatch(PDRIVER_OBJECT _pDriverObject)
{
	// 删除创建的设备
	UNICODE_STRING  Win32DeviceName;
	RtlInitUnicodeString(&Win32DeviceName, L"\\DosDevices\\LySharkSync");
	IoDeleteDevice(_pDriverObject->DeviceObject);
}

// 驱动入口
NTSTATUS DriverEntry(PDRIVER_OBJECT _pDriverObject, PUNICODE_STRING _pRegistryPath)
{
	UNICODE_STRING DeviceName, Win32DeivceName;
	PDEVICE_OBJECT pDeviceObject = NULL;
	NTSTATUS status;
	HANDLE hThread;
	OBJECT_ATTRIBUTES ObjectAttributes;

	// 设置符号名
	RtlInitUnicodeString(&DeviceName, L"\\Device\\LySharkSync");
	RtlInitUnicodeString(&Win32DeivceName, L"\\DosDevices\\LySharkSync");

	// 循环初始化IRP函数
	for (ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		_pDriverObject->MajorFunction[i] = _DefaultDispatch;
	}

	// 再次覆盖派遣函数
	_pDriverObject->MajorFunction[IRP_MJ_CREATE] = _SyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_CLOSE] = _SyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_READ] = _SyncReadDispatch;
	_pDriverObject->DriverUnload = _UnloadDispatch;

	// 分配一个自定义扩展 大小为sizeof(DEVEXT)
	// By: LyShark.com
	status = IoCreateDevice(_pDriverObject, sizeof(FILE_DEVICE_EXTENSION), &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
	if (!NT_SUCCESS(status))
		return status;
	
	if (!pDeviceObject)
		return STATUS_UNEXPECTED_IO_ERROR;

	// 为全局变量赋值
	strcpy(global_char[0], "hi,lyshark A");
	strcpy(global_char[1], "hi,lyshark B");
	strcpy(global_char[2], "hi,lyshark C");
	strcpy(global_char[3], "hi,lyshark D");
	strcpy(global_char[4], "hi,lyshark E");

	// 指定读写方式为 直接I/O MDL模式
	pDeviceObject->Flags |= DO_DIRECT_IO;

	// 数据传输时地址校验大小
	pDeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
	status = IoCreateSymbolicLink(&Win32DeivceName, &DeviceName);

	pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
	return STATUS_SUCCESS;
}

对于应用层来说并没有什么特别的,同样调用ReadFile读取内核中的参数,同样for循环读取五次,代码如下:

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
	HANDLE hFile;
	char Buffer[10] = { 0 };
	DWORD dwRet = 0;
	BOOL bRet;

	hFile = CreateFileA("\\\\.\\LySharkSync", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	for (int x = 0; x < 5; x++)
	{
		bRet = ReadFile(hFile, Buffer, 20, &dwRet, NULL);
		if (!bRet)
		{
			CloseHandle(hFile);
			return 0;
		}
		printf("读入数据: %s -> 读取长度: %d \n", Buffer, dwRet);
	}
	return 0;
}

这段代码运行效果如下:

与同步模式不同,异步模式虽然同样使用ReadFile实现通信,但在通信中引入了Event事件通知机制,这也是异步与同步最大的区别所在,用户层可以分别创建多个Event事件,等待内核依次做出相应并最终一并返回。

首先驱动内定义了_DeviceExtension自定义接口,该接口用于保存此次事件所对应的Irp以及其所对应的DPC时间等。

异步分发函数_AsyncReadDispatch同样是被IRP_MJ_READ派遣函数触发的,触发后其内部会首先IoGetCurrentIrpStackLocation得到当前IRP的堆栈信息,然后设置IoMarkIrpPending()并最终将该IRP通过InsertTailList()插入到IRP链表内等待被处理。

  • IoMarkIrpPending
    • 用于标记指定的IRP,标志着某个驱动的分发例程(分发函数)因需要被其他的驱动程序进一步处理最终返回STATUS_PENDING状态。

函数_CustomDpc则是定时器内部要执行的具体操作,在DriverEntry驱动入口处做了如下初始化,初始化了链表,并初始化了一个定时器,最后启动这个定时器每隔1秒都会执行一次_CustomDpc如果我们的IRP链表内IsListEmpty() 检测存在数据,则会主动拷贝内存RtlCopyMemory并推送到应用层。

// 初始化IRP链表
InitializeListHead(&pDevExt->IrpList);
// 初始化定时器
KeInitializeTimer(&(pDevExt->timer));
// 初始化DPC pDevExt是传给_CustomDpc函数的参数
KeInitializeDpc(&pDevExt->dpc, (PKDEFERRED_ROUTINE)_CustomDpc, pDevExt);

// 设置定时时间位1s
pDevExt->liDueTime = RtlConvertLongToLargeInteger(-10000000);
// 启动定时器
KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);

驱动层完成代码如下所示:

#include <ntddk.h>

// 自定义接口扩展
typedef struct _DeviceExtension
{
	LIST_ENTRY IrpList;
	KTIMER timer;
	LARGE_INTEGER liDueTime;
	KDPC dpc;
}DEV_EXT, *PDEV_EXT;

// 默认派遣函数
NTSTATUS _DefaultDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 创建派遣函数
NTSTATUS _AsyncCreateCloseDispatch(PDEVICE_OBJECT _pDevcieObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_SUCCESS;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return  _pIrp->IoStatus.Status;
}

// 读取派遣函数
NTSTATUS _AsyncReadDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	NTSTATUS status;
	PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(_pIrp);
	PDEV_EXT pDevExt = (PDEV_EXT)_pDeviceObject->DeviceExtension;

	IoMarkIrpPending(_pIrp);

	// 将IRP插入自定义链表中插入的是ListEntry
	InsertTailList(&pDevExt->IrpList, &_pIrp->Tail.Overlay.ListEntry);

	// 返回pending 主要返回给I/O管理器的值必须和IRP的Pending标志位一致
	// By: LyShark.com
	// 即调用iomarkirppending和返回值要一致 
	return STATUS_PENDING;
}

// DPC线程
VOID _CustomDpc(PKDPC Dpc, PVOID DeferredContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
	PIRP pIrp;
	PDEV_EXT pDevExt = (PDEV_EXT)DeferredContext;
	PVOID pBuffer = NULL;
	ULONG uBufferLen = 0;
	PIO_STACK_LOCATION pIrpStack = NULL;

	do
	{
		if (!pDevExt)
		{
			break;
		}

		// 检查尾端IRP链表是否为空 为空则跳出
		if (IsListEmpty(&pDevExt->IrpList))
		{
			break;
		}

		// 从IRP链表中取出一个IRP并完成该IRP 取出的是ListEntry的地址
		PLIST_ENTRY pListEntry = (PLIST_ENTRY)RemoveHeadList(&pDevExt->IrpList);
		if (!pListEntry)
			break;

		pIrp = (PIRP)CONTAINING_RECORD(pListEntry, IRP, Tail.Overlay.ListEntry);
		pIrpStack = IoGetCurrentIrpStackLocation(pIrp);

		DbgPrint("当前DPC Irp: 0x%x\n", pIrp);

		// 驱动程序的读写方式位直接I/O
		pBuffer = MmGetSystemAddressForMdl(pIrp->MdlAddress);
		if (pBuffer == NULL)
		{
			pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
			pIrp->IoStatus.Information = 0;
			IoCompleteRequest(pIrp, IO_NO_INCREMENT);

			break;
		}
		uBufferLen = pIrpStack->Parameters.Read.Length;
		DbgPrint("读取DPC长度: %d\n", uBufferLen);

		// 支持5字节以下的读请求
		uBufferLen = uBufferLen > 13 ? 13 : uBufferLen;

		// 复制请求内容
		RtlCopyMemory(pBuffer, "hello lyshark", uBufferLen);

		pIrp->IoStatus.Status = STATUS_SUCCESS;
		pIrp->IoStatus.Information = uBufferLen;

		// 完成该IRP
		IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	} while (FALSE);

	// 重新设置定时器
	KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);
}

// 卸载驱动
VOID _UnloadDispatch(PDRIVER_OBJECT _pDriverObject)
{
	UNICODE_STRING Win32DeviceName;
	PDEV_EXT pDevExt = (PDEV_EXT)_pDriverObject->DeviceObject->DeviceExtension;

	RtlInitUnicodeString(&Win32DeviceName, L"\\DosDevices\\LySharkAsync");

	// 删除定时器
	// LyShark
	KeCancelTimer(&pDevExt->timer);
	// 删除创建的设备
	IoDeleteDevice(_pDriverObject->DeviceObject);
}

// 驱动入口
NTSTATUS DriverEntry(PDRIVER_OBJECT _pDriverObject, PUNICODE_STRING _pRegistryPath)
{
	UNICODE_STRING DeviceName, Win32DeivceName;
	PDEVICE_OBJECT pDeviceObject = NULL;
	NTSTATUS status;
	PDEV_EXT pDevExt = NULL;
	HANDLE hThread;
	OBJECT_ATTRIBUTES ObjectAttributes;
	CLIENT_ID CID;

	RtlInitUnicodeString(&DeviceName, L"\\Device\\LySharkAsync");
	RtlInitUnicodeString(&Win32DeivceName, L"\\DosDevices\\LySharkAsync");

	for (ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		_pDriverObject->MajorFunction[i] = _DefaultDispatch;
	}

	_pDriverObject->MajorFunction[IRP_MJ_CREATE] = _AsyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_CLOSE] = _AsyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_READ] = _AsyncReadDispatch;
	_pDriverObject->DriverUnload = _UnloadDispatch;

	// 分配自定义扩展
	status = IoCreateDevice(_pDriverObject, sizeof(DEV_EXT), &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
	if (!NT_SUCCESS(status))
		return status;
	if (!pDeviceObject)
		return STATUS_UNEXPECTED_IO_ERROR;

	pDeviceObject->Flags |= DO_DIRECT_IO;
	pDeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
	status = IoCreateSymbolicLink(&Win32DeivceName, &DeviceName);

	pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
	pDevExt = (PDEV_EXT)pDeviceObject->DeviceExtension;

	// 初始化IRP链表
	InitializeListHead(&pDevExt->IrpList);
	// 初始化定时器
	KeInitializeTimer(&(pDevExt->timer));
	// 初始化DPC pDevExt是传给_CustomDpc函数的参数
	KeInitializeDpc(&pDevExt->dpc, (PKDEFERRED_ROUTINE)_CustomDpc, pDevExt);

	// 设置定时时间位1s
	pDevExt->liDueTime = RtlConvertLongToLargeInteger(-10000000);
	// 启动定时器
	KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);

	return STATUS_SUCCESS;
}

驱动层说完了,接下来是应用层,对于应用层来说,需要使用CreateEvent打开通知事件,或者叫做事件对象,然后当有通知时,则直接使用ReadFile读取对应的缓冲区,当所有读取全部结束WaitForMultipleObjects等待结束即输出结果。

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
	HANDLE hFile;
	char Buffer[3][32] = { 0 };
	DWORD dwRet[3] = { 0 };
	OVERLAPPED ol[3] = { 0 };
	HANDLE hEvent[3] = { 0 };

	// By:LyShark
	hFile = CreateFileA("\\\\.\\LySharkAsync", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);
	if (INVALID_HANDLE_VALUE == hFile)
		return  0;

	// event用来通知请求完成
	hEvent[0] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[0].hEvent = hEvent[0];

	hEvent[1] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[1].hEvent = hEvent[1];

	hEvent[2] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[2].hEvent = hEvent[2];

	// 读取事件内容到缓存
	ReadFile(hFile, Buffer[0], 13, &dwRet[0], &ol[0]);
	ReadFile(hFile, Buffer[1], 13, &dwRet[1], &ol[1]);
	ReadFile(hFile, Buffer[2], 13, &dwRet[2], &ol[2]);

	// 等待三个事件执行完毕
	WaitForMultipleObjects(3, hEvent, TRUE, INFINITE);

	// 输出结果
	printf("缓存LyShark A: %s \n", Buffer[0]);
	printf("缓存LyShark B: %s \n", Buffer[1]);
	printf("缓存LyShark C: %s \n", Buffer[2]);

	CloseHandle(hFile);
	return  0;
}

这段代码最终运行效果如下:

有关驱动开发:通过Async反向与内核通信的更多相关文章

  1. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  2. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  3. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  4. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  5. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  6. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  7. ruby-on-rails - Enumerator.new 如何处理已通过的 block ? - 2

    我在理解Enumerator.new方法的工作原理时遇到了一些困难。假设文档中的示例:fib=Enumerator.newdo|y|a=b=1loopdoy[1,1,2,3,5,8,13,21,34,55]循环中断条件在哪里,它如何知道循环应该迭代多少次(因为它没有任何明确的中断条件并且看起来像无限循环)? 最佳答案 Enumerator使用Fibers在内部。您的示例等效于:require'fiber'fiber=Fiber.newdoa=b=1loopdoFiber.yieldaa,b=b,a+bendend10.times.m

  8. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  9. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  10. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

随机推荐