PG14:adminpack 外掛原始碼分析

ZARD_Fans發表於2024-03-14

adminpack 提供了大量支援功能,pgAdmin 和其他管理工具可以使用這些功能提供額外功能,例如遠端管理伺服器日誌檔案。預設情況下,只有資料庫超級使用者才能使用所有這些功能,但其他使用者也可以使用 GRANT 命令使用這些功能。

我們先來看一下他支援的函式,可以透過 \dx+ adminpack 來進行檢視

  • function pg_file_rename(text,text) 重新命名檔案
  • function pg_file_rename(text,text,text) 重新命名檔案,如果新檔案存在,將將其命名為第三個引數的名字
  • function pg_file_sync(text) 檔案刷入磁碟
  • function pg_file_unlink(text) 刪除檔案
  • function pg_file_write(text,text,boolean) 寫檔案
  • function pg_logdir_ls() 列出日誌目錄下的檔案

pg_file_rename(text,text)

用於重新命名檔案,我們看一下 sql 程式碼

CREATE FUNCTION pg_catalog.pg_file_rename(text, text)
RETURNS bool
AS 'SELECT pg_catalog.pg_file_rename($1, $2, NULL::pg_catalog.text);'
LANGUAGE SQL VOLATILE STRICT;

這裡我們看到兩個引數版本的 pg_file_rename 直接呼叫來三引數版本的 pg_file_rename, 因此我們直接檢視三引數版本的 SQL 程式碼

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_rename(text, text, text)
RETURNS bool
AS 'MODULE_PATHNAME', 'pg_file_rename_v1_1'
LANGUAGE C VOLATILE;

這個 SQL 程式碼直接呼叫來 C 函式 pg_file_rename_v1_1 來實現檔案重新命名。

現在我們來看一下 C 函式 pg_file_rename_v1_1

Datum
pg_file_rename_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file1;
	text	   *file2;
	text	   *file3;
	bool		result;

	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
		PG_RETURN_NULL();

	file1 = PG_GETARG_TEXT_PP(0);
	file2 = PG_GETARG_TEXT_PP(1);

	if (PG_ARGISNULL(2))
		file3 = NULL;
	else
		file3 = PG_GETARG_TEXT_PP(2);

	result = pg_file_rename_internal(file1, file2, file3);

	PG_RETURN_BOOL(result);
}

這個程式碼中僅僅是判斷引數是否為空,如果不為空,則獲取引數,然後呼叫 pg_file_rename_internal 這個函式

static bool
pg_file_rename_internal(text *file1, text *file2, text *file3)
{
	char	   *fn1,
			   *fn2,
			   *fn3;
	int			rc;

	fn1 = convert_and_check_filename(file1);
	fn2 = convert_and_check_filename(file2);

	if (file3 == NULL)
		fn3 = NULL;
	else
		fn3 = convert_and_check_filename(file3);

	if (access(fn1, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("file \"%s\" is not accessible: %m", fn1)));

		return false;
	}

	if (fn3 && access(fn2, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("file \"%s\" is not accessible: %m", fn2)));

		return false;
	}

	rc = access(fn3 ? fn3 : fn2, W_OK);
	if (rc >= 0 || errno != ENOENT)
	{
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_FILE),
				 errmsg("cannot rename to target file \"%s\"",
						fn3 ? fn3 : fn2)));
	}

	if (fn3)
	{
		if (rename(fn2, fn3) != 0)
		{
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn2, fn3)));
		}
		if (rename(fn1, fn2) != 0)
		{
			ereport(WARNING,
					(errcode_for_file_access(),
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn1, fn2)));

			if (rename(fn3, fn2) != 0)
			{
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not rename \"%s\" back to \"%s\": %m",
								fn3, fn2)));
			}
			else
			{
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_FILE),
						 errmsg("renaming \"%s\" to \"%s\" was reverted",
								fn2, fn3)));
			}
		}
	}
	else if (rename(fn1, fn2) != 0)
	{
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not rename \"%s\" to \"%s\": %m", fn1, fn2)));
	}

	return true;
}

這個函式的整體邏輯為,先將 text* 型別的資料轉換為 char* 型別的資料,會在這個轉換的過程中處理一些路徑相關和許可權驗證的問題。

