正确枚举 Winlogon 桌面窗口层次

news2024/9/20 8:10:23

目录

前言

原理解释

原理实现

 Winlogon 桌面窗口层次


本文出处链接:https://blog.csdn.net/qq_59075481/article/details/141608316。

前言

众所周知,从 Windows 7 开始,Winlogon 桌面不再使用 SASWindow 作为背景窗口,而是采用了一套新的安全桌面模式。我发现 CSDN 上在这方面的研究很少。

在我前面的几篇文章里面已经详细分析并实现了,编程化拦截与 Winlogon 有关的登陆事件(如 Ctrl + Alt + Delete 快捷键)。在之前,我浅谈过一些有关 LogonUI Interface 登陆 UI 界面的内容,但并未做详细的解释。这篇文章将就如何动态枚举 Winlogon 桌面的窗口层次并进行记录进行详细的讲解。如有错误,敬请点拨。

专栏文章:

快捷键机制系列文章 | 涟幽 516icon-default.png?t=N7T8https://blog.csdn.net/qq_59075481/category_12568641.html

原理解释

首先,我们需要知道 Winlogon 桌面(登陆桌面,运行在控制台会话)和 Default 桌面(用户默认桌面,运行在 UI 会话)均是由 Winlogon.exe 所创建的。

Winlogon.exe 是 Windows 操作系统中负责处理用户登录和注销等会话管理功能的进程。通常情况下,每个控制台会话(即本地会话)会有一个对应的 Winlogon.exe 实例,用于处理用户认证等任务。

UI 会话 是指用户交互的会话,通常指的是图形用户界面(GUI)环境下的桌面会话。每个用户登录到系统时,Windows 会为该用户分配一个会话 ID,并启动一个桌面环境来处理用户的输入和输出。

在多用户环境中,每个用户的 UI 会话可以与一个特定的控制台会话关联。控制台会话是指直接连接到物理屏幕、键盘和鼠标的会话(如通过直接本地登录),而其他 UI 会话则可能是通过远程桌面或其他方式登录的。

所以,用户一般接触最多的就是交互式用户会话和桌面,对于 Winlogon 桌面可能知之甚少。其实用户所熟知的 UAC 对话框,就是运行在 Winlogon 桌面下的系统程序所创建的窗口。

UAC 对话框界面

LogonUI.exe 进程:这是登陆用户交互式界面进程,Winlogon.exe 进程会在需要在登陆桌面下显示交互式界面时,启动该进程。并通过进程间通信(IPC)技术,如管道、RPC 等完成多进程同步事务。用户看到的登陆界面、CAD 界面、UAC 界面均最终由它启动。

LogonUI 运行时模块:LogonUI.exe 实质上是一个加载容器,用于装载运行时需要的接口模块。第一个加载的模块为:LogonUIController.dll。其他运行时模块通过延迟加载技术完成加载。

窗口工作站:窗口工作站是与进程关联的安全对象,包含一个或多个绑定的桌面对象、剪贴板等等。Winlogon 桌面和 Default 桌面默认运行在 Winsta0 窗口工作站下。进程必须以合适的访问权限打开窗口工作站,才能够打开指定的桌面访问句柄。进程每次获取的窗口工作站的访问句柄不同,桌面的访问句柄就不同。如果需要获知对象句柄的名称,则需要通过 WINAPI GetUserObjectInformation。

工作线程:进程连接到窗口工作站后,系统会将桌面分配给建立连接的线程。 

为了在运行时研究清楚 Winlogon 桌面有哪些窗口,就必须切换程序的工作线程然后枚举活动桌面的窗口。

登陆界面示例

频繁切换工作线程绑定的桌面是不被允许的,因为 SetThreadDesktop 之前不能有任何桌面窗口服务正在运作(微软说会造成安全问题)。在实际测试过程当中,我们发现连续第二次切换时就会引起失败。但是我们又需要进行动态监测,所以必须要每次都能够及时切换工作线程的桌面。解决方案很简单,就是每次切换时创建一个新的线程作为工作线程,然后在新的线程里面切换桌面。

然后,打开窗口工作站和桌面需要 SYSTEM 令牌的 Local System 权限,所以必须首先以管理员身份运行,然后再从 SYSTEM 进程复制模拟令牌来重新启动高权限进程。

