윈도우 환경에서 여러 프로세스나 스레드가 동시에 동일한 파일에 접근하려고 할 때 발생하는 공유 위반 오류는 실무에서 꽤 자주 마주치는 상황이더라고요. 특히 어떤 프로그램이 파일을 점유하고 있는지 파악하기 어려운 상태에서 파일 읽기나 쓰기를 시도하면 시스템이 접근을 거부하며 예외를 던지는 경우가 많네요. 이런 문제를 해결하려면 단순히 파일 경로가 맞는지 확인하는 것을 넘어 시스템 수준에서 정의된 공유 모드인 FileShare 설정을 정확히 이해하고 적용해야 하더라고요.

using System.IO;

// 다른 프로세스가 읽기 권한을 가질 수 있도록 허용하며 파일을 여는 예시입니다.
FileStream fs = new FileStream("data_log.txt", 
    FileMode.OpenOrCreate, 
    FileAccess.ReadWrite, 
    FileShare.ReadWrite);

위 코드에서 사용된 FileShare 옵션은 현재 내 프로세스가 파일을 열 때 다른 프로세스에게 어떤 권한을 허용할 것인지를 결정하는 중요한 역할을 수행하더라고요. 기본적으로 아무런 옵션을 주지 않아도 FileStream은 FileShare.Read를 기본값으로 사용하기 때문에, 다른 프로세스의 읽기 접근은 이미 허용되어 있어요. 실제로 독점 모드로 파일을 열려면 FileShare.None을 명시적으로 지정해야 하며, 이 부분은 Win32 CreateFile API의 dwShareMode=0 기본값과 혼동하기 쉬운 부분이에요. 만약 내가 파일을 수정하는 동안에도 다른 쪽에서 동시 쓰기가 가능해야 한다면 FileShare.Write나 FileShare.ReadWrite를 추가해 주어야 하더라고요.

여기서 주의할 점은 윈도우 시스템의 파일 권한과 이 공유 모드는 서로 다른 개념이라는 점이에요. 사용자 계정 수준에서 부여된 보안 정책인 ACL이 허용되어 있더라도, 이미 다른 프로세스가 파일을 열고 있는 상태라면 FileShare 설정이 올바르지 않을 경우 접근이 거부될 수 있어요. 따라서 멀티 프로세스 환경이나 네트워크 드라이브를 사용하는 환경에서는 단순히 파일 경로에 접근하는 것이 아니라, 서비스의 요구사항에 맞춰 어떤 수준의 동시성을 허용할 것인지 고민하고 그에 맞는 Flag를 선택하는 과정이 꼭 필요해 보여요.

결국 공유 위반 오류가 발생했을 때 당황하지 말고 현재 프로그램이 파일을 점유하는 방식과 다른 프로세스가 개입할 여지를 얼마나 남겨둘지 검토하면 문제를 해결할 수 있더라고요. 상황에 맞는 적절한 FileShare 옵션을 선택함으로써 안정적인 파일 입출력 환경을 구축할 수 있는 것 같아요.

윈도우 운영체제에서 프로그램이 설정 파일을 공유할 때 환경 변수를 활용하는 경우가 많습니다. 보통 개발 중에는 임시로 세팅해 놓으면 바로 적용되지만, 재부팅 시 초기화되거나 다른 사용자 계정에 반영되지 않아 설정 관리가 번거로운 경우가 있습니다. 이런 경우 레지스트리를 직접 건드리거나 setx 명령어를 쓰게 되는데요, 여기서는 영구 저장과 동시에 실행 중인 프로그램에 즉시 알림을 전달하는 방법을 정리해 봤습니다.

먼저 환경 변수를 시스템 또는 사용자 단위에 저장하려면 reg add 명령어나 setx 도구를 사용합니다. 사용자 계정에만 반영하려면 HKEY_CURRENT_USER\Environment 키를 사용해야 하고, 전체 컴퓨터에 적용하려면 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment 를 건드려야 합니다. PATH 같은 기존 값에 새 경로를 붙이려면 setx /M PATH "%PATH%;C:\custom\bin" 형식으로 실행하면 레지스트리에 직접 기록되더라고요. 다만 레지스트리만 수정해도 현재 실행 중인 프로세스는 새로운 값을 가져오지 못합니다.