然後先判斷一下 fn1 是不是存在,如果不存在那肯定是沒法重新命名的,然後判斷一下 fn3 是不是為空,並且 fn2 是不是存在,如果不存在,那麼將 fn2 重新命名為 fn3 也會失敗。

然後判斷一下 fn3 是不是存在,如果存在,那麼說明檔案已經存在,肯定不能重新命名,也會報一個 DUPLICATE 的錯誤。如果 fn3 為空,那麼就判斷 fn2 檔案是不是存在,如果存在那也是不能重新命名的。

接下來,我們就可以將 fn2 重新命名成 fn3, 然後將 fn1 重新命名為 fn2,

如果 fn1 重新命名為 fn2 出錯,則將 fn3 重名名為 fn2 ,即撤消之前的修改操作。

那如果 fn3 為空,直接將 fn1 重新命名為 fn2 就可以了,

pg_file_sync

我們先來看一下 SQL 程式碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_sync(text)
RETURNS void
AS 'MODULE_PATHNAME', 'pg_file_sync'
LANGUAGE C VOLATILE STRICT;

可以看到他呼叫的是 C 函式 pg_file_sync ,我們來看一下這個 C 程式碼:

Datum
pg_file_sync(PG_FUNCTION_ARGS)
{
	char	   *filename;
	struct stat fst;

	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));

	if (stat(filename, &fst) < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not stat file \"%s\": %m", filename)));

	fsync_fname_ext(filename, S_ISDIR(fst.st_mode), false, ERROR);

	PG_RETURN_VOID();
}

可以看到這個僅僅是將 text 型別的資料轉換為 char * 型別的資料。然後呼叫 storage 的函式實現的功能。(src/backend/storage/file/fd.c)

我們先來看一下 SQL 程式碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_unlink(text)
RETURNS bool
AS 'MODULE_PATHNAME', 'pg_file_unlink_v1_1'
LANGUAGE C VOLATILE STRICT;

發現它呼叫了 C 函式 pg_file_unlink_v1_1. 我們來看一下這個函式:

Datum
pg_file_unlink_v1_1(PG_FUNCTION_ARGS)
{
	char	   *filename;

	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));

	if (access(filename, W_OK) < 0)
	{
		if (errno == ENOENT)
			PG_RETURN_BOOL(false);
		else
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("file \"%s\" is not accessible: %m", filename)));
	}

	if (unlink(filename) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("could not unlink file \"%s\": %m", filename)));

		PG_RETURN_BOOL(false);
	}
	PG_RETURN_BOOL(true);
}

這個函式的整體邏輯是將 text* 型別的資料轉換為 char* 型別的資料,並處理路徑相關的問題,然後判斷一下檔案是不是可訪問的。然後呼叫 unlink 對檔案進行刪除。

pg_file_write

看一下 SQL 程式碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_file_write(text, text, bool)
RETURNS bigint
AS 'MODULE_PATHNAME', 'pg_file_write_v1_1'
LANGUAGE C VOLATILE STRICT;

發現它呼叫的是 C 函式 pg_file_write_v1_1

Datum
pg_file_write_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file = PG_GETARG_TEXT_PP(0);
	text	   *data = PG_GETARG_TEXT_PP(1);
	bool		replace = PG_GETARG_BOOL(2);
	int64		count = 0;

	count = pg_file_write_internal(file, data, replace);

	PG_RETURN_INT64(count);
}

/* ------------------------------------
 * pg_file_write_internal - Workhorse for pg_file_write functions.
 *
 * This handles the actual work for pg_file_write.
 */
static int64
pg_file_write_internal(text *file, text *data, bool replace)
{
	FILE	   *f;
	char	   *filename;
	int64		count = 0;

	filename = convert_and_check_filename(file);

	if (!replace)
	{
		struct stat fst;

		if (stat(filename, &fst) >= 0)
			ereport(ERROR,
					(errcode(ERRCODE_DUPLICATE_FILE),
					 errmsg("file \"%s\" exists", filename)));

		f = AllocateFile(filename, "wb");
	}
	else
		f = AllocateFile(filename, "ab");

	if (!f)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\" for writing: %m",
						filename)));

	count = fwrite(VARDATA_ANY(data), 1, VARSIZE_ANY_EXHDR(data), f);
	if (count != VARSIZE_ANY_EXHDR(data) || FreeFile(f))
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write file \"%s\": %m", filename)));

	return (count);
}