然后,枚举窗口这边也很简单,通过 EnumWindows 和 EnumChildWindows 枚举并保存窗口层次。

至于桌面的切换检测,最简单的就是通过一个循环不断去获取当前活动桌面(ActiveDesktop),主要通过 OpenInputDesktop 来获取,然后分析桌面名称,当桌面切换到 Winlogon 桌面时切换线程并遍历桌面窗口,当桌面切换到 Default 桌面时,停止记录并保存日志;

原理实现

目前在管理员用户下运行一切正常(对于多用户登陆是否正常还未测试)。

#include <windows.h>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <ctime>
#include <sstream>
#include <codecvt>
#include <functional>
#include <tlhelp32.h>
#include <userenv.h>
#include <sddl.h>

// 记录窗口信息的结构体,使用宽字符
struct WindowInfo {
    std::wstring className;
    std::wstring windowTitle;
    HWND hwnd;
    std::vector<WindowInfo> children;
};

// 工作线程传递信息的结构体
struct MYTHREADINFO {
    HDESK hDesktop;
    std::vector<WindowInfo> wndInfo;
};

// 启用特定的权限(例如 SeDebugPrivilege)
bool EnablePrivilege(LPCWSTR privilege) {
    HANDLE hToken;
    TOKEN_PRIVILEGES tp;
    LUID luid;

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
        std::wcerr << L"Failed to open process token." << std::endl;
        return false;
    }

    if (!LookupPrivilegeValueW(NULL, privilege, &luid)) {
        std::wcerr << L"Failed to lookup privilege." << std::endl;
        CloseHandle(hToken);
        return false;
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
        std::wcerr << L"Failed to adjust token privileges." << std::endl;
        CloseHandle(hToken);
        return false;
    }

    if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
        std::wcerr << L"The privilege was not assigned." << std::endl;
        CloseHandle(hToken);
        return false;
    }

    CloseHandle(hToken);
    return true;
}

// 检查是否以管理员权限运行
bool IsRunAsAdmin() {
    BOOL isAdmin = FALSE;
    PSID administratorsGroup = NULL;
    SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;

    if (AllocateAndInitializeSid(&NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID,
        DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administratorsGroup)) {
        CheckTokenMembership(NULL, administratorsGroup, &isAdmin);
        FreeSid(administratorsGroup);
    }
    return isAdmin == TRUE;
}

// 重新启动并请求管理员权限
bool RelaunchAsAdmin() {
    wchar_t szPath[MAX_PATH];
    if (!GetModuleFileNameW(NULL, szPath, MAX_PATH)) {
        std::wcerr << L"Failed to get module file name." << std::endl;
        return false;
    }

    SHELLEXECUTEINFOW sei = { sizeof(sei) };
    sei.lpVerb = L"runas";  // 请求管理员权限
    sei.lpFile = szPath;
    sei.hwnd = NULL;
    sei.nShow = SW_NORMAL;

    if (!ShellExecuteExW(&sei)) {
        std::wcerr << L"Failed to relaunch as administrator." << std::endl;
        return false;
    }
    return true;
}

// 获取 winlogon 进程的 SYSTEM 令牌
HANDLE GetSystemTokenFromWinlogon() {
    HANDLE hToken = NULL;
    HANDLE hProcess = NULL;

    // 获取 Winlogon 进程的进程ID
    DWORD winlogonPid = 0;
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnapshot == INVALID_HANDLE_VALUE) {
        std::wcerr << L"Failed to create process snapshot!" << std::endl;
        return NULL;
    }

    if (Process32First(hProcessSnapshot, &pe32)) {
        do {
            if (_wcsicmp(pe32.szExeFile, L"winlogon.exe") == 0) {
                winlogonPid = pe32.th32ProcessID;
                break;
            }
        } while (Process32Next(hProcessSnapshot, &pe32));
    }
    CloseHandle(hProcessSnapshot);

    if (winlogonPid == 0) {
        std::wcerr << L"Failed to find winlogon.exe process!" << std::endl;
        return NULL;
    }

    // 打开 Winlogon 进程
    hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, winlogonPid);
    if (!hProcess) {
        std::wcerr << L"Failed to open winlogon process!" << std::endl;
        return NULL;
    }

    // 打开该进程的令牌
    if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hToken)) {
        std::wcerr << L"Failed to open process token!" << std::endl;
        CloseHandle(hProcess);
        return NULL;
    }

    CloseHandle(hProcess);
    return hToken;
}