setx /M PATH "%PATH%;C:\custom\bin"

그래서 바로 적용하려면 WM_SETTINGCHANGE 메시지를 전체 윈도우에 브로드캐스트해야 합니다. SendMessageW API를 호출하면서 lParam 에 문자열 Environment 를 전달하면 됩니다. C++ 기준으로 구현하면 가장 간단하거든요. HWND_BROADCAST 를 첫 번째 인자로 넣고, 메시지 코드 WM_SETTINGCHANGE, WParam 은 0, LParam 은 "Environment" 포인터로 설정한 뒤 SendMessageTimeout 으로 실행합니다. timeout 은 보통 1000 밀리초 정도면 충분해요. 이때 SMTO_ABORTIFHUNG 플래그를 함께 주면 응답 없이 멈춘 윈도우가 전체 메시지를 가로채지 않습니다.

#include <windows.h>
#include <strsafe.h>

void BroadcastEnvChange(const wchar_t* lpszSection) {
    SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 
                        (LPARAM)lpszSection, SMTO_ABORTIFHUNG, 1000, nullptr);
}

// 호출 예시
BroadcastEnvChange(L"Environment");

이렇게 하면 레지스트리 수정 직후 실행 중인 탐색기나 타사 프로그램도 새 경로를 인식하게 됩니다. 특히 설치형 애플리케이션이나 서비스 개발 시 재시작 없이 설정을 적용해야 할 때 유용하더라고요. 단, setx 는 1024 자를 초과하는 문자열을 저장하려 하면 잘라내며, 긴 PATH 등은 reg add로 REG_EXPAND_SZ 형식으로 직접 기록하는 것이 안전합니다. 필요할 때마다 레지스트리 세팅과 메시지 브로드캐스트를 조합해 사용하면 되겠습니다.

최근 설정 파일 동기화나 모놀리식 앱 분리 작업을 하다가 프로세스 간 데이터 주고받이가 필요해졌거든요. 보통 소켓이나 메시 큐를 떠올리지만, 로컬에서만 돌아가는 도구라면 윈도우의 네임드 파이프가 가장 가볍고 안정적이더라고요. 처음엔 CreateNamedPipe로 서버를 만들고 클라이언트가 CreateFile로 연결하는 줄 알았는데, 보안 기술자 설정을 빼먹으면 오류가 날 거라 걱정했는데 알고 보니 같은 사용자 세션에서 로컬 테스트할 때는 기본값으로도 정상적으로 연결이 되더라고요. 저도 문서만 보고 넘어갔다가 며칠째 디버깅에 시간을 쏟은 적이 있어요.

서버는 파이프 핸들을 생성한 뒤 ConnectNamedPipe에서 클라이언트 연결을 기다리면 되고, 클라이언트는 파일처럼 열어서 읽고 쓰면 됩니다. 여기서 진짜 주의할 점은 보안 기술자를 무조건 채워야 한다고 생각하기 쉽다는 거예요. 실제로 기본 보안 설정만으로도 동일 사용자 환경에서는 충분히 접근이 가능하고, 크로스 컨텍스트나 다른 계정 간 접근이 필요할 때만 SECURITY_ATTRIBUTES를 직접 지정해 주면 돼요. 비동기 모드로 만들면 OVERLAPPED 구조체와 이벤트 객체를 함께 관리해야 하는데, 동기 방식으로 풀어도 실무에서는 코드 가독성이 훨씬 나아지더라고요. 메시지 유형을 PIPE_TYPE_MESSAGE로 지정해 주면 패킷 단위로 끊기지 않게 받을 수 있어요.

HANDLE hPipe = CreateNamedPipe(
    L"\\.\pipe\MyLocalPipe",
    PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1, 4096, 4096, 0, NULL);

if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED)
{
    // 연결 성공 시 데이터 읽고 쓰기
}

