Giter Site home page Giter Site logo

petergeoghegan / pg_hexedit Goto Github PK

View Code? Open in Web Editor NEW
143.0 13.0 17.0 4.18 MB

Open PostgreSQL relation files in a hex editor with tags and annotations

License: Other

Makefile 1.56% Shell 10.08% C 88.35%
postgresql postgres hexeditor corruption btree-indexes wxhexeditor hex-editor

pg_hexedit's Introduction

pg_hexedit

Open PostgreSQL relation files in a hex editor with tags and annotations

Copyright (c) 2018-2021, Crunchy Data Solutions, Inc.
Copyright (c) 2017-2018, VMware, Inc.
Copyright (c) 2002-2010, Red Hat, Inc.
Copyright (c) 2011-2021, PostgreSQL Global Development Group

Author: Peter Geoghegan <[email protected]>
Author (pg_filedump): Patrick Macdonald <[email protected]>

License: GNU General Public License version 2

Build Status

Overview

pg_hexedit is an experimental toolkit to format PostgreSQL heap, sequence, and index files (B-Tree, GiST, GIN, hash, BRIN, and SP-GiST indexes) when opened within the open source GUI hex editor wxHexEditor. It makes viewing and editing PostgreSQL relation files significantly easier. PostgreSQL versions 12+ are supported.

wxHexEditor with pg_type wxHexEditor with pg_type system catalog table

wxHexEditor with pg_type_typname_nsp_index wxHexEditor with pg_type_typname_nsp_index system catalog index

CAUTION: Do not use pg_hexedit with a PostgreSQL data directory if you are not prepared to have it corrupt data! pg_hexedit is primarily made available for educational purposes. It is an experimental tool, originally used for simulating corruption/corruption analysis.

pg_hexedit is a fork of pg_filedump. The pg_hexedit frontend executable generates wxHexEditor format XML. pg_hexedit also provides "convenience" bash scripts. The scripts automate everything: they take the name of a table or index as an argument, connect to a database and find where the underlying file for the table or index is located, generate wxHexEditor format XML for the relevant file, and open the wxHexEditor GUI to display the relevant file with tags and annotations.

Annotation, tags, and the use of color

pg_hexedit uses color to convey conceptual similarities between distinct fields. For example, the t_infomask2 and t_infomask heap tuple fields are both green. Font color is sometimes used to convey the status of a value contained within a particular field, without that information necessarily being directly inferred from the affected field (it could come from another metadata field within the same tuple). For example, MultiXact xmax values have green text, though the enclosing xmax tag's color is unaffected (it is always red). Similarly, non-contrasting font colors are used to deemphasize the contents of a field where the contents are redundant or otherwise insignificant. For example, in the common case where a never-updated heap tuple's t_ctid fields point to the heap tuple itself, a non-contrasting font color is used (a shade of blue is used that is very similar to the enclosing t_ctid field color).

pg_hexedit annotations use field names and status flags that are grep'able from a PostgreSQL source code directory. While pg_hexedit aims to make interpreting the contents of pages as intuitive as possible, it does not go as far as interpreting the contents on the user's behalf. For example, t_infomask flag bits appear in annotations as a simple combination of "base" flag bits, rather than presenting the user with the logical state of t_infomask bits based on the combination of bits set: composite/logical flags such as HEAP_XMIN_FROZEN and HEAP_XMAX_SHR_LOCK will never appear in t_infomask annotations.

pg_hexedit dynamically assigns colors to columns/attributes based on the attribute name. Indexes will have the same attribute field colors as the corresponding attributes in the tables that are indexed, since their pg_attribute.attname values will match.

See also:
PostgreSQL documentation - heap_tuple_infomask_flags() contrib/pageinspect function
PostgreSQL documentation - Database Page Layout
PostgreSQL documentation - Preventing Transaction ID Wraparound Failures (xmin and xmax fields, Multixacts, and freezing)

Initial setup on Linux

There are no pg_hexedit packages available. pg_hexedit follows a simple trunk-based development model without any formal assigned versions. The only way to get pg_hexedit is to build it from source yourself. This is a deliberate trade-off; pg_hexedit is experimental, and it's not difficult to build from source anyway.

Building pg_hexedit

To build pg_hexedit you will need to have a PostgreSQL source tree or complete install tree (with include files). PostgreSQL system packages should provide all you need. If you run into trouble when building against system packages, please open a Github issue.

The Makefile builds the pg_hexedit and pg_filenodemapdata frontend utility programs. pg_config must be visible in your $PATH. To build pg_hexedit from within the source directory:

  $ which pg_config # Check which Postgres installation you'll build against:
  /code/postgresql/REL_16_STABLE/install/bin/pg_config
  $ make