// 创建具有 SYSTEM 权限的进程
bool CreateSystemProcess(LPCWSTR applicationName, LPCWSTR commandLine) {
    HANDLE hToken = GetSystemTokenFromWinlogon();
    if (!hToken) {
        std::wcerr << L"Failed to get SYSTEM token!" << std::endl;
        return false;
    }

    // 使用 SYSTEM 权限创建进程
    STARTUPINFOW si = { sizeof(STARTUPINFOW) };
    PROCESS_INFORMATION pi = { 0 };

    if (!CreateProcessAsUserW(hToken, applicationName, const_cast<LPWSTR>(commandLine), NULL,
        NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
        std::wcerr << L"Failed to create process as SYSTEM!" << std::endl;
        CloseHandle(hToken);
        return false;
    }

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    CloseHandle(hToken);
    return true;
}

// 检查当前进程是否具有 SYSTEM 权限
bool IsSystem() {
    HANDLE hToken = NULL;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
        return false;
    }

    DWORD tokenInfoLength = 0;
    GetTokenInformation(hToken, TokenUser, NULL, 0, &tokenInfoLength);

    PTOKEN_USER tokenUser = (PTOKEN_USER)malloc(tokenInfoLength);
    if (!GetTokenInformation(hToken, TokenUser, tokenUser, tokenInfoLength, &tokenInfoLength)) {
        CloseHandle(hToken);
        free(tokenUser);
        return false;
    }

    LPWSTR sidString = NULL;
    ConvertSidToStringSidW(tokenUser->User.Sid, &sidString);
    bool isSystem = (_wcsicmp(sidString, L"S-1-5-18") == 0);

    LocalFree(sidString);
    CloseHandle(hToken);
    free(tokenUser);

    return isSystem;
}

// 递归获取窗口层次,使用宽字符 API
void EnumChildWindowsRecursive(HWND hwndParent, std::vector<WindowInfo>& windowList) {
    wchar_t className[256];
    wchar_t windowTitle[256];

    ZeroMemory(className, sizeof(className));
    ZeroMemory(windowTitle, sizeof(windowTitle));

    // 获取窗口类名和标题
    GetClassNameW(hwndParent, className, sizeof(className) / sizeof(wchar_t));
    GetWindowTextW(hwndParent, windowTitle, sizeof(windowTitle) / sizeof(wchar_t));

    if (className[0] == L'\0') {
        wcscpy_s(className, L"(None)");
    }

    if (windowTitle[0] == L'\0') {
        wcscpy_s(windowTitle, L"(None)");
    }

    WindowInfo windowInfo = { className, windowTitle, hwndParent };

    // 枚举子窗口
    EnumChildWindows(hwndParent, [](HWND hwnd, LPARAM lParam) -> BOOL {
        std::vector<WindowInfo>* children = reinterpret_cast<std::vector<WindowInfo>*>(lParam);
        EnumChildWindowsRecursive(hwnd, *children);
        return TRUE;
        }, reinterpret_cast<LPARAM>(&windowInfo.children));

    windowList.push_back(windowInfo);
}

// 获取当前桌面窗口层次,使用宽字符 API
std::vector<WindowInfo> GetWindowHierarchy() {
    std::vector<WindowInfo> windows;
    EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {
        std::vector<WindowInfo>* windows = reinterpret_cast<std::vector<WindowInfo>*>(lParam);
        EnumChildWindowsRecursive(hwnd, *windows);
        return TRUE;
        }, reinterpret_cast<LPARAM>(&windows));
    return windows;
}

// 格式化时间为宽字符格式
std::wstring FormatTime(const std::time_t& time) {
    wchar_t timeBuffer[100];
    tm ti = {};
    localtime_s(&ti, &time);
    std::wcsftime(timeBuffer, sizeof(timeBuffer) / sizeof(wchar_t), L"%Y-%m-%d %H:%M:%S", &ti);
    return std::wstring(timeBuffer);
}

// 转换宽字符到 UTF-8
std::string WideToUTF8(const std::wstring& wideStr) {
    std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
    return conv.to_bytes(wideStr);
}