파이프가 끊기면 GetLastError로 ERROR_BROKEN_PIPE를 체크해서 재연결 로직을 넣으면 되거든요. 처음엔 핸들 수명 관리와 연결 상태 체크가 헷갈렸지만, 일단 기본 템플릿을 잡고 보안 컨텍스트만 상황에 맞게 잡아두면 이후에 메시지 직렬화나 타임아웃 설정도 훨씬 수월해지더라고요. 윈도우 로컬 IPC는 생각보다 간단하게 풀리니 필요할 때 바로 참고하시면 좋을 것 같아요.

윈도우에서 .ps1 스크립트를 하나 만들어 돌려보려는데 빨간 글씨로 cannot be loaded because running scripts is disabled on this system 이 뜨면서 막힌 적 있으실 거예요. 저도 처음엔 스크립트에 문법 오류가 있나 하고 한참 들여다봤는데, 알고 보니 코드 문제가 아니라 PowerShell의 실행 정책(Execution Policy) 때문이더라고요.

윈도우 클라이언트(10, 11)의 기본 실행 정책이 Restricted라서 그렇습니다. 이 상태에서는 콘솔에 직접 치는 명령어는 잘 실행되는데 .ps1 파일은 아예 안 돌아가요. 반대로 Windows Server 계열은 기본값이 RemoteSigned라, 서버에서는 멀쩡히 돌던 스크립트가 내 노트북에서만 막히는 황당한 상황도 자주 생깁니다.

그래서 뭘 고치기 전에 지금 정책이 어떻게 걸려 있는지부터 보는 게 좋아요. Get-ExecutionPolicy -List 를 쓰면 스코프별로 어떤 값이 적용돼 있는지 한눈에 들어옵니다.

# 현재 실행 정책 확인 (스코프별)
Get-ExecutionPolicy -List

# 현재 사용자에게만 RemoteSigned 적용 (관리자 권한 없이 가능)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

# 지금 켜 둔 세션에서만 임시로 허용
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

# 딱 한 번만 실행하고 끝낼 때
powershell -ExecutionPolicy Bypass -File .\deploy.ps1

# 다운로드한 스크립트의 차단 표시 제거 (내용 확인 후에)
Unblock-File .\script.ps1

가장 무난한 선택지는 CurrentUser 스코프에 RemoteSigned를 주는 방식이에요. 관리자 권한이 없어도 되고, 내가 직접 만든 로컬 스크립트는 그냥 돌아가면서 인터넷에서 받아온 스크립트는 서명이 없으면 계속 막아주거든요. 답답한 마음에 LocalMachine 스코프에 Unrestricted를 걸어버리고 싶어질 수도 있는데, 그건 시스템 전체의 방어선을 내려버리는 거라 웬만하면 피하는 게 좋습니다.

한 가지 더 짚어두면, RemoteSigned로 바꿨는데도 인터넷에서 받은 스크립트가 계속 막히는 경우가 있어요. 다운로드한 파일에는 Zone.Identifier라는 표시가 붙는데, PowerShell이 이걸 보고 외부에서 온 파일로 판단하기 때문입니다. 그래서 내용을 직접 열어서 이상한 게 없는지 확인한 다음에 Unblock-File로 표시를 떼주면 실행돼요. 순서가 중요합니다. 확인이 먼저고 Unblock이 나중이에요.

그리고 스코프에는 우선순위가 있는데, 그룹 정책으로 내려오는 MachinePolicy와 UserPolicy가 가장 셉니다. 회사 PC에서 분명히 Set-ExecutionPolicy를 실행했는데도 아무것도 안 바뀐다면 십중팔구 그룹 정책이 위에서 덮어쓰고 있는 거예요. 이럴 땐 혼자 붙잡고 씨름하지 말고 Get-ExecutionPolicy -List 결과부터 확인한 뒤 관리자에게 문의하는 편이 훨씬 빠릅니다.

출처: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies

출처: https://zeliard.tistory.com/entry/win10-11-check-capacity-of-each-folder

PS (Power Shell) 관리자 모드에서 실행