Once this step succeeds, you can usually move on to the next initial setup step (which is to install wxHexEditor). You probably won't need to actually install the pg_hexedit executables you just built. (You'll probably only ever use pg_hexedit through the convenience bash scripts, which are designed to be run from the pg_hexedit source directory directly. The convenience scripts expect to find the pg_hexedit frontend program in the current working directory.)

If you really want to install the pg_hexedit frontend utility programs, you can do so in the usual way:

  $ make install

Note that the convenience scripts won't be installed.

Installing wxHexEditor

There are wxHexEditor packages available on major Linux platforms, though they all seem to use older versions. It is recommended that you use wxHexEditor version 0.25 or higher, since that version has fixes for a couple of bugs that affect the tag feature that pg_hexedit targets (see also: wxHexEditor bug 87). At the time of writing there are no version 0.25 packages, so installing 0.25 actually means building wxHexEditor from its git master branch. You can get by with an older version, though you can expect some aspects of how tags are displayed to be glitchy.

Installing wxHexEditor system package (easier, currently not recommended)

On Debian-based systems you can install wxhexeditor in the usual way:

  $ sudo apt-get install wxhexeditor

Building wxHexEditor from source

It's only mildly inconvenient to build wxHexEditor on a modern desktop Linux system. On Debian-based systems with source repositories set up, obtaining all build dependencies quickly should be straightforward:

  $ sudo apt-get build-dep wxhexeditor
  $ # (Install dependencies)
  $ sudo apt-get install libtool

Note that this doesn't actually install any wxHexEditor package. It just installs the dependencies to build the wxHexEditor source package, which, along with libtool, should be all we need to build wxHexEditor from git tip.

Build wxHexEditor's master branch from source/git by cloning the official git repo, and following the instructions that it provides:

https://github.com/EUA/wxHexEditor

Initial setup on MacOS

These steps should help you install pg_hexedit on MacOS (tested on Catalina with installed Homebrew):

  1. brew install automake autoconf libtool wxmac
  2. Get wxHexEditor source code and edit Makefile removing the line mentioning OpenMP (LIBS += -lgomp)
  3. Build wxHexEditor (make)
  4. Get pg_hexedit source code, build (make) with $PATH including path to PostgreSQL binaries (Postgres installed via Homebrew โ€“ brew install postgresql โ€“ should be enough)
  5. In hexedit.cfg, check export HEXEDITOR, it needs to point to the wxHexEditor binary (created above)

Note: The convenience scripts set $HOME to the current working directory so that wxHexEditor reads its settings from a convenience-script-generated .wxHexEditor config file rather than the true user-wide/system-wide config file. This is like an .ini file. This hack is currently broken on MacOS. The relation_hexedit script works, but not exactly as designed. For example, the optimal "Bytes per line limit" for pg_hexedit seems to be 32, but that's not the default.

(When wxHexEditor is built on MacOS, it will look for the config file in /Library/Preferences or ~/Library/Preferences - not in the user's home directory. It's not clear how the hack that works on Linux can be adopted to MacOS.)

Suggested partial workaround: comment out the HOME=$(pwd) line in the __open_relation file (which is used by relation_hexedit). This will at least allow you to start up pg_hexedit in a way that works with your system/user wide wxHexEditor config.

Quickstart guide - Using the convenience scripts

pg_hexedit and wxHexEditor can be invoked using convenience scripts that take care of everything. These are designed to be run on a PostgreSQL backend hacker's laptop, while the target PostgreSQL server is actually running. The server is queried to locate the relevant relation files. The scripts also take care of adding convenience offsets to the wxHexEditor cache, which can be used to quickly locate internal pages of a B-Tree, for example. The wxHexEditor shortcut for accessing the offsets is Ctrl + G.

The convenience scripts will download a portion of the relation using psql in the event of not finding relation files at the expected location on the filesystem. For the fallback to work, contrib/pageinspect must be installed and tagging must start at the beginning of the relation file (see hexedit.cfg).

The convenience scripts automate away starting pg_hexedit in test environments, but it is still highly recommended that you familiarize yourself with PostgreSQL's file layout. See: PostgreSQL documentation - Database File Layout.

Requirements

psql should be within your $PATH when the scripts are invoked. libpq environment variables like $PGDATABASE can be set within the hexedit.cfg file. These control what database is opened by wxHexEditor, and other such standard details. Note that just like pg_filedump, pg_hexeditor has no dependency on a running server, and is generally safer to use offline, despite the fact that it is typically used online. It is convenient to invoke wxHexEditor using the scripts provided during analysis of in situ issues, or when learning about PostgreSQL internals.