// 保存窗口层次信息到文件,使用 UTF-8 编码
void SaveWindowHierarchy(const std::vector<WindowInfo>& windows, const std::time_t& changeTime, const std::wstring& filename) {
    std::ofstream file(WideToUTF8(filename), std::ios::app | std::ios::binary);  // 使用 binary 防止换行符的意外转换
    if (!file.is_open()) {
        std::wcerr << L"Unable to open file for writing!" << std::endl;
        return;
    }

    // 在文件开头写入 BOM,标识为 UTF-8 编码
    static bool bomWritten = false;
    if (!bomWritten) {
        const unsigned char bom[] = { 0xEF, 0xBB, 0xBF };  // UTF-8 BOM
        file.write(reinterpret_cast<const char*>(bom), sizeof(bom));
        bomWritten = true;
    }

    // 格式化时间为字符串
    wchar_t timeBuffer[100];
    tm ti = {};
    localtime_s(&ti, &changeTime);
    std::wcsftime(timeBuffer, sizeof(timeBuffer) / sizeof(wchar_t), L"%Y-%m-%d %H:%M:%S", &ti);

    // 写入时间戳
    file << WideToUTF8(std::wstring(timeBuffer)) << "\n";

    // 递归写入窗口信息
    std::function<void(const std::vector<WindowInfo>&, int)> WriteHierarchy;
    WriteHierarchy = [&file, &WriteHierarchy](const std::vector<WindowInfo>& windows, int indent) {
        for (const auto& window : windows) {
            file << std::string(indent, ' ')  // 使用空格进行缩进
                << "Class Name: " << WideToUTF8(window.className)
                << ", Title: " << WideToUTF8(window.windowTitle)
                << ", HWND: " << std::hex << window.hwnd << "\n";
            if (!window.children.empty()) {
                WriteHierarchy(window.children, indent + 4);  // 递归写入子窗口信息
            }
        }
    };

    WriteHierarchy(windows, 0);
    file << "\n";
    file.close();
}

// 获取当前桌面的名称
std::wstring GetDesktopName(HDESK hDesktop) {
    wchar_t desktopName[256];
    DWORD neededLength = 0;

    if (!GetUserObjectInformationW(hDesktop, UOI_NAME, desktopName, sizeof(desktopName), &neededLength)) {
        std::wcerr << L"Failed to get desktop name." << std::endl;
        return L"";
    }

    return std::wstring(desktopName);
}


// 切换到指定桌面并枚举窗口,任务结束后恢复到原始桌面
std::vector<WindowInfo> GetWindowsFromDesktop(HDESK hDesktop) {
    // 获取原始桌面句柄
    HDESK hOriginalDesktop = GetThreadDesktop(GetCurrentThreadId());

    // 切换到目标桌面
    if (!SetThreadDesktop(hDesktop)) {
        std::wcerr << L"Failed to set thread desktop!" << std::endl;
        return {};
    }

    // 切换后获取窗口层次
    std::vector<WindowInfo> windows = GetWindowHierarchy();

     恢复到原始桌面
    //if (!SetThreadDesktop(hOriginalDesktop)) {
    //    std::wcerr << L"Failed to restore original desktop!" << std::endl;
    //}

    return windows;
}

// 使用辅助线程进行桌面切换和窗口枚举
DWORD WINAPI MonitorDesktopThread(LPVOID param) {
    _wsetlocale(LC_ALL, L"zh-CN");
    MYTHREADINFO* threadInfo = static_cast<MYTHREADINFO*>(param);

    threadInfo->wndInfo = GetWindowsFromDesktop(threadInfo->hDesktop);

    return 0;
}

// 打开窗口工作站并切换到桌面
HDESK OpenDesktopWithWindowStation(LPCWSTR desktopName, HWINSTA hWinsta) {
    // 打开桌面
    HDESK hDesktop = OpenDesktopW(desktopName, 0, FALSE, GENERIC_ALL);
    if (!hDesktop) {
        std::wcerr << L"Failed to open desktop! name = " << desktopName << std::endl;
        CloseWindowStation(hWinsta);
    }

    return hDesktop;
}

// 获取当前活动桌面
HDESK GetActiveDesktop() {
    return OpenInputDesktop(0, FALSE, GENERIC_ALL);
}