$unitFactor = 1MB
$unitLabel = "MB"
Get-ChildItem . -Directory | ForEach-Object {
    $folderPath = $_.FullName
    $folderSize = (Get-ChildItem -Path $folderPath -Recurse -File | Measure-Object -Property Length -Sum).Sum
    [PSCustomObject]@{
        FolderName = $_.Name
        FolderSize = [Math]::Round($folderSize / $unitFactor, 2)
        Unit = $unitLabel
    }
} | Sort-Object FolderSize -Descending | Format-Table -AutoSize

이미지가 포함되어 있는 HTML파일을 MHTML파일로 저장시 이미지의 경로에 한글이 포함되면 이미지가 정상적으로 저장되지 않는다.

이미지를 포함해서 MTHML파일로 저장시에는 꼭 영문 경로로 된 곳에 이미지 파일을 놓고 HTML파일에 삽입시켜야 한다.


클래스 라이브러리를 웹에서 다운 받은 후 동적로딩하면 더 좋지 않을까? 란 생각에 다운로드받는 부분도 찾아봤다..

        private void Download(string strModule)
        {
            try {
                WebRequest ttt = WebRequest.Create("http://indra17.mireene.com/Test/" + strModule + ".dll");
                WebResponse mmm = ttt.GetResponse();
                Stream kkk = mmm.GetResponseStream();

                int BUFFER = 1024;
                BinaryWriter bw = null;
                BinaryReader br = new BinaryReader(kkk);
                try
                {
                    Byte[] data = new byte[BUFFER];
                    FileStream fs = new FileStream(System.IO.Path.Combine(m_strSavePath, strModule + ".dll"), FileMode.Create);

                    long total = 0;
                    int recv_size = 0;

                    long prg_value = 0;
                    long time = 0;

                    try
                    {
                        bw = new BinaryWriter(fs);

                        while (true)
                        {
                            recv_size = br.Read(data, 0, BUFFER);

                            if (recv_size <= 0) break;

                            bw.Write(data, 0, recv_size);

                            total += recv_size;
                        }
                        Console.WriteLine("Download Size = {0}", total);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.ToString());
                    }
                    finally
                    {
                        if (bw != null) bw.Close();
                        if (br != null) br.Close();
                    }
                    fs.Close();

                    kkk.Close();
                    mmm.Close();
                } catch (ArgumentNullException ane) {
                    Console.WriteLine("ArgumentNullException : {0}",ane.ToString());
                } catch (SocketException se) {
                    Console.WriteLine("SocketException : {0}",se.ToString());
                } catch (Exception e) {
                    Console.WriteLine("Unexpected exception : {0}", e.ToString());
                }
            } catch (Exception e) {
                Console.WriteLine( e.ToString());
            }
       }

// 클래스 라이브러리 로딩
Assembly assem = Assembly.LoadFile(System.IO.Path.Combine(m_strSavePath, strModule + ".dll"));

//타입 얻기
string strType = strModule.Substring(0, strModule.IndexOf("Library"));
//Type targetType = assem.GetType(strModule + "." + strType);
Type targetType = assem.GetType(strModule + "." + strType);

//객체 생성 - 디폴트 생성자 호출
Object obj = assem.CreateInstance(targetType.FullName);

//멤버에 값 설정 - 마지막 인수 5가 멤버에 값을 설정하는 부분 입니다.
//바인딩 플래그의 마지막이 셋필드임을 확인하실 수 있을 겁니다.
try
{
        strRes = (string)targetType.InvokeMember(strMethod,
        BindingFlags.InvokeMethod | BindingFlags.Instance |
        BindingFlags.Public | BindingFlags.Static, null, obj, new string[] { strParam });
}
catch(Exception e) {
        Console.WriteLine(e.ToString());
}



참고.
http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=17&MAEULNo=8&no=82904&ref=82872

MSDN 검색
reflection, Dynamically Loading and Using Types