我們可以看到 pg_file_write_v1_1 僅僅是獲取了引數,然後就呼叫了 pg_file_write_internal 函式。
這個函式的主要邏輯是將 text* 的資料轉換為 char* 的資料 。然後判斷一下 replace 引數,如果為 false,則檔案不能存在,然後使用 AllocateFile 建立一個檔案。
然後使用 fwrite 將資料寫入檔案,VARDATA_ANY 宏的作用是獲取實際的資料指標,VARSIZE_ANY_EXHDR 的作用是獲取資料的長度。最後返回寫入的長度。

pg_logdir_ls

我們看一下 SQL 程式碼:

CREATE OR REPLACE FUNCTION pg_catalog.pg_logdir_ls()
RETURNS setof record
AS 'MODULE_PATHNAME', 'pg_logdir_ls_v1_1'
LANGUAGE C VOLATILE STRICT;

這裡它呼叫了 C 函式 pg_logidr_ls_v1_1,我們看一下這個 C 函式:

Datum
pg_logdir_ls_v1_1(PG_FUNCTION_ARGS)
{
	return (pg_logdir_ls_internal(fcinfo));
}

static Datum
pg_logdir_ls_internal(FunctionCallInfo fcinfo)
{
	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
	bool		randomAccess;
	TupleDesc	tupdesc;
	Tuplestorestate *tupstore;
	AttInMetadata *attinmeta;
	DIR		   *dirdesc;
	struct dirent *de;
	MemoryContext oldcontext;

	if (strcmp(Log_filename, "postgresql-%Y-%m-%d_%H%M%S.log") != 0)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("the log_filename parameter must equal 'postgresql-%%Y-%%m-%%d_%%H%%M%%S.log'")));

	/* check to see if caller supports us returning a tuplestore */
	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("set-valued function called in context that cannot accept a set")));
	if (!(rsinfo->allowedModes & SFRM_Materialize))
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("materialize mode required, but it is not allowed in this context")));

	/* The tupdesc and tuplestore must be created in ecxt_per_query_memory */
	oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);

	tupdesc = CreateTemplateTupleDesc(2);
	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starttime",
					   TIMESTAMPOID, -1, 0);
	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "filename",
					   TEXTOID, -1, 0);

	randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
	tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
	rsinfo->returnMode = SFRM_Materialize;
	rsinfo->setResult = tupstore;
	rsinfo->setDesc = tupdesc;

	MemoryContextSwitchTo(oldcontext);

	attinmeta = TupleDescGetAttInMetadata(tupdesc);

	dirdesc = AllocateDir(Log_directory);
	while ((de = ReadDir(dirdesc, Log_directory)) != NULL)
	{
		char	   *values[2];
		HeapTuple	tuple;
		char		timestampbuf[32];
		char	   *field[MAXDATEFIELDS];
		char		lowstr[MAXDATELEN + 1];
		int			dtype;
		int			nf,
					ftype[MAXDATEFIELDS];
		fsec_t		fsec;
		int			tz = 0;
		struct pg_tm date;

		/*
		 * Default format: postgresql-YYYY-MM-DD_HHMMSS.log
		 */
		if (strlen(de->d_name) != 32
			|| strncmp(de->d_name, "postgresql-", 11) != 0
			|| de->d_name[21] != '_'
			|| strcmp(de->d_name + 28, ".log") != 0)
			continue;

		/* extract timestamp portion of filename */
		strcpy(timestampbuf, de->d_name + 11);
		timestampbuf[17] = '\0';

		/* parse and decode expected timestamp to verify it's OK format */
		if (ParseDateTime(timestampbuf, lowstr, MAXDATELEN, field, ftype, MAXDATEFIELDS, &nf))
			continue;

		if (DecodeDateTime(field, ftype, nf, &dtype, &date, &fsec, &tz))
			continue;

		/* Seems the timestamp is OK; prepare and return tuple */

		values[0] = timestampbuf;
		values[1] = psprintf("%s/%s", Log_directory, de->d_name);

		tuple = BuildTupleFromCStrings(attinmeta, values);

		tuplestore_puttuple(tupstore, tuple);
	}

	FreeDir(dirdesc);
	return (Datum) 0;
}