Having a PostgreSQL relfilenode file open in a hex editor risks data corruption, especially when the PostgreSQL server is actually running throughout. The scripts were designed with backend development convenience in mind, where the database should only contain disposable test data.

Convenience script assumptions:

  • The scripts initially assume that they're run as an OS user that has the operating system level permissions needed to open/read all PostgreSQL relation files, using the same absolute paths as PostgreSQL. Be very careful if the Postgres data directory is containerized; a convenience script might open relation files from an unrelated installation if this assumption is not fully met.

  • Most convenience scripts rely on CREATE EXTENSION IF NOT EXISTS pageinspect running and making available various SQL-callable functions. These functions are used to generate interesting offsets, or to display hints on index structure. (This is highly recommended, but not actually required.)

  • psql must connect using a PostgreSQL role with superuser permissions. This is needed to determine the path of Postgres relfiles (and to install contrib/pageinspect).

contrib/pageinspect must be available (the extensions supporting files must be installed) to use the convenience scripts that depend on contrib/pageinspect. Note that the relation_hexedit script does not depend on contrib/pageinspect. relation_hexedit is designed to work equally well with relations of any access method, and uses simple convenience offsets (decile offsets).

To open the Postgres table pg_type with tags and annotations:

  # Should be invoked with CWD that finds pg_hexedit executable:
  $ pwd
  /home/pg/code/pg_hexedit
  # Confirm configuration:
  $ $EDITOR hexedit.cfg
  # Invoke generic script (works on tables and indexes):
  $ ./relation_hexedit pg_type
Replacing /home/pg/code/pg_hexedit/.wxHexEditor with pg_hexedit optimized settings...
...

To open the Postgres B-Tree pg_type_typname_nsp_index with tags and annotations:

  $ ./btree_hexedit pg_type_typname_nsp_index
Replacing /home/pg/code/pg_hexedit/.wxHexEditor with pg_hexedit optimized settings...
...

The advantage of using the btree_hexedit script for B-Tree indexes over the generic relation_hexedit script is that btree_hexedit sets offsets for every non-leaf block that is a direct child of the root page (and for the root page itself).

There is also a gin_hexedit convenience script. This does not set offsets automatically. Instead, it runs an SQL query that summarizes contiguous ranges within the index based on block type (this is output to stdout). Byte-wise offsets are output, which may be manually input using the offsets dialog. GIN indexes are often made up of a fairly small number of contiguous ranges of a single page type (e.g., 'data', 'leaf'), so a high level summary can help when locating the section that is of interest. Note that there might be fragmentation (many distinct, smaller contiguous ranges) in uncommon cases.

The scripts will only open the first 1GB segment file in the relation. Note also that these convenience scripts limit the range of blocks that are summarized, to keep the overhead acceptable. (This can be changed by modifying hexedit.cfg.)

If there is concurrent write activity by Postgres, the process of building XML tags may error out before finishing. In practice there is unlikely to be trouble. The scripts perform a CHECKPOINT before opening relation files.

Getting acceptable performance

While wxHexEditor compares favorably with other hex editors when tasked with editing very large files, it appears to be far more likely to become unresponsive when there are many tags. It may be necessary to work around this limitation at some point.

Generalize from the example of the convenience scripts for guidance on this. Limiting the range that is summarized can be very effective in simple cases.

pg_hexedit's -x flag can be used to specify a page LSN before which pages should not have tags emitted. This is another option to reduce the overhead of tags within wxHexEditor by avoiding generating tags for non-interesting blocks/pages in the first place. It can be useful during debugging to specify an LSN that is only a few checkpoints old, to limit annotations to recently modified blocks. This advanced option isn't used by the convenience scripts. pg_waldump may be used to find a relevant cutoff point's LSN value (e.g., based on a commit WAL record's timestamp value).

pg_hexedit's -l flag can be used when the target is an nbtree, GiST, GIN, or SP-GiST index relation. This will have pg_hexedit emit all-green, single-page tags for leaf pages. Leaf pages can be much less interesting than internal pages in some debugging scenarios. For example, when the balance of a tree structure must be examined, only the internal pages are relevant. This is yet another option for limiting the number of tags generated to control overhead within wxHexEditor. This advanced option isn't currently used by the convenience scripts.

Direct invocation

Invoking pg_hexedit directly (not using convenience scripts) is useful when you want to work on a copy of the database that is not under the control of a running PostgreSQL server, or when a psql connection to the running PostgreSQL server cannot be established. pg_hexedit should have stdout redirected to a file. wxHexEditor will automatically open tags for a target file when it is opened and a tag file is found in the same directory (provided the tag file has the same name as the target file with a ".tags" extension postfixed). Alternatively, tags can be directly imported once wxHexEditor has opened a file.