http://mixy.egloos.com/953406

    /// <summary>
    /// See Internet SDK, IObjectSafety.
    /// </summary>
    [Flags]
    public enum ObjectSafetyFlags : int
    {
        /// <summary>

        /// Caller of interface may be untrusted
        /// </summary>
        INTERFACESAFE_FOR_UNTRUSTED_CALLER = 1,

        /// <summary>
        /// Data passed into interface may be untrusted
        /// </summary>
        INTERFACESAFE_FOR_UNTRUSTED_DATA = 2,

        /// <summary>
        /// Object knows to use IDispatchEx.
        /// </summary>
        INTERFACE_USES_DISPEX = 4,

        /// <summary>
        /// Objects knows to use IInternetHostSecurityManager.
        /// </summary>
        INTERFACE_USES_SECURITY_MANAGER = 8,

        /// <summary>
        /// Flags combination.
        /// </summary>
        SafeForScripting = INTERFACESAFE_FOR_UNTRUSTED_CALLER |
        INTERFACESAFE_FOR_UNTRUSTED_DATA
    }

    [ComImport]
    [Guid("CB5BDC81-93C1-11cf-8F20-00805F3CD064")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
        void GetInterfaceSafetyOptions(ref Guid riid, out int supportedOptions, out int enabledOptions);
        void SetInterfaceSafetyOptions(ref Guid riid, int optionSetMask, int enabledOptions);
    }
    [Guid("5E603C7D-4B7C-412a-BAB4-064E5674F97A")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface IActiveXControl_Method
    {
        [DispId(1)]
        string dispFunction(string str);
    }

[ProgId("TestServer.ActiveX")]
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    [Guid("D2BD9E76-C60B-4724-B44E-986132D8FA7A")]
    public partial class Server : UserControl, IActiveXControl_Method, IObjectSafety
    {
        #region For Registration
        [ComRegisterFunction()]
        public static void RegisterClass(string key)
        {
            //Strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it
            StringBuilder sb = new StringBuilder(key);
            sb.Replace(@"HKEY_CLASSES_ROOT\", "");

            //Open the CLSID\{guid} key for write access
            RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);

            //And create the 'Control' key - this allows it to show up in
            //the ActiveX control container
            RegistryKey ctrl = k.CreateSubKey("Control");

            ctrl.Close();

            //Next create the CodeBase entry - needed if not string named and GACced.
            RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
            inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);
            inprocServer32.Close();

            //Finally close the main key
            k.Close();
        }

        [ComUnregisterFunction()]
        public static void UnregisterClass(string Key)
        {
            StringBuilder sb = new StringBuilder(Key);
            sb.Replace(@"HKEY_CLASSES_ROOT\", "");

            //Open HKCR\CLSID\{guid} for write access
            RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);

            //Delete the 'Control' key, but don't throw an exception if it does not exist
            k.DeleteSubKey("Control", false);

            //Next open InprocServer32
            RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);

            //And delete the CodeBase key, again not throwing if missing
            k.DeleteSubKey("CodeBase", false);

            //Finally close the main key
            k.Close();
        }
        #endregion

        #region IObjectSafety 멤버
        public void GetInterfaceSafetyOptions(ref Guid riid, out int supportedOptions, out int enabledOptions)
        {
            supportedOptions = enabledOptions = (int)ObjectSafetyFlags.SafeForScripting;
        }

        public void SetInterfaceSafetyOptions(ref Guid riid, int optionSetMask, int enabledOptions)
        {
        }
        #endregion

        string m_strSavePath = System.Environment.GetFolderPath(Environment.SpecialFolder.Templates);

        public Server()
        {
            InitializeComponent();
        }

        #region IActiveX_Method
        public string dispFunction(string str)
        {
                       string strRes;

                        return strRes;
        }
        #endregion
  }


App::InitInstance 에서 AfxEnableControlContainer(); 함수를 꼭 호출해주어야 한다.

'develop > windows' 카테고리의 다른 글

C#에서 클래스 라이브러리 동적 로딩  (0) 2010.09.29
C# ActiveX 만들기  (0) 2010.09.29
IObjectSafety  (1) 2010.02.03
BOOST 컴파일 옵션  (0) 2010.02.03
메모리릭 발생 위치에 Breakpoint 설정하기  (0) 2010.02.03

+ Recent posts