// 监控桌面切换
void MonitorDesktop() {

    // 打开窗口工作站
    HWINSTA hWinsta = OpenWindowStationW(L"WinSta0", FALSE, GENERIC_READ | GENERIC_WRITE);
    if (!hWinsta) {
        std::wcerr << L"Failed to open window station!" << std::endl;
        return;
    }

    // 将当前线程关联到工作站
    if (!SetProcessWindowStation(hWinsta)) {
        std::wcerr << L"Failed to set process window station!" << std::endl;
        CloseWindowStation(hWinsta);
        return;
    }

    HDESK hDefaultDesk = OpenDesktopWithWindowStation(L"Default", hWinsta);
    HDESK hWinlogonDesk = OpenDesktopWithWindowStation(L"Winlogon", hWinsta);

    if (!hDefaultDesk || !hWinlogonDesk) {
        std::wcerr << L"Failed to open desktops!" << std::endl;
        return;
    }

    std::wcout << L"Monitoring desktop changes (SYSTEM privileges detected)..." << std::endl;
    std::wcout << L"Desktops: Winlogon(" << std::hex << (UINT64)hWinlogonDesk << L"), Default("
        << std::hex << (UINT64)hDefaultDesk << L")." << std::endl;
    
    // 桌面监控代码

    std::vector<WindowInfo> windowHistory;
    bool monitoring = false;

    while (true) {
        // 检查指定按键是否被按下(比如 ESC 键,键码 0x1B)
        if (GetAsyncKeyState(VK_ESCAPE) & 0x8000) {
            std::wcout << L"Escape key pressed. Exiting monitoring..." << std::endl;
            break;  // 退出循环,结束监控
        }

        HDESK hCurrentDesk = GetActiveDesktop();   // 获取活动桌面句柄

        std::wstring desktopName = GetDesktopName(hCurrentDesk);   // 获取桌面名称

        //std::wcout << L"Current Desktop: " << desktopName << std::endl;

        if (desktopName == L"Winlogon" && !monitoring) {
            std::wcout << L"Switched to Winlogon desktop. Start monitoring window hierarchy..." << std::endl;
            monitoring = true;
            windowHistory.clear();  // 清空之前的记录

            // 切换到 winlogon 桌面并枚举窗口
            MYTHREADINFO info = { hWinlogonDesk };
            HANDLE hThread = CreateThread(NULL, 0, MonitorDesktopThread, &info, 0, NULL);
            if (hThread) {
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }
            windowHistory = info.wndInfo;
        }
        else if (desktopName == L"Default" && monitoring) {
            std::wcout << L"Switched back to Default desktop. Stopping monitoring..." << std::endl;
            std::time_t currentTime = std::time(nullptr);
            SaveWindowHierarchy(windowHistory, currentTime, L"D:\\window_hierarchy.log");
            monitoring = false;

            // 显示历史记录
            MessageBoxW(NULL, L"The window hierarchy log has been saved. Check window_hierarchy.log for details.",
                L"History Saved", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);
        }

        if (monitoring) {
            // 切换到 winlogon 桌面枚举窗口
            MYTHREADINFO info = { hWinlogonDesk };
            HANDLE hThread = CreateThread(NULL, 0, MonitorDesktopThread, &info, 0, NULL);
            if (hThread) {
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }

            if (!info.wndInfo.empty()) {
                windowHistory = info.wndInfo;
            }
        }

        Sleep(1000);  // 每秒检查一次
    }

    CloseDesktop(hDefaultDesk);
    CloseDesktop(hWinlogonDesk);
}

int wmain(int argc, wchar_t* argv[]) {
    _wsetlocale(LC_ALL, L"zh-CN");
    // 检查是否为管理员权限运行
    if (!IsRunAsAdmin()) {
        std::wcout << L"Attempting to restart with administrator privileges..." << std::endl;
        if (RelaunchAsAdmin()) {
            return 0;  // 提升后进程将重新启动,当前进程结束
        }
        else {
            std::wcerr << L"Failed to relaunch as administrator." << std::endl;
            return 1;
        }
    }

    // 启用 SeDebugPrivilege
    if (!EnablePrivilege(SE_DEBUG_NAME)) {
        std::wcerr << L"Failed to enable SeDebugPrivilege." << std::endl;
        return 1;
    }

    // 检查 SYSTEM 权限
    if (!IsSystem()) {
        std::wcout << L"Attempting to restart with SYSTEM privileges..." << std::endl;

        // 检查命令行参数,避免无限递归
        if (argc < 2 || _wcsicmp(argv[1], L"system") != 0) {
            // 重新启动自身并传递 "system" 参数
            wchar_t commandLine[MAX_PATH];
            swprintf(commandLine, MAX_PATH, L"%s system", argv[0]);
            if (CreateSystemProcess(argv[0], commandLine)) {
                std::wcout << L"Restarted with SYSTEM privileges." << std::endl;
            }
            else {
                std::wcerr << L"Failed to restart with SYSTEM privileges." << std::endl;
            }
            return 0;
        }
        else {
            std::wcerr << L"Already tried to elevate privileges but failed." << std::endl;
            return 1;
        }
    }

    // 如果当前进程已经是 SYSTEM 权限,则继续执行桌面监控
    MonitorDesktop();
    return 0;
}