這裡它直接呼叫了 pg_logdir_ls_internal 函式,其中的 fcinfo 是這個宏展開的:

#define PG_FUNCTION_ARGS FunctionCallInfo fcinfo

我們來看一下 pg_logdir_ls_internal 函式:

首先我們看到了它從 fcinfo 中獲取了一個 resultinfo 的欄位,然後將其轉換為 ReturnSetInfo 這個結構體指標。我們來看一下這個結構體

/*
 * When calling a function that might return a set (multiple rows),
 * a node of this type is passed as fcinfo->resultinfo to allow
 * return status to be passed back.  A function returning set should
 * raise an error if no such resultinfo is provided.
 */
typedef struct ReturnSetInfo
{
	NodeTag		type;
	/* values set by caller: */
	ExprContext *econtext;		/* context function is being called in */
	TupleDesc	expectedDesc;	/* tuple descriptor expected by caller */
	int			allowedModes;	/* bitmask: return modes caller can handle */
	/* result status from function (but pre-initialized by caller): */
	SetFunctionReturnMode returnMode;	/* actual return mode */
	ExprDoneCond isDone;		/* status for ValuePerCall mode */
	/* fields filled by function in Materialize return mode: */
	Tuplestorestate *setResult; /* holds the complete returned tuple set */
	TupleDesc	setDesc;		/* actual descriptor for returned tuples */
} ReturnSetInfo;

從這個註釋中我們可以看到,當我們呼叫的函式需要返回一個集合,即多行資料的時候,我們就可以使用這個結構體,而 resultinfo 的實際型別是一個 fmNodePtr ,實際上就是一個 Node 節點。而 ReturnSetInfo 也是一個 Node 節點。

接下來我們定義來一些用於返回結果需要的輔助資料結構。

第一個判斷我們的 Log_filename 的格式是不是符合 postgresql-%Y-%m-%d_%H%M%S.log 這個規則,不符合這個規則是無法使用這個函式的。Log_filename 是一個 GUC 引數,你可以在 postgresql.conf 中修改,也可以使用 show log_filename 命令來檢視當前的值。

然後我們檢查一下 rsinfo 變數的型別是不是一個 ReturnSetInfo,實際上就是透過 NodeTag 進行比較的。

接下來我們檢查一下 rsinfo 中的 allowedMode 返回模式是否是 SFRM_Materialize 這個模式可以讓我們將要返回的資料都儲存在 Tuplestore 中。

接下來我們需要將記憶體上下文切換到 rsinfo->econtext->ecxt_per_query_memory ,在這個記憶體上下文中我們儲存要返回的結果,即這裡的 tuple.

然後我們使用 CreateTemplateTupleDesc 來建立一個 tuple 的描述,其實我感覺可以理解為 DDL ,即這個表各個欄位是什麼樣的資料。這裡我們新增了兩個欄位,其資料型別分別為 TIMESTAMPTEXT.

然後我們使用 tuplestore_begin_heap 函式建立一個 tuplestore ,我們最終的結果也將存放在這個資料結構中。

attinmeta = TupleDescGetAttInMetadata(tupdesc); 這個語句把 tuple 的屬性資訊儲存到 attinmeta 這個會在我們後續構造 tuple 的時候要用到。

接下來,我們開啟目錄,這個 Log_directory 也是一個 GUC 引數可以設定和檢視。
這個目錄是相對於 data 目錄來說的。

接下來,我們遍歷目錄中的檔案,檢視檔案的名字是否滿足 postgresql- 這種形式,然後將檔名中包含的日期資訊提取出來,然後將其變成好看的日期格式,最後我們將日期和檔名寫入到一個 tuple 裡

values[0] = timestampbuf;
values[1] = psprintf("%s/%s", Log_directory, de->d_name);
tuple = BuildTupleFromCStrings(attinmeta, values);

其中的 attinmeta 就是我們之前準備好的 tuple 描述資訊,構造好一個 tuple 以後,我們就可以把它放在 tuplestore 裡邊了。

tuplestore_puttuple(tupstore, tuple);

最後關閉目錄。

相關文章