Module: Winreg

Defined in:
lib/winreg.rb,
lib/winreg/version.rb,
ext/winreg/winreg.c

Overview

winreg — typed Windows registry access for Ruby: exact wire formats, least-privilege opens, WOW64 views as a first-class option, and change notification that cooperates with a fiber scheduler.

Winreg.open('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion') do |k|
k.string("ProductName")             # => "Windows 10 ..."
k.read("CurrentMajorVersionNumber") # => [:dword, 10]
end

Winreg.create('HKCU\Software\Vendor\App') do |k|
k.write_multi_string("Plugins", %w[alpha beta]) # correct double-NUL wire format
k.watch(filter: :values) { |event| break if event == :deleted }
end

Defined Under Namespace

Classes: AccessDenied, Closed, Error, Key, KeyDeleted, MalformedValue, NotFound, OSError, TypeMismatch, Watch

Constant Summary collapse

TYPES =

Registry type tags (verified, winnt.h) — Symbol <-> tag map.

{
  none: 0, sz: 1, expand_sz: 2, binary: 3, dword: 4,
  dword_be: 5, link: 6, multi_sz: 7, qword: 11
}.freeze
KEY_QUERY_VALUE =

---- Win32 flag values (verified, winnt.h) ------------------------------

0x0001
KEY_NOTIFY =
0x0010
KEY_READ =

STANDARD_RIGHTS_READ | QUERY_VALUE | ENUMERATE_SUB_KEYS | NOTIFY

0x20019
KEY_WRITE =

STANDARD_RIGHTS_WRITE | SET_VALUE | CREATE_SUB_KEY

0x20006
KEY_WOW64_64KEY =
0x0100
KEY_WOW64_32KEY =
0x0200
REG_NOTIFY_CHANGE_NAME =
0x1
REG_NOTIFY_CHANGE_ATTRIBUTES =
0x2
REG_NOTIFY_CHANGE_LAST_SET =
0x4
REG_NOTIFY_CHANGE_SECURITY =
0x8
VERSION =
"0.1.0"
HKEY_CLASSES_ROOT =

---- predefined root handle values (verified, winreg.h) ---- Public-but-undocumented plumbing for the Ruby layer's path parser. HKEY_PERFORMANCE_DATA (0x80000004) is deliberately not defined.

ULONG2NUM(0x80000000UL)
HKEY_CURRENT_USER =
ULONG2NUM(0x80000001UL)
HKEY_LOCAL_MACHINE =
ULONG2NUM(0x80000002UL)
HKEY_USERS =
ULONG2NUM(0x80000003UL)
HKEY_CURRENT_CONFIG =
ULONG2NUM(0x80000005UL)

Class Method Summary collapse

Class Method Details

._expand(str) ⇒ Object

=====================================================================

Winreg._expand — ExpandEnvironmentStringsW



825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'ext/winreg/winreg.c', line 825

static VALUE
wr_expand(VALUE mod, VALUE str)
{
    VALUE wsrc = to_wide_val(str); /* Ruby-owned: raise-safe across allocs */
    VALUE wout, out;
    DWORD n, r;

    /* size probe: returned size is in WCHARs and includes the NUL */
    n = ExpandEnvironmentStringsW((const WCHAR *)RSTRING_PTR(wsrc), NULL, 0);
    if (n == 0) raise_gle("ExpandEnvironmentStrings", GetLastError());
    wout = rb_str_new(NULL, (long)n * (long)sizeof(WCHAR));
    for (;;) {
        /* re-take both pointers after any Ruby allocation */
        r = ExpandEnvironmentStringsW((const WCHAR *)RSTRING_PTR(wsrc),
                                      (WCHAR *)RSTRING_PTR(wout), n);
        if (r == 0) raise_gle("ExpandEnvironmentStrings", GetLastError());
        if (r <= n) { n = r; break; }
        n = r; /* grew between calls (env mutated): resize and retry */
        rb_str_resize(wout, (long)n * (long)sizeof(WCHAR));
    }
    /* n includes the terminating NUL; exclude it from the result */
    out = wide_to_utf8((const WCHAR *)RSTRING_PTR(wout), (int)(n - 1));
    RB_GC_GUARD(wsrc);
    RB_GC_GUARD(wout);
    return out;
}

.create(path, access: :read_write, view: :default) ⇒ Object

Open-or-create a key (creates all missing intermediate keys). Default access is :read_write — you create keys to write to them. The security descriptor is inherited from the parent (NULL SECURITY_ATTRIBUTES).



136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/winreg.rb', line 136

def create(path, access: :read_write, view: :default)
  root, sub = parse_path(path)
  sam = access_mask(access) | view_mask(view)
  k = sub.nil? ? Key.send(:_open, root, nil, sam) : Key.send(:_create, root, sub, sam)
  k.send(:_init_meta, root, sub, view)
  return k unless block_given?

  begin
    yield k
  ensure
    k.close
  end
end

.expand_string(str) ⇒ Object

Expand %VAR% references using ExpandEnvironmentStringsW (NOT a Ruby gsub over ENV — semantics differ: unknown vars stay literal, exactly as the OS defines). Input/output UTF-8 String.



153
154
155
# File 'lib/winreg.rb', line 153

def expand_string(str)
  _expand(str)
end

.ms_for(timeout) ⇒ Object

Seconds (nil = infinite) -> milliseconds for the C layer (-1 = INFINITE). Never collapse a tiny-but-positive wait into a non-blocking poll.

Raises:

  • (ArgumentError)


105
106
107
108
109
110
111
112
113
# File 'lib/winreg.rb', line 105

def ms_for(timeout)
  return -1 if timeout.nil?

  t = Float(timeout)
  raise ArgumentError, "timeout must be non-negative, got #{timeout.inspect}" if t.negative?

  ms = (t * 1000).round
  ms.zero? && t.positive? ? 1 : ms
end

.open(path, access: :read, view: :default) ⇒ Object

Open an existing key. access: :read (default) | :read_write | Integer raw samDesired (must not contain the WOW64 view bits — the view comes from view:). view: :default | :v64 | :v32. Yields the Key (ensure-closed) if a block is given, returning the block's value.



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/winreg.rb', line 119

def open(path, access: :read, view: :default)
  root, sub = parse_path(path)
  sam = access_mask(access) | view_mask(view)
  k = Key.send(:_open, root, sub, sam)
  k.send(:_init_meta, root, sub, view)
  return k unless block_given?

  begin
    yield k
  ensure
    k.close
  end
end

.run_blockingObject

Run a blocking native call cooperatively. Under a Fiber scheduler (e.g. winloop) the call is offloaded to a worker Thread so the calling fiber parks (Thread#value routes through the scheduler) and the event loop keeps serving other fibers; with no scheduler it runs inline (the C call already releases the GVL). On fiber unwind the worker is killed+joined so it can't leak. Registry watch registrations are REG_NOTIFY_THREAD_AGNOSTIC, so the ephemeral workers are sound (a registration does not die with its thread).

Caveat: a fiber unwound (e.g. Timeout) after the worker observed :changed but before value delivery loses that one delivery — harmless here, because :changed is stateless ("go look") and the registration stays armed.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/winreg.rb', line 85

def run_blocking
  sched = Fiber.scheduler
  return yield unless sched

  worker = Thread.new do
    Thread.current.report_on_exception = false
    yield
  end
  begin
    worker.value
  ensure
    if worker.alive?
      worker.kill
      worker.join
    end
  end
end