编译时设置监视器高 DPI 识别,和以管理员身份启动。

运行效果如下:

测试截图

日志记录存在放在 "D:\window_hierarchy.log" 文件中。

日志记录样例

 Winlogon 桌面窗口层次

Winlogon 桌面默认是没有窗口的,所以是黑色背景。当执行任务时,具有窗口。当在 UAC 界面时,背景窗口是 Credential Dialog Xaml Host 窗口;当在 CAD 或登陆界面时,背景窗口是 LogonUI Logon Window。

备注:由于博主时间有限,更多信息请自行探索。


本文出处链接:https://blog.csdn.net/qq_59075481/article/details/141608316。 

本文发布于:2024.08.27;更新于:2024.08.27。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2080190.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

翻斗雨量监测站

翻斗雨量监测站通常用于测量和记录降雨量&#xff0c;其主要功能包括&#xff1a; 测量降雨量&#xff1a;翻斗雨量监测站使用翻斗式测量原理&#xff0c;通过记录翻斗倒转的次数或翻斗中积累的水量来测量降雨量。可以准确地记录降雨量的变化。 记录降雨时间&#xff1a;翻斗雨…

PowerDesigner生成数据字典文档

PowerDesigner生成数据字典文档 目录 1. 设置报告 2. 导出报告 3. 查看报告 设置报告 删除多余的选项&#xff0c;只保留【LIst of Table Columns -表%PARENT%的栏的清单】选项。 只显示Name、Code、Data Type、Length、Is Key等列 导出报告 查看报告

如何用Java SpringBoot+Vue构建房产信息管理系统?详解开发流程

&#x1f393; 作者&#xff1a;计算机毕设小月哥 | 软件开发专家 &#x1f5a5;️ 简介&#xff1a;8年计算机软件程序开发经验。精通Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等技术栈。 &#x1f6e0;️ 专业服务 &#x1f6e0;️ 需求定制化开发源码提…

互联网应用主流框架整合之Spring缓存机制和Redis结合

Redis和数据库的结合 在实际的商用软件使用中&#xff0c;通常都是Redis和关系型数据配置使用&#xff0c;单纯使用Redis来存数据成本太高&#xff0c;并且其持久化和计算能力偏差&#xff0c;这两块无法和关系型数据相比较&#xff0c;而Redis和关系型数据库共存的场景就会带…

C++必修:set与map的模拟实现

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 1. set与map的结构 我们知道STL中的set与map底层就是一颗红黑树&#xff0c;接下…

如何在Spring中为`@Value`注解设置默认值

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

如何处理在学校Linux连接不上服务器

一、问题描述 当我们在周末在图书馆背着室友偷偷学习时&#xff0c;准备好好学习Linux&#xff0c;争取在日后大展拳脚时&#xff0c;却突然尴尬的发现&#xff0c;连接不上服务器&#xff0c;总是出现以下画面&#xff1a; 那么&#xff0c;我们该如何解决问题呢&#xff1f; …

螺杆支撑座与滚珠丝杆的精准适配!

螺杆支撑座与滚珠丝杆的适配是确保机械系统的稳定性、精度和耐用性的关键&#xff0c;其适配方法主要包括螺纹连接、联轴器连接、锁紧连接。 螺杆支撑座种类多样&#xff0c;每种类型都有其特定的适用范围和性能特点。因此&#xff0c;根据滚珠丝杆的规格和应用需求&#xff0c…

Python接口测试之如何使用requests发起请求例子解析

在Python中&#xff0c;使用requests库发起HTTP请求是一种常见的接口测试方法。以下是一些使用requests库的基本示例&#xff0c;涵盖了GET、POST、PUT、DELETE等HTTP方法。 安装requests库 首先&#xff0c;确保你已经安装了requests库。如果未安装&#xff0c;可以通过以下…