pg_hexedit retains a minority of the flags that appear in pg_filedump. Usage:

  pg_hexedit [options] file

The -D flag can be used to decode tuples. The flag should be followed by a tuple descriptor string in pg_hexedit's "attrlist" format. Decoding allows pg_hexedit to generate distinct tags for each user attribute/column value in each tuple, rather than just creating a single tag for all column data within each tuple. The attrlist format consists of a list with an entry for each pg_attribute entry's attlen, attname, and attalign, which should be specified as:

-D 'attlen,attname,attalign,attlen,attname,attalign,...'

Each attribute's triple of metadata should appear in pg_attribute.attnum order. The convenience scripts use an SQL query to form the string (see the __open_relation utility script). The attrlist SQL query will produce a correct -D argument when run against a relation with the same schema as the target relation/file, even when run against an unrelated PostgreSQL installation, provided a compatible CPU architecture is used. Note that dropped columns need to be represented in the attrlist string. Elements that contain whitespace or comma characters can be parsed as a single element by appearing within double quotes. It's good practice to use single quotes for the attrlist argument as a whole.

See pg_hexedit -h for full details of all available options.

Using pg_hexedit while debugging Postgres with GDB

Sometimes, it is useful to invoke pg_hexedit/wxHexEditor to visualize an arbitrary Postgres page image from within an interactive GDB debugging session that is attached to a Postgres backend. This also works well with core dumps.

Generalize from the following example, which can be added to your .gdbinit dotfile:

define dump_page
  dump binary memory /tmp/gdb_postgres_page.dump $arg0 ($arg0 + 8192)
  echo Invoking pg_hexedit + wxHexEditor on page...\n
  ! ~/code/pg_hexedit/pg_hexedit -n 1 /tmp/gdb_postgres_page.dump > /tmp/gdb_postgres_page.dump.tags
  ! ~/code/wxHexEditor/wxHexEditor /tmp/gdb_postgres_page.dump &> /dev/null
end

This creates a user-defined GDB command that dumps 8192 bytes (default BLCKSZ) at an arbitrary memory address to a temp file. From there, pg_hexedit is invoked on the temp file.

If we wanted to see a page image of an nbtree page that is about to be split, we can set a breakpoint within _bt_split(), insert tuples until there is a page split, and invoke pg_hexedit using the dump_page GDB command:

Breakpoint 1, _bt_split (rel=0x7f555b6f3460, itup_key=0x55d03a745d40, buf=232, cbuf=0, firstright=366, newitemoff=216, newitemsz=16, newitem=0x55d03a745d18, newitemonleft=true) at nbtinsert.c:1205
1205	{
(gdb) n
1215		Buffer		sbuf = InvalidBuffer;
(gdb)
1216		Page		spage = NULL;
(gdb)
1217		BTPageOpaque sopaque = NULL;
(gdb)
1227		int			indnatts = IndexRelationGetNumberOfAttributes(rel);
(gdb)
1228		int			indnkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
(gdb)
1231		rbuf = _bt_getbuf(rel, P_NEW, BT_WRITE);
(gdb)
1244		origpage = BufferGetPage(buf);
(gdb)
1245		leftpage = PageGetTempPage(origpage);
(gdb)
1246		rightpage = BufferGetPage(rbuf);
(gdb)
1248		origpagenumber = BufferGetBlockNumber(buf);
(gdb)
1249		rightpagenumber = BufferGetBlockNumber(rbuf);
(gdb) dump_page origpage
Invoking pg_hexedit + wxHexEditor on page...

Note that the block number displayed by pg_hexedit's annotations will be spurious when produced by dump_page, because there is no general way to determine the correct block number without more context. This is usually only a minor annoyance, though.

Determining catalog relation file mappings without a database connection

pg_filenodemapdata is a program that prints the contents of a specified catalog relation to relfile mapping (pg_filenode.map) file. It is distributed with pg_hexedit. pg_filenodemapdata can be used to determine which system catalog relfiles to examine in cases where a database connection cannot be established due to severe system catalog corruption. It's usually easier to figure this out using the generic SQL-based approach that the convenience scripts take, but that isn't always possible.

To print the mappings for the database with pg_database OID 12389 (and verify the pg_filenode.map checksum in passing):

  $ cd $PGDATA
  $ pg_filenodemapdata base/12389/pg_filenode.map
    magic:               0x00592717
    num_mappings:        15

     0) 1259 - pg_class:                                  1259
     1) 1249 - pg_attribute:                              1249
     2) 1255 - pg_proc:                                   1255
     3) 1247 - pg_type:                                   1247
     4) 2836 - pg_toast_1255:                             2836
     5) 2837 - pg_toast_1255_index:                       2837
     6) 2658 - pg_attribute_relid_attnam_index:           2658
     7) 2659 - pg_attribute_relid_attnum_index:           2659
     8) 2662 - pg_class_oid_index:                        2662
     9) 2663 - pg_class_relname_nsp_index:                2663
    10) 3455 - pg_class_tblspc_relfilenode_index:         3455
    11) 2690 - pg_proc_oid_index:                         2690
    12) 2691 - pg_proc_proname_args_nsp_index:            2691
    13) 2703 - pg_type_oid_index:                         2703
    14) 2704 - pg_type_typname_nsp_index:                 2704

    file checksum:       0x3AA59965