【系统分析师】-案例篇-数据库

1、分布式数据库 1&#xff09;请用300字以内的文字简述分布式数据库跟集中式数据库相比的优点。 &#xff08;1&#xff09;坚固性好。由于分布式数据库系统在个别结点或个别通信链路发生故障的情况下&#xff0c;它仍然可以降低级别继续工作&#xff0c;系统的坚固性好&…

线程:线程创建pthread_create,线程间的同步与互斥

线程的创建 线程的创建是通过调用pthread_create函数来实现的。该函数的原型如下&#xff1a; int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);参数说明&#xff1a; thread&#xff1a;指向pthread_t类型…

开源word文档相似度对比 软件WinMerge

WinMerge 官网下载 &#xff1a;GitHub - WinMerge/winmerge: WinMerge is an Open Source differencing and merging tool for Windows. WinMerge can compare both folders and files, presenting differences in a visual text format that is easy to understand and hand…

ros2_python编程_多个文件python打包_目录拷贝_解决import错误问题ModuleNotFoundError

1.问题 ros2 python编写程序, 有多个python文件 如何打包多个python文件?解决import错误问题如何打包 有python目录结构的工程 1.ros2 多个python文件示例 代码目录结构, gitee 在线代码 tree 7_multi_file_setup/ 7_multi_file_setup/ ├── file1.py ├── main_node.…

飞书怎么关联任意两段话

最近开始用飞书记文档&#xff0c;体验实在是非常的丝滑&#xff0c;对我来说感觉没有找到更好的竞品了。废话不多说&#xff0c;接下来简单介绍一下怎么关联任意两段话吧。 首先说明&#xff0c;关联可以单向&#xff0c;也可以双向。 直接举例。 我想要将蓝字关联到最下面的…

国标GB28181视频监控EasyCVR视频汇聚平台国标注册被陌生IP入侵如何处理?

GB28181国标/GA/T1400协议/安防综合管理系统EasyCVR视频汇聚平台能在复杂的网络环境中&#xff0c;将前端设备统一集中接入与汇聚管理。智慧安防/视频存储/视频监控/视频汇聚EasyCVR平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级…

Java基础(包装类)

文章目录 前言 一、包装类的概述 二、自动拆装箱 三、128陷阱&#xff08;面试重点&#xff09; 四、自动拆装箱例题分析 前言 该篇文章创作时参考查阅了如下文章 Java种的包装类 Java包装类&#xff08;自动拆装箱&#xff09; Java--自动拆箱/装箱/实例化顺序/缓存…

第三期书生大模型实战营之茴香豆工具实践

文章目录 基础任务作业记录1. 环境准备2. 模型准备3. 修改配置文件4. 知识库创建6. 启动茴香豆webui 基础任务 在 InternStudio 中利用 Internlm2-7b 搭建标准版茴香豆知识助手&#xff0c;并使用 Gradio 界面完成 2 轮问答&#xff08;问题不可与教程重复&#xff0c;作业截图…

IDEA2023版本创建SSM项目框架

按图中红色数字顺序&#xff0c;先点击Maven&#xff0c;设置该项目为maven构建管理的项目&#xff0c;然后点击create进行项目创建 配置该项目的相关maven信息&#xff0c;按下图顺序进入到maven配置页面后进行本地maven相关信息配置。 创建web模块依次按下图中顺序进行点击 配…

朴世龙院士团队《Global Change Biology 》精确量化全球植被生产力对极端温度的响应阈值!

本文首发于“生态学者”微信公众号&#xff01; 随着全球气候变暖的加剧&#xff0c;极端温度事件对陆地生态系统的影响日益显著。植被作为生态系统的重要组成部分&#xff0c;其生产力对温度变化的响应尤为敏感。然而&#xff0c;关于极端温度如何以及在何种程度上影响植被生产…

TCP三次握手过程详解

三次握手过程&#xff1a; 客户端视角&#xff1a; 1.客户端调用connect&#xff0c;开启计时器&#xff0c;发送SYN包&#xff0c;如果重传超时&#xff0c;认为连接失败 2.如果收到服务端的ACK&#xff0c;则进入ESTABLISHED状态 3.清除重传计时器&#xff0c;发送ACK&…