In this example, all mapped system catalog relations within the database have relfilenode numbers that match their universal, fixed pg_class OID identifiers. This is often not the case, though. Operations like VACUUM FULL will assign a new relfilenode to the target table relation, and to all associated index relations.

Installations with many small databases may require an additional step. It may be unclear which subdirectory of the base directory corresponds to a database that happens to be of interest. The relevant metadata is stored in the pg_database shared system catalog: the names of base subdirectories correspond to a pg_database entry OID. The location of the relfile for the global pg_database table might need to be determined first, so that a particular base directory can be identified:

  $ pg_filenodemapdata global/pg_filenode.map | grep pg_database
     0) 1262 - pg_database:                               1262
    21) 2671 - pg_database_datname_index:                 2671
    22) 2672 - pg_database_oid_index:                     2672

The file global/1262 can now be opened using wxHexEditor, to examine the contents of pg_database. pg_database tuples contain a database name, so it should be possible to search for the entry of interest using the wxHexEditor search dialog. The OID for each tuple/database is a 4 byte unsigned integer that appears in heap tuple headers with HEAP_HASOID set.

Supporting other hex editors

While pg_hexedit targets wxHexEditor, it should not be difficult to adopt it to other hex editors with a similar tag import feature if that becomes a requirement in the future. (Alternative hex editors should ideally be able to represent tag color using HTML color codes, and support setting both font and tag colors.)

Actually generating raw tag output is confined to the following simple C functions:

EmitXmlDocHeader()
EmitXmlFooter()
EmitXmlTag()
EmitXmlItemId()
EmitXmlTupleTag()
EmitXmlTupleTagFont()
EmitXmlTupleTagFontTwoName()

These routines could be changed to call a per-hexeditor callback. Each supported hex editor could have its own "provider" routines.

pg_hexedit's People

Contributors

ardentperf avatar miles170 avatar nikolays avatar petergeoghegan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pg_hexedit's Issues

Installation issues on a modern Ubuntu 20.04, 18.04, etc (SOLVED)

On a clean Ubuntu 20.04, just installed Postgres 13 with additional packages, and got this:

$ make
cc -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -L/usr/lib/llvm-9/lib -Wl,--as-needed  -o pg_hexedit pg_hexedit.o -L/usr/lib/x86_64-linux-gnu -lpgport -lpgcommon
/usr/bin/ld: cannot find -lpgport
/usr/bin/ld: cannot find -lpgcommon
collect2: error: ld returned 1 exit status

Why: zalando/spilo#228 (comment)

Workaround (https://github.com/zalando/spilo/pull/229/files):

$ version=13
$ sudo ln -snf /usr/lib/postgresql/${version}/lib/libpgcommon.a /usr/lib/x86_64-linux-gnu/libpgcommon.a
$ sudo ln -snf /usr/lib/postgresql/${version}/lib/libpgport.a /usr/lib/x86_64-linux-gnu/libpgport.a
$ make
cc -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -L/usr/lib/llvm-9/lib -Wl,--as-needed  -o pg_hexedit pg_hexedit.o -L/usr/lib/x86_64-linux-gnu -lpgport -lpgcommon
cc -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -L/usr/lib/llvm-9/lib -Wl,--as-needed  -o pg_filenodemapdata pg_filenodemapdata.o -L/usr/lib/x86_64-linux-gnu -lpgport

Add CI to pg_hexedit using cirrus-ci.org

When Travis CI support was dropped, we never replaced it with another free CI provider. That should happen at some point, and should test all Postgres branches for major versions that are under support by the Postgres community (just as it was back when Travis CI could still be used).

Presumably we'd want to use cirrus-ci.org for this, since that's what Postgres itself uses